From 62549e6c9818f422698f030e0b242609115493ed Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 21 Oct 2021 16:28:39 +0200 Subject: [PATCH] Rewrite youtube-dl import Use python3 binary Allows to use a custom youtube-dl release URL Allows to use yt-dlp (youtube-dl fork) Remove proxy config from configuration to use HTTP_PROXY and HTTPS_PROXY env variables --- config/default.yaml | 17 +- config/production.yaml.example | 5 +- config/test-1.yaml | 1 + config/test-2.yaml | 1 + config/test-3.yaml | 1 + config/test-4.yaml | 1 + config/test-5.yaml | 1 + config/test-6.yaml | 1 + config/test.yaml | 3 - package.json | 4 +- server/controllers/api/videos/import.ts | 10 +- server/helpers/requests.ts | 9 +- server/helpers/youtube-dl.ts | 394 ----------- server/helpers/youtube-dl/index.ts | 3 + server/helpers/youtube-dl/youtube-dl-cli.ts | 198 ++++++ .../youtube-dl/youtube-dl-info-builder.ts | 154 +++++ .../helpers/youtube-dl/youtube-dl-wrapper.ts | 135 ++++ server/initializers/config.ts | 13 +- server/initializers/constants.ts | 7 + server/lib/job-queue/handlers/video-import.ts | 6 +- .../schedulers/youtube-dl-update-scheduler.ts | 4 +- server/tests/api/server/proxy.ts | 107 ++- server/tests/api/videos/video-imports.ts | 632 ++++++++++-------- .../fixtures/video_import_preview_yt_dlp.jpg | Bin 0 -> 15844 bytes .../video_import_thumbnail_yt_dlp.jpg | Bin 0 -> 10163 bytes server/tools/peertube-import-videos.ts | 179 ++--- shared/extra-utils/miscs/tests.ts | 2 +- shared/extra-utils/videos/captions.ts | 8 +- .../docker/production/config/production.yaml | 1 + yarn.lock | 397 +---------- 30 files changed, 1053 insertions(+), 1241 deletions(-) delete mode 100644 server/helpers/youtube-dl.ts create mode 100644 server/helpers/youtube-dl/index.ts create mode 100644 server/helpers/youtube-dl/youtube-dl-cli.ts create mode 100644 server/helpers/youtube-dl/youtube-dl-info-builder.ts create mode 100644 server/helpers/youtube-dl/youtube-dl-wrapper.ts create mode 100644 server/tests/fixtures/video_import_preview_yt_dlp.jpg create mode 100644 server/tests/fixtures/video_import_thumbnail_yt_dlp.jpg diff --git a/config/default.yaml b/config/default.yaml index c46d0e883..ec9622477 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -85,6 +85,7 @@ client: # From the project root directory storage: tmp: 'storage/tmp/' # Use to download data (imports etc), store uploaded files before and during processing... + bin: 'storage/bin/' avatars: 'storage/avatars/' videos: 'storage/videos/' streaming_playlists: 'storage/streaming-playlists/' @@ -394,13 +395,21 @@ import: http: # Classic HTTP or all sites supported by youtube-dl https://rg3.github.io/youtube-dl/supportedsites.html enabled: false + youtube_dl_release: + # Direct download URL to youtube-dl binary + # Github releases API is also supported + # Examples: + # * https://api.github.com/repos/ytdl-org/youtube-dl/releases + # * https://api.github.com/repos/yt-dlp/yt-dlp/releases + url: 'https://yt-dl.org/downloads/latest/youtube-dl' + + # youtube-dl binary name + # yt-dlp is also supported + name: 'youtube-dl' + # IPv6 is very strongly rate-limited on most sites supported by youtube-dl force_ipv4: false - # You can use an HTTP/HTTPS/SOCKS proxy with youtube-dl - proxy: - enabled: false - url: '' torrent: # Magnet URI or torrent file (use classic TCP/UDP/WebSeed to download the file) enabled: false diff --git a/config/production.yaml.example b/config/production.yaml.example index d023070e3..588d6a3a5 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example @@ -83,6 +83,7 @@ client: # From the project root directory storage: tmp: '/var/www/peertube/storage/tmp/' # Use to download data (imports etc), store uploaded files before and during processing... + bin: '/var/www/peertube/storage/bin/' avatars: '/var/www/peertube/storage/avatars/' videos: '/var/www/peertube/storage/videos/' streaming_playlists: '/var/www/peertube/storage/streaming-playlists/' @@ -407,10 +408,6 @@ import: # IPv6 is very strongly rate-limited on most sites supported by youtube-dl force_ipv4: false - # You can use an HTTP/HTTPS/SOCKS proxy with youtube-dl - proxy: - enabled: false - url: '' torrent: # Magnet URI or torrent file (use classic TCP/UDP/WebSeed to download the file) enabled: false diff --git a/config/test-1.yaml b/config/test-1.yaml index fe5b3cf44..d5f8299e0 100644 --- a/config/test-1.yaml +++ b/config/test-1.yaml @@ -11,6 +11,7 @@ database: # From the project root directory storage: tmp: 'test1/tmp/' + bin: 'test1/bin/' avatars: 'test1/avatars/' videos: 'test1/videos/' streaming_playlists: 'test1/streaming-playlists/' diff --git a/config/test-2.yaml b/config/test-2.yaml index b559769c3..9da79da16 100644 --- a/config/test-2.yaml +++ b/config/test-2.yaml @@ -11,6 +11,7 @@ database: # From the project root directory storage: tmp: 'test2/tmp/' + bin: 'test2/bin/' avatars: 'test2/avatars/' videos: 'test2/videos/' streaming_playlists: 'test2/streaming-playlists/' diff --git a/config/test-3.yaml b/config/test-3.yaml index 9a7a944e9..594439b62 100644 --- a/config/test-3.yaml +++ b/config/test-3.yaml @@ -11,6 +11,7 @@ database: # From the project root directory storage: tmp: 'test3/tmp/' + bin: 'test3/bin/' avatars: 'test3/avatars/' videos: 'test3/videos/' streaming_playlists: 'test3/streaming-playlists/' diff --git a/config/test-4.yaml b/config/test-4.yaml index 1e4bee974..1e6368bf7 100644 --- a/config/test-4.yaml +++ b/config/test-4.yaml @@ -11,6 +11,7 @@ database: # From the project root directory storage: tmp: 'test4/tmp/' + bin: 'test4/bin/' avatars: 'test4/avatars/' videos: 'test4/videos/' streaming_playlists: 'test4/streaming-playlists/' diff --git a/config/test-5.yaml b/config/test-5.yaml index 9725e84f4..97f18a7a0 100644 --- a/config/test-5.yaml +++ b/config/test-5.yaml @@ -11,6 +11,7 @@ database: # From the project root directory storage: tmp: 'test5/tmp/' + bin: 'test5/bin/' avatars: 'test5/avatars/' videos: 'test5/videos/' streaming_playlists: 'test5/streaming-playlists/' diff --git a/config/test-6.yaml b/config/test-6.yaml index a04c8a6a9..156da84d2 100644 --- a/config/test-6.yaml +++ b/config/test-6.yaml @@ -11,6 +11,7 @@ database: # From the project root directory storage: tmp: 'test6/tmp/' + bin: 'test6/bin/' avatars: 'test6/avatars/' videos: 'test6/videos/' streaming_playlists: 'test6/streaming-playlists/' diff --git a/config/test.yaml b/config/test.yaml index 9a522a983..3eb2f04d8 100644 --- a/config/test.yaml +++ b/config/test.yaml @@ -118,9 +118,6 @@ import: concurrency: 2 http: enabled: true - proxy: - enabled: false - url: "" torrent: enabled: true diff --git a/package.json b/package.json index e1ddf1168..0737df7d5 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "decache": "^4.6.0", "deep-object-diff": "^1.1.0", "email-templates": "^8.0.3", + "execa": "^5.1.1", "express": "^4.12.4", "express-rate-limit": "^5.0.0", "express-validator": "^6.4.0", @@ -144,8 +145,7 @@ "webfinger.js": "^2.6.6", "webtorrent": "^1.0.0", "winston": "3.3.3", - "ws": "^8.0.0", - "youtube-dl": "^3.0.2" + "ws": "^8.0.0" }, "devDependencies": { "@types/async": "^3.0.0", diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts index 4265f3217..eddb9b32d 100644 --- a/server/controllers/api/videos/import.ts +++ b/server/controllers/api/videos/import.ts @@ -26,7 +26,7 @@ import { isArray } from '../../../helpers/custom-validators/misc' import { cleanUpReqFiles, createReqFiles } from '../../../helpers/express-utils' import { logger } from '../../../helpers/logger' import { getSecureTorrentName } from '../../../helpers/utils' -import { YoutubeDL, YoutubeDLInfo } from '../../../helpers/youtube-dl' +import { YoutubeDLWrapper, YoutubeDLInfo } from '../../../helpers/youtube-dl' import { CONFIG } from '../../../initializers/config' import { MIMETYPES } from '../../../initializers/constants' import { sequelizeTypescript } from '../../../initializers/database' @@ -134,12 +134,12 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response) const targetUrl = body.targetUrl const user = res.locals.oauth.token.User - const youtubeDL = new YoutubeDL(targetUrl, ServerConfigManager.Instance.getEnabledResolutions('vod')) + const youtubeDL = new YoutubeDLWrapper(targetUrl, ServerConfigManager.Instance.getEnabledResolutions('vod')) // Get video infos let youtubeDLInfo: YoutubeDLInfo try { - youtubeDLInfo = await youtubeDL.getYoutubeDLInfo() + youtubeDLInfo = await youtubeDL.getInfoForDownload() } catch (err) { logger.info('Cannot fetch information from import for URL %s.', targetUrl, { err }) @@ -373,9 +373,9 @@ function extractNameFromArray (name: string | string[]) { return isArray(name) ? name[0] : name } -async function processYoutubeSubtitles (youtubeDL: YoutubeDL, targetUrl: string, videoId: number) { +async function processYoutubeSubtitles (youtubeDL: YoutubeDLWrapper, targetUrl: string, videoId: number) { try { - const subtitles = await youtubeDL.getYoutubeDLSubs() + const subtitles = await youtubeDL.getSubtitles() logger.info('Will create %s subtitles from youtube import %s.', subtitles.length, targetUrl) diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts index 991270952..d93f55776 100644 --- a/server/helpers/requests.ts +++ b/server/helpers/requests.ts @@ -1,9 +1,9 @@ import { createWriteStream, remove } from 'fs-extra' -import got, { CancelableRequest, Options as GotOptions, RequestError } from 'got' +import got, { CancelableRequest, Options as GotOptions, RequestError, Response } from 'got' import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent' import { join } from 'path' import { CONFIG } from '../initializers/config' -import { ACTIVITY_PUB, PEERTUBE_VERSION, REQUEST_TIMEOUT, WEBSERVER } from '../initializers/constants' +import { ACTIVITY_PUB, BINARY_CONTENT_TYPES, PEERTUBE_VERSION, REQUEST_TIMEOUT, WEBSERVER } from '../initializers/constants' import { pipelinePromise } from './core-utils' import { processImage } from './image-utils' import { logger } from './logger' @@ -180,12 +180,17 @@ function getUserAgent () { return `PeerTube/${PEERTUBE_VERSION} (+${WEBSERVER.URL})` } +function isBinaryResponse (result: Response) { + return BINARY_CONTENT_TYPES.has(result.headers['content-type']) +} + // --------------------------------------------------------------------------- export { doRequest, doJSONRequest, doRequestAndSaveToFile, + isBinaryResponse, downloadImage, peertubeGot } diff --git a/server/helpers/youtube-dl.ts b/server/helpers/youtube-dl.ts deleted file mode 100644 index 0392ec4c7..000000000 --- a/server/helpers/youtube-dl.ts +++ /dev/null @@ -1,394 +0,0 @@ -import { createWriteStream } from 'fs' -import { ensureDir, move, pathExists, remove, writeFile } from 'fs-extra' -import { join } from 'path' -import { CONFIG } from '@server/initializers/config' -import { HttpStatusCode } from '../../shared/models/http/http-error-codes' -import { VideoResolution } from '../../shared/models/videos' -import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../initializers/constants' -import { peertubeTruncate, pipelinePromise, root } from './core-utils' -import { isVideoFileExtnameValid } from './custom-validators/videos' -import { logger } from './logger' -import { peertubeGot } from './requests' -import { generateVideoImportTmpPath } from './utils' - -export type YoutubeDLInfo = { - name?: string - description?: string - category?: number - language?: string - licence?: number - nsfw?: boolean - tags?: string[] - thumbnailUrl?: string - ext?: string - originallyPublishedAt?: Date -} - -export type YoutubeDLSubs = { - language: string - filename: string - path: string -}[] - -const processOptions = { - maxBuffer: 1024 * 1024 * 10 // 10MB -} - -class YoutubeDL { - - constructor (private readonly url: string = '', private readonly enabledResolutions: number[] = []) { - - } - - getYoutubeDLInfo (opts?: string[]): Promise { - return new Promise((res, rej) => { - let args = opts || [] - - if (CONFIG.IMPORT.VIDEOS.HTTP.FORCE_IPV4) { - args.push('--force-ipv4') - } - - args = this.wrapWithProxyOptions(args) - args = [ '-f', this.getYoutubeDLVideoFormat() ].concat(args) - - YoutubeDL.safeGetYoutubeDL() - .then(youtubeDL => { - youtubeDL.getInfo(this.url, args, processOptions, (err, info) => { - if (err) return rej(err) - if (info.is_live === true) return rej(new Error('Cannot download a live streaming.')) - - const obj = this.buildVideoInfo(this.normalizeObject(info)) - if (obj.name && obj.name.length < CONSTRAINTS_FIELDS.VIDEOS.NAME.min) obj.name += ' video' - - return res(obj) - }) - }) - .catch(err => rej(err)) - }) - } - - getYoutubeDLSubs (opts?: object): Promise { - return new Promise((res, rej) => { - const cwd = CONFIG.STORAGE.TMP_DIR - const options = opts || { all: true, format: 'vtt', cwd } - - YoutubeDL.safeGetYoutubeDL() - .then(youtubeDL => { - youtubeDL.getSubs(this.url, options, (err, files) => { - if (err) return rej(err) - if (!files) return [] - - logger.debug('Get subtitles from youtube dl.', { url: this.url, files }) - - const subtitles = files.reduce((acc, filename) => { - const matched = filename.match(/\.([a-z]{2})(-[a-z]+)?\.(vtt|ttml)/i) - if (!matched || !matched[1]) return acc - - return [ - ...acc, - { - language: matched[1], - path: join(cwd, filename), - filename - } - ] - }, []) - - return res(subtitles) - }) - }) - .catch(err => rej(err)) - }) - } - - getYoutubeDLVideoFormat () { - /** - * list of format selectors in order or preference - * see https://github.com/ytdl-org/youtube-dl#format-selection - * - * case #1 asks for a mp4 using h264 (avc1) and the exact resolution in the hope - * of being able to do a "quick-transcode" - * case #2 is the first fallback. No "quick-transcode" means we can get anything else (like vp9) - * case #3 is the resolution-degraded equivalent of #1, and already a pretty safe fallback - * - * in any case we avoid AV1, see https://github.com/Chocobozzz/PeerTube/issues/3499 - **/ - const resolution = this.enabledResolutions.length === 0 - ? VideoResolution.H_720P - : Math.max(...this.enabledResolutions) - - return [ - `bestvideo[vcodec^=avc1][height=${resolution}]+bestaudio[ext=m4a]`, // case #1 - `bestvideo[vcodec!*=av01][vcodec!*=vp9.2][height=${resolution}]+bestaudio`, // case #2 - `bestvideo[vcodec^=avc1][height<=${resolution}]+bestaudio[ext=m4a]`, // case #3 - `bestvideo[vcodec!*=av01][vcodec!*=vp9.2]+bestaudio`, - 'best[vcodec!*=av01][vcodec!*=vp9.2]', // case fallback for known formats - 'best' // Ultimate fallback - ].join('/') - } - - downloadYoutubeDLVideo (fileExt: string, timeout: number) { - // Leave empty the extension, youtube-dl will add it - const pathWithoutExtension = generateVideoImportTmpPath(this.url, '') - - let timer - - logger.info('Importing youtubeDL video %s to %s', this.url, pathWithoutExtension) - - let options = [ '-f', this.getYoutubeDLVideoFormat(), '-o', pathWithoutExtension ] - options = this.wrapWithProxyOptions(options) - - if (process.env.FFMPEG_PATH) { - options = options.concat([ '--ffmpeg-location', process.env.FFMPEG_PATH ]) - } - - logger.debug('YoutubeDL options for %s.', this.url, { options }) - - return new Promise((res, rej) => { - YoutubeDL.safeGetYoutubeDL() - .then(youtubeDL => { - youtubeDL.exec(this.url, options, processOptions, async err => { - clearTimeout(timer) - - try { - // If youtube-dl did not guess an extension for our file, just use .mp4 as default - if (await pathExists(pathWithoutExtension)) { - await move(pathWithoutExtension, pathWithoutExtension + '.mp4') - } - - const path = await this.guessVideoPathWithExtension(pathWithoutExtension, fileExt) - - if (err) { - remove(path) - .catch(err => logger.error('Cannot delete path on YoutubeDL error.', { err })) - - return rej(err) - } - - return res(path) - } catch (err) { - return rej(err) - } - }) - - timer = setTimeout(() => { - const err = new Error('YoutubeDL download timeout.') - - this.guessVideoPathWithExtension(pathWithoutExtension, fileExt) - .then(path => remove(path)) - .finally(() => rej(err)) - .catch(err => { - logger.error('Cannot remove file in youtubeDL timeout.', { err }) - return rej(err) - }) - }, timeout) - }) - .catch(err => rej(err)) - }) - } - - buildOriginallyPublishedAt (obj: any) { - let originallyPublishedAt: Date = null - - const uploadDateMatcher = /^(\d{4})(\d{2})(\d{2})$/.exec(obj.upload_date) - if (uploadDateMatcher) { - originallyPublishedAt = new Date() - originallyPublishedAt.setHours(0, 0, 0, 0) - - const year = parseInt(uploadDateMatcher[1], 10) - // Month starts from 0 - const month = parseInt(uploadDateMatcher[2], 10) - 1 - const day = parseInt(uploadDateMatcher[3], 10) - - originallyPublishedAt.setFullYear(year, month, day) - } - - return originallyPublishedAt - } - - private async guessVideoPathWithExtension (tmpPath: string, sourceExt: string) { - if (!isVideoFileExtnameValid(sourceExt)) { - throw new Error('Invalid video extension ' + sourceExt) - } - - const extensions = [ sourceExt, '.mp4', '.mkv', '.webm' ] - - for (const extension of extensions) { - const path = tmpPath + extension - - if (await pathExists(path)) return path - } - - throw new Error('Cannot guess path of ' + tmpPath) - } - - private normalizeObject (obj: any) { - const newObj: any = {} - - for (const key of Object.keys(obj)) { - // Deprecated key - if (key === 'resolution') continue - - const value = obj[key] - - if (typeof value === 'string') { - newObj[key] = value.normalize() - } else { - newObj[key] = value - } - } - - return newObj - } - - private buildVideoInfo (obj: any): YoutubeDLInfo { - return { - name: this.titleTruncation(obj.title), - description: this.descriptionTruncation(obj.description), - category: this.getCategory(obj.categories), - licence: this.getLicence(obj.license), - language: this.getLanguage(obj.language), - nsfw: this.isNSFW(obj), - tags: this.getTags(obj.tags), - thumbnailUrl: obj.thumbnail || undefined, - originallyPublishedAt: this.buildOriginallyPublishedAt(obj), - ext: obj.ext - } - } - - private titleTruncation (title: string) { - return peertubeTruncate(title, { - length: CONSTRAINTS_FIELDS.VIDEOS.NAME.max, - separator: /,? +/, - omission: ' […]' - }) - } - - private descriptionTruncation (description: string) { - if (!description || description.length < CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.min) return undefined - - return peertubeTruncate(description, { - length: CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max, - separator: /,? +/, - omission: ' […]' - }) - } - - private isNSFW (info: any) { - return info.age_limit && info.age_limit >= 16 - } - - private getTags (tags: any) { - if (Array.isArray(tags) === false) return [] - - return tags - .filter(t => t.length < CONSTRAINTS_FIELDS.VIDEOS.TAG.max && t.length > CONSTRAINTS_FIELDS.VIDEOS.TAG.min) - .map(t => t.normalize()) - .slice(0, 5) - } - - private getLicence (licence: string) { - if (!licence) return undefined - - if (licence.includes('Creative Commons Attribution')) return 1 - - for (const key of Object.keys(VIDEO_LICENCES)) { - const peertubeLicence = VIDEO_LICENCES[key] - if (peertubeLicence.toLowerCase() === licence.toLowerCase()) return parseInt(key, 10) - } - - return undefined - } - - private getCategory (categories: string[]) { - if (!categories) return undefined - - const categoryString = categories[0] - if (!categoryString || typeof categoryString !== 'string') return undefined - - if (categoryString === 'News & Politics') return 11 - - for (const key of Object.keys(VIDEO_CATEGORIES)) { - const category = VIDEO_CATEGORIES[key] - if (categoryString.toLowerCase() === category.toLowerCase()) return parseInt(key, 10) - } - - return undefined - } - - private getLanguage (language: string) { - return VIDEO_LANGUAGES[language] ? language : undefined - } - - private wrapWithProxyOptions (options: string[]) { - if (CONFIG.IMPORT.VIDEOS.HTTP.PROXY.ENABLED) { - logger.debug('Using proxy for YoutubeDL') - - return [ '--proxy', CONFIG.IMPORT.VIDEOS.HTTP.PROXY.URL ].concat(options) - } - - return options - } - - // Thanks: https://github.com/przemyslawpluta/node-youtube-dl/blob/master/lib/downloader.js - // We rewrote it to avoid sync calls - static async updateYoutubeDLBinary () { - logger.info('Updating youtubeDL binary.') - - const binDirectory = join(root(), 'node_modules', 'youtube-dl', 'bin') - const bin = join(binDirectory, 'youtube-dl') - const detailsPath = join(binDirectory, 'details') - const url = process.env.YOUTUBE_DL_DOWNLOAD_HOST || 'https://yt-dl.org/downloads/latest/youtube-dl' - - await ensureDir(binDirectory) - - try { - const gotContext = { bodyKBLimit: 20_000 } - - const result = await peertubeGot(url, { followRedirect: false, context: gotContext }) - - if (result.statusCode !== HttpStatusCode.FOUND_302) { - logger.error('youtube-dl update error: did not get redirect for the latest version link. Status %d', result.statusCode) - return - } - - const newUrl = result.headers.location - const newVersion = /\/(\d{4}\.\d\d\.\d\d(\.\d)?)\/youtube-dl$/.exec(newUrl)[1] - - const downloadFileStream = peertubeGot.stream(newUrl, { context: gotContext }) - const writeStream = createWriteStream(bin, { mode: 493 }) - - await pipelinePromise( - downloadFileStream, - writeStream - ) - - const details = JSON.stringify({ version: newVersion, path: bin, exec: 'youtube-dl' }) - await writeFile(detailsPath, details, { encoding: 'utf8' }) - - logger.info('youtube-dl updated to version %s.', newVersion) - } catch (err) { - logger.error('Cannot update youtube-dl.', { err }) - } - } - - static async safeGetYoutubeDL () { - let youtubeDL - - try { - youtubeDL = require('youtube-dl') - } catch (e) { - // Download binary - await this.updateYoutubeDLBinary() - youtubeDL = require('youtube-dl') - } - - return youtubeDL - } -} - -// --------------------------------------------------------------------------- - -export { - YoutubeDL -} diff --git a/server/helpers/youtube-dl/index.ts b/server/helpers/youtube-dl/index.ts new file mode 100644 index 000000000..6afc77dcf --- /dev/null +++ b/server/helpers/youtube-dl/index.ts @@ -0,0 +1,3 @@ +export * from './youtube-dl-cli' +export * from './youtube-dl-info-builder' +export * from './youtube-dl-wrapper' diff --git a/server/helpers/youtube-dl/youtube-dl-cli.ts b/server/helpers/youtube-dl/youtube-dl-cli.ts new file mode 100644 index 000000000..440869205 --- /dev/null +++ b/server/helpers/youtube-dl/youtube-dl-cli.ts @@ -0,0 +1,198 @@ +import execa from 'execa' +import { pathExists, writeFile } from 'fs-extra' +import { join } from 'path' +import { CONFIG } from '@server/initializers/config' +import { VideoResolution } from '@shared/models' +import { logger, loggerTagsFactory } from '../logger' +import { getProxy, isProxyEnabled } from '../proxy' +import { isBinaryResponse, peertubeGot } from '../requests' + +const lTags = loggerTagsFactory('youtube-dl') + +const youtubeDLBinaryPath = join(CONFIG.STORAGE.BIN_DIR, CONFIG.IMPORT.VIDEOS.HTTP.YOUTUBE_DL_RELEASE.NAME) + +export class YoutubeDLCLI { + + static async safeGet () { + if (!await pathExists(youtubeDLBinaryPath)) { + await this.updateYoutubeDLBinary() + } + + return new YoutubeDLCLI() + } + + static async updateYoutubeDLBinary () { + const url = CONFIG.IMPORT.VIDEOS.HTTP.YOUTUBE_DL_RELEASE.URL + + logger.info('Updating youtubeDL binary from %s.', url, lTags()) + + const gotOptions = { context: { bodyKBLimit: 20_000 }, responseType: 'buffer' as 'buffer' } + + try { + let gotResult = await peertubeGot(url, gotOptions) + + if (!isBinaryResponse(gotResult)) { + const json = JSON.parse(gotResult.body.toString()) + const latest = json.filter(release => release.prerelease === false)[0] + if (!latest) throw new Error('Cannot find latest release') + + const releaseName = CONFIG.IMPORT.VIDEOS.HTTP.YOUTUBE_DL_RELEASE.NAME + const releaseAsset = latest.assets.find(a => a.name === releaseName) + if (!releaseAsset) throw new Error(`Cannot find appropriate release with name ${releaseName} in release assets`) + + gotResult = await peertubeGot(releaseAsset.browser_download_url, gotOptions) + } + + if (!isBinaryResponse(gotResult)) { + throw new Error('Not a binary response') + } + + await writeFile(youtubeDLBinaryPath, gotResult.body) + + logger.info('youtube-dl updated %s.', youtubeDLBinaryPath, lTags()) + } catch (err) { + logger.error('Cannot update youtube-dl from %s.', url, { err, ...lTags() }) + } + } + + static getYoutubeDLVideoFormat (enabledResolutions: VideoResolution[]) { + /** + * list of format selectors in order or preference + * see https://github.com/ytdl-org/youtube-dl#format-selection + * + * case #1 asks for a mp4 using h264 (avc1) and the exact resolution in the hope + * of being able to do a "quick-transcode" + * case #2 is the first fallback. No "quick-transcode" means we can get anything else (like vp9) + * case #3 is the resolution-degraded equivalent of #1, and already a pretty safe fallback + * + * in any case we avoid AV1, see https://github.com/Chocobozzz/PeerTube/issues/3499 + **/ + const resolution = enabledResolutions.length === 0 + ? VideoResolution.H_720P + : Math.max(...enabledResolutions) + + return [ + `bestvideo[vcodec^=avc1][height=${resolution}]+bestaudio[ext=m4a]`, // case #1 + `bestvideo[vcodec!*=av01][vcodec!*=vp9.2][height=${resolution}]+bestaudio`, // case #2 + `bestvideo[vcodec^=avc1][height<=${resolution}]+bestaudio[ext=m4a]`, // case #3 + `bestvideo[vcodec!*=av01][vcodec!*=vp9.2]+bestaudio`, + 'best[vcodec!*=av01][vcodec!*=vp9.2]', // case fallback for known formats + 'best' // Ultimate fallback + ].join('/') + } + + private constructor () { + + } + + download (options: { + url: string + format: string + output: string + processOptions: execa.NodeOptions + additionalYoutubeDLArgs?: string[] + }) { + return this.run({ + url: options.url, + processOptions: options.processOptions, + args: (options.additionalYoutubeDLArgs || []).concat([ '-f', options.format, '-o', options.output ]) + }) + } + + async getInfo (options: { + url: string + format: string + processOptions: execa.NodeOptions + additionalYoutubeDLArgs?: string[] + }) { + const { url, format, additionalYoutubeDLArgs = [], processOptions } = options + + const completeArgs = additionalYoutubeDLArgs.concat([ '--dump-json', '-f', format ]) + + const data = await this.run({ url, args: completeArgs, processOptions }) + const info = data.map(this.parseInfo) + + return info.length === 1 + ? info[0] + : info + } + + async getSubs (options: { + url: string + format: 'vtt' + processOptions: execa.NodeOptions + }) { + const { url, format, processOptions } = options + + const args = [ '--skip-download', '--all-subs', `--sub-format=${format}` ] + + const data = await this.run({ url, args, processOptions }) + const files: string[] = [] + + const skipString = '[info] Writing video subtitles to: ' + + for (let i = 0, len = data.length; i < len; i++) { + const line = data[i] + + if (line.indexOf(skipString) === 0) { + files.push(line.slice(skipString.length)) + } + } + + return files + } + + private async run (options: { + url: string + args: string[] + processOptions: execa.NodeOptions + }) { + const { url, args, processOptions } = options + + let completeArgs = this.wrapWithProxyOptions(args) + completeArgs = this.wrapWithIPOptions(completeArgs) + completeArgs = this.wrapWithFFmpegOptions(completeArgs) + + const output = await execa('python', [ youtubeDLBinaryPath, ...completeArgs, url ], processOptions) + + logger.debug('Runned youtube-dl command.', { command: output.command, stdout: output.stdout, ...lTags() }) + + return output.stdout + ? output.stdout.trim().split(/\r?\n/) + : undefined + } + + private wrapWithProxyOptions (args: string[]) { + if (isProxyEnabled()) { + logger.debug('Using proxy %s for YoutubeDL', getProxy(), lTags()) + + return [ '--proxy', getProxy() ].concat(args) + } + + return args + } + + private wrapWithIPOptions (args: string[]) { + if (CONFIG.IMPORT.VIDEOS.HTTP.FORCE_IPV4) { + logger.debug('Force ipv4 for YoutubeDL') + + return [ '--force-ipv4' ].concat(args) + } + + return args + } + + private wrapWithFFmpegOptions (args: string[]) { + if (process.env.FFMPEG_PATH) { + logger.debug('Using ffmpeg location %s for YoutubeDL', process.env.FFMPEG_PATH, lTags()) + + return [ '--ffmpeg-location', process.env.FFMPEG_PATH ].concat(args) + } + + return args + } + + private parseInfo (data: string) { + return JSON.parse(data) + } +} diff --git a/server/helpers/youtube-dl/youtube-dl-info-builder.ts b/server/helpers/youtube-dl/youtube-dl-info-builder.ts new file mode 100644 index 000000000..9746a7067 --- /dev/null +++ b/server/helpers/youtube-dl/youtube-dl-info-builder.ts @@ -0,0 +1,154 @@ +import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../../initializers/constants' +import { peertubeTruncate } from '../core-utils' + +type YoutubeDLInfo = { + name?: string + description?: string + category?: number + language?: string + licence?: number + nsfw?: boolean + tags?: string[] + thumbnailUrl?: string + ext?: string + originallyPublishedAt?: Date +} + +class YoutubeDLInfoBuilder { + private readonly info: any + + constructor (info: any) { + this.info = { ...info } + } + + getInfo () { + const obj = this.buildVideoInfo(this.normalizeObject(this.info)) + if (obj.name && obj.name.length < CONSTRAINTS_FIELDS.VIDEOS.NAME.min) obj.name += ' video' + + return obj + } + + private normalizeObject (obj: any) { + const newObj: any = {} + + for (const key of Object.keys(obj)) { + // Deprecated key + if (key === 'resolution') continue + + const value = obj[key] + + if (typeof value === 'string') { + newObj[key] = value.normalize() + } else { + newObj[key] = value + } + } + + return newObj + } + + private buildOriginallyPublishedAt (obj: any) { + let originallyPublishedAt: Date = null + + const uploadDateMatcher = /^(\d{4})(\d{2})(\d{2})$/.exec(obj.upload_date) + if (uploadDateMatcher) { + originallyPublishedAt = new Date() + originallyPublishedAt.setHours(0, 0, 0, 0) + + const year = parseInt(uploadDateMatcher[1], 10) + // Month starts from 0 + const month = parseInt(uploadDateMatcher[2], 10) - 1 + const day = parseInt(uploadDateMatcher[3], 10) + + originallyPublishedAt.setFullYear(year, month, day) + } + + return originallyPublishedAt + } + + private buildVideoInfo (obj: any): YoutubeDLInfo { + return { + name: this.titleTruncation(obj.title), + description: this.descriptionTruncation(obj.description), + category: this.getCategory(obj.categories), + licence: this.getLicence(obj.license), + language: this.getLanguage(obj.language), + nsfw: this.isNSFW(obj), + tags: this.getTags(obj.tags), + thumbnailUrl: obj.thumbnail || undefined, + originallyPublishedAt: this.buildOriginallyPublishedAt(obj), + ext: obj.ext + } + } + + private titleTruncation (title: string) { + return peertubeTruncate(title, { + length: CONSTRAINTS_FIELDS.VIDEOS.NAME.max, + separator: /,? +/, + omission: ' […]' + }) + } + + private descriptionTruncation (description: string) { + if (!description || description.length < CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.min) return undefined + + return peertubeTruncate(description, { + length: CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max, + separator: /,? +/, + omission: ' […]' + }) + } + + private isNSFW (info: any) { + return info?.age_limit >= 16 + } + + private getTags (tags: string[]) { + if (Array.isArray(tags) === false) return [] + + return tags + .filter(t => t.length < CONSTRAINTS_FIELDS.VIDEOS.TAG.max && t.length > CONSTRAINTS_FIELDS.VIDEOS.TAG.min) + .map(t => t.normalize()) + .slice(0, 5) + } + + private getLicence (licence: string) { + if (!licence) return undefined + + if (licence.includes('Creative Commons Attribution')) return 1 + + for (const key of Object.keys(VIDEO_LICENCES)) { + const peertubeLicence = VIDEO_LICENCES[key] + if (peertubeLicence.toLowerCase() === licence.toLowerCase()) return parseInt(key, 10) + } + + return undefined + } + + private getCategory (categories: string[]) { + if (!categories) return undefined + + const categoryString = categories[0] + if (!categoryString || typeof categoryString !== 'string') return undefined + + if (categoryString === 'News & Politics') return 11 + + for (const key of Object.keys(VIDEO_CATEGORIES)) { + const category = VIDEO_CATEGORIES[key] + if (categoryString.toLowerCase() === category.toLowerCase()) return parseInt(key, 10) + } + + return undefined + } + + private getLanguage (language: string) { + return VIDEO_LANGUAGES[language] ? language : undefined + } +} + +// --------------------------------------------------------------------------- + +export { + YoutubeDLInfo, + YoutubeDLInfoBuilder +} diff --git a/server/helpers/youtube-dl/youtube-dl-wrapper.ts b/server/helpers/youtube-dl/youtube-dl-wrapper.ts new file mode 100644 index 000000000..6960fbae4 --- /dev/null +++ b/server/helpers/youtube-dl/youtube-dl-wrapper.ts @@ -0,0 +1,135 @@ +import { move, pathExists, readdir, remove } from 'fs-extra' +import { dirname, join } from 'path' +import { CONFIG } from '@server/initializers/config' +import { isVideoFileExtnameValid } from '../custom-validators/videos' +import { logger, loggerTagsFactory } from '../logger' +import { generateVideoImportTmpPath } from '../utils' +import { YoutubeDLCLI } from './youtube-dl-cli' +import { YoutubeDLInfo, YoutubeDLInfoBuilder } from './youtube-dl-info-builder' + +const lTags = loggerTagsFactory('youtube-dl') + +export type YoutubeDLSubs = { + language: string + filename: string + path: string +}[] + +const processOptions = { + maxBuffer: 1024 * 1024 * 10 // 10MB +} + +class YoutubeDLWrapper { + + constructor (private readonly url: string = '', private readonly enabledResolutions: number[] = []) { + + } + + async getInfoForDownload (youtubeDLArgs: string[] = []): Promise { + const youtubeDL = await YoutubeDLCLI.safeGet() + + const info = await youtubeDL.getInfo({ + url: this.url, + format: YoutubeDLCLI.getYoutubeDLVideoFormat(this.enabledResolutions), + additionalYoutubeDLArgs: youtubeDLArgs, + processOptions + }) + + if (info.is_live === true) throw new Error('Cannot download a live streaming.') + + const infoBuilder = new YoutubeDLInfoBuilder(info) + + return infoBuilder.getInfo() + } + + async getSubtitles (): Promise { + const cwd = CONFIG.STORAGE.TMP_DIR + + const youtubeDL = await YoutubeDLCLI.safeGet() + + const files = await youtubeDL.getSubs({ url: this.url, format: 'vtt', processOptions: { cwd } }) + if (!files) return [] + + logger.debug('Get subtitles from youtube dl.', { url: this.url, files, ...lTags() }) + + const subtitles = files.reduce((acc, filename) => { + const matched = filename.match(/\.([a-z]{2})(-[a-z]+)?\.(vtt|ttml)/i) + if (!matched || !matched[1]) return acc + + return [ + ...acc, + { + language: matched[1], + path: join(cwd, filename), + filename + } + ] + }, []) + + return subtitles + } + + async downloadVideo (fileExt: string, timeout: number): Promise { + // Leave empty the extension, youtube-dl will add it + const pathWithoutExtension = generateVideoImportTmpPath(this.url, '') + + let timer: NodeJS.Timeout + + logger.info('Importing youtubeDL video %s to %s', this.url, pathWithoutExtension, lTags()) + + const youtubeDL = await YoutubeDLCLI.safeGet() + + const timeoutPromise = new Promise((_, rej) => { + timer = setTimeout(() => rej(new Error('YoutubeDL download timeout.')), timeout) + }) + + const downloadPromise = youtubeDL.download({ + url: this.url, + format: YoutubeDLCLI.getYoutubeDLVideoFormat(this.enabledResolutions), + output: pathWithoutExtension, + processOptions + }).then(() => clearTimeout(timer)) + .then(async () => { + // If youtube-dl did not guess an extension for our file, just use .mp4 as default + if (await pathExists(pathWithoutExtension)) { + await move(pathWithoutExtension, pathWithoutExtension + '.mp4') + } + + return this.guessVideoPathWithExtension(pathWithoutExtension, fileExt) + }) + + return Promise.race([ downloadPromise, timeoutPromise ]) + .catch(async err => { + const path = await this.guessVideoPathWithExtension(pathWithoutExtension, fileExt) + + remove(path) + .catch(err => logger.error('Cannot remove file in youtubeDL timeout.', { err, ...lTags() })) + + throw err + }) + } + + private async guessVideoPathWithExtension (tmpPath: string, sourceExt: string) { + if (!isVideoFileExtnameValid(sourceExt)) { + throw new Error('Invalid video extension ' + sourceExt) + } + + const extensions = [ sourceExt, '.mp4', '.mkv', '.webm' ] + + for (const extension of extensions) { + const path = tmpPath + extension + + if (await pathExists(path)) return path + } + + const directoryContent = await readdir(dirname(tmpPath)) + + throw new Error(`Cannot guess path of ${tmpPath}. Directory content: ${directoryContent.join(', ')}`) + } +} + +// --------------------------------------------------------------------------- + +export { + YoutubeDLWrapper +} diff --git a/server/initializers/config.ts b/server/initializers/config.ts index 3a7c72a1c..e20efe02c 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts @@ -69,6 +69,7 @@ const CONFIG = { STORAGE: { TMP_DIR: buildPath(config.get('storage.tmp')), + BIN_DIR: buildPath(config.get('storage.bin')), ACTOR_IMAGES: buildPath(config.get('storage.avatars')), LOG_DIR: buildPath(config.get('storage.logs')), VIDEOS_DIR: buildPath(config.get('storage.videos')), @@ -292,11 +293,13 @@ const CONFIG = { HTTP: { get ENABLED () { return config.get('import.videos.http.enabled') }, - get FORCE_IPV4 () { return config.get('import.videos.http.force_ipv4') }, - PROXY: { - get ENABLED () { return config.get('import.videos.http.proxy.enabled') }, - get URL () { return config.get('import.videos.http.proxy.url') } - } + + YOUTUBE_DL_RELEASE: { + get URL () { return config.get('import.videos.http.youtube_dl_release.url') }, + get NAME () { return config.get('import.videos.http.youtube_dl_release.name') } + }, + + get FORCE_IPV4 () { return config.get('import.videos.http.force_ipv4') } }, TORRENT: { get ENABLED () { return config.get('import.videos.torrent.enabled') } diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index dcbad9264..1d434d5ab 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -497,6 +497,12 @@ const MIMETYPES = { MIMETYPES.AUDIO.EXT_MIMETYPE = invert(MIMETYPES.AUDIO.MIMETYPE_EXT) MIMETYPES.IMAGE.EXT_MIMETYPE = invert(MIMETYPES.IMAGE.MIMETYPE_EXT) +const BINARY_CONTENT_TYPES = new Set([ + 'binary/octet-stream', + 'application/octet-stream', + 'application/x-binary' +]) + // --------------------------------------------------------------------------- const OVERVIEWS = { @@ -903,6 +909,7 @@ export { MIMETYPES, CRAWL_REQUEST_CONCURRENCY, DEFAULT_AUDIO_RESOLUTION, + BINARY_CONTENT_TYPES, JOB_COMPLETED_LIFETIME, HTTP_SIGNATURE, VIDEO_IMPORT_STATES, diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts index 8313c2561..4ce1a6c30 100644 --- a/server/lib/job-queue/handlers/video-import.ts +++ b/server/lib/job-queue/handlers/video-import.ts @@ -2,7 +2,7 @@ import { Job } from 'bull' import { move, remove, stat } from 'fs-extra' import { getLowercaseExtension } from '@server/helpers/core-utils' import { retryTransactionWrapper } from '@server/helpers/database-utils' -import { YoutubeDL } from '@server/helpers/youtube-dl' +import { YoutubeDLWrapper } from '@server/helpers/youtube-dl' import { isPostImportVideoAccepted } from '@server/lib/moderation' import { generateWebTorrentVideoFilename } from '@server/lib/paths' import { Hooks } from '@server/lib/plugins/hooks' @@ -77,10 +77,10 @@ async function processYoutubeDLImport (job: Job, payload: VideoImportYoutubeDLPa videoImportId: videoImport.id } - const youtubeDL = new YoutubeDL(videoImport.targetUrl, ServerConfigManager.Instance.getEnabledResolutions('vod')) + const youtubeDL = new YoutubeDLWrapper(videoImport.targetUrl, ServerConfigManager.Instance.getEnabledResolutions('vod')) return processFile( - () => youtubeDL.downloadYoutubeDLVideo(payload.fileExt, VIDEO_IMPORT_TIMEOUT), + () => youtubeDL.downloadVideo(payload.fileExt, VIDEO_IMPORT_TIMEOUT), videoImport, options ) diff --git a/server/lib/schedulers/youtube-dl-update-scheduler.ts b/server/lib/schedulers/youtube-dl-update-scheduler.ts index 898691c13..93d02f8a9 100644 --- a/server/lib/schedulers/youtube-dl-update-scheduler.ts +++ b/server/lib/schedulers/youtube-dl-update-scheduler.ts @@ -1,4 +1,4 @@ -import { YoutubeDL } from '@server/helpers/youtube-dl' +import { YoutubeDLCLI } from '@server/helpers/youtube-dl' import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' import { AbstractScheduler } from './abstract-scheduler' @@ -13,7 +13,7 @@ export class YoutubeDlUpdateScheduler extends AbstractScheduler { } protected internalExecute () { - return YoutubeDL.updateYoutubeDLBinary() + return YoutubeDLCLI.updateYoutubeDLBinary() } static get Instance () { diff --git a/server/tests/api/server/proxy.ts b/server/tests/api/server/proxy.ts index 72bd49078..29f3e10d8 100644 --- a/server/tests/api/server/proxy.ts +++ b/server/tests/api/server/proxy.ts @@ -2,8 +2,18 @@ import 'mocha' import * as chai from 'chai' -import { cleanupTests, createMultipleServers, doubleFollow, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/extra-utils' +import { + cleanupTests, + createMultipleServers, + doubleFollow, + FIXTURE_URLS, + PeerTubeServer, + setAccessTokensToServers, + setDefaultVideoChannel, + waitJobs +} from '@shared/extra-utils' import { MockProxy } from '@shared/extra-utils/mock-servers/mock-proxy' +import { HttpStatusCode, VideoPrivacy } from '@shared/models' const expect = chai.expect @@ -25,43 +35,90 @@ describe('Test proxy', function () { goodEnv.HTTP_PROXY = 'http://localhost:' + proxyPort await setAccessTokensToServers(servers) + await setDefaultVideoChannel(servers) await doubleFollow(servers[0], servers[1]) }) - it('Should succeed federation with the appropriate proxy config', async function () { - await servers[0].kill() - await servers[0].run({}, { env: goodEnv }) + describe('Federation', function () { - await servers[0].videos.quickUpload({ name: 'video 1' }) + it('Should succeed federation with the appropriate proxy config', async function () { + this.timeout(40000) - await waitJobs(servers) + await servers[0].kill() + await servers[0].run({}, { env: goodEnv }) - for (const server of servers) { - const { total, data } = await server.videos.list() - expect(total).to.equal(1) - expect(data).to.have.lengthOf(1) - } + await servers[0].videos.quickUpload({ name: 'video 1' }) + + await waitJobs(servers) + + for (const server of servers) { + const { total, data } = await server.videos.list() + expect(total).to.equal(1) + expect(data).to.have.lengthOf(1) + } + }) + + it('Should fail federation with a wrong proxy config', async function () { + this.timeout(40000) + + await servers[0].kill() + await servers[0].run({}, { env: badEnv }) + + await servers[0].videos.quickUpload({ name: 'video 2' }) + + await waitJobs(servers) + + { + const { total, data } = await servers[0].videos.list() + expect(total).to.equal(2) + expect(data).to.have.lengthOf(2) + } + + { + const { total, data } = await servers[1].videos.list() + expect(total).to.equal(1) + expect(data).to.have.lengthOf(1) + } + }) }) - it('Should fail federation with a wrong proxy config', async function () { - await servers[0].kill() - await servers[0].run({}, { env: badEnv }) + describe('Videos import', async function () { + + function quickImport (expectedStatus: HttpStatusCode = HttpStatusCode.OK_200) { + return servers[0].imports.importVideo({ + attributes: { + name: 'video import', + channelId: servers[0].store.channel.id, + privacy: VideoPrivacy.PUBLIC, + targetUrl: FIXTURE_URLS.peertube_long + }, + expectedStatus + }) + } + + it('Should succeed import with the appropriate proxy config', async function () { + this.timeout(40000) + + await servers[0].kill() + await servers[0].run({}, { env: goodEnv }) - await servers[0].videos.quickUpload({ name: 'video 2' }) + await quickImport() - await waitJobs(servers) + await waitJobs(servers) - { const { total, data } = await servers[0].videos.list() - expect(total).to.equal(2) - expect(data).to.have.lengthOf(2) - } + expect(total).to.equal(3) + expect(data).to.have.lengthOf(3) + }) - { - const { total, data } = await servers[1].videos.list() - expect(total).to.equal(1) - expect(data).to.have.lengthOf(1) - } + it('Should fail import with a wrong proxy config', async function () { + this.timeout(40000) + + await servers[0].kill() + await servers[0].run({}, { env: badEnv }) + + await quickImport(HttpStatusCode.BAD_REQUEST_400) + }) }) after(async function () { diff --git a/server/tests/api/videos/video-imports.ts b/server/tests/api/videos/video-imports.ts index 948c779e8..cfb188060 100644 --- a/server/tests/api/videos/video-imports.ts +++ b/server/tests/api/videos/video-imports.ts @@ -1,368 +1,444 @@ /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ import 'mocha' -import * as chai from 'chai' +import { expect } from 'chai' +import { pathExists, remove } from 'fs-extra' +import { join } from 'path' import { areHttpImportTestsDisabled, cleanupTests, createMultipleServers, + createSingleServer, doubleFollow, FIXTURE_URLS, PeerTubeServer, setAccessTokensToServers, + setDefaultVideoChannel, testCaptionFile, testImage, waitJobs } from '@shared/extra-utils' import { VideoPrivacy, VideoResolution } from '@shared/models' -const expect = chai.expect +async function checkVideosServer1 (server: PeerTubeServer, idHttp: string, idMagnet: string, idTorrent: string) { + const videoHttp = await server.videos.get({ id: idHttp }) + + expect(videoHttp.name).to.equal('small video - youtube') + // FIXME: youtube-dl seems broken + // expect(videoHttp.category.label).to.equal('News & Politics') + // expect(videoHttp.licence.label).to.equal('Attribution') + expect(videoHttp.language.label).to.equal('Unknown') + expect(videoHttp.nsfw).to.be.false + expect(videoHttp.description).to.equal('this is a super description') + expect(videoHttp.tags).to.deep.equal([ 'tag1', 'tag2' ]) + expect(videoHttp.files).to.have.lengthOf(1) + + const originallyPublishedAt = new Date(videoHttp.originallyPublishedAt) + expect(originallyPublishedAt.getDate()).to.equal(14) + expect(originallyPublishedAt.getMonth()).to.equal(0) + expect(originallyPublishedAt.getFullYear()).to.equal(2019) + + const videoMagnet = await server.videos.get({ id: idMagnet }) + const videoTorrent = await server.videos.get({ id: idTorrent }) + + for (const video of [ videoMagnet, videoTorrent ]) { + expect(video.category.label).to.equal('Misc') + expect(video.licence.label).to.equal('Unknown') + expect(video.language.label).to.equal('Unknown') + expect(video.nsfw).to.be.false + expect(video.description).to.equal('this is a super torrent description') + expect(video.tags).to.deep.equal([ 'tag_torrent1', 'tag_torrent2' ]) + expect(video.files).to.have.lengthOf(1) + } + + expect(videoTorrent.name).to.contain('你好 世界 720p.mp4') + expect(videoMagnet.name).to.contain('super peertube2 video') + + const bodyCaptions = await server.captions.list({ videoId: idHttp }) + expect(bodyCaptions.total).to.equal(2) +} + +async function checkVideoServer2 (server: PeerTubeServer, id: number | string) { + const video = await server.videos.get({ id }) + + expect(video.name).to.equal('my super name') + expect(video.category.label).to.equal('Entertainment') + expect(video.licence.label).to.equal('Public Domain Dedication') + expect(video.language.label).to.equal('English') + expect(video.nsfw).to.be.false + expect(video.description).to.equal('my super description') + expect(video.tags).to.deep.equal([ 'supertag1', 'supertag2' ]) + + expect(video.files).to.have.lengthOf(1) + + const bodyCaptions = await server.captions.list({ videoId: id }) + expect(bodyCaptions.total).to.equal(2) +} describe('Test video imports', function () { - let servers: PeerTubeServer[] = [] - let channelIdServer1: number - let channelIdServer2: number if (areHttpImportTestsDisabled()) return - async function checkVideosServer1 (server: PeerTubeServer, idHttp: string, idMagnet: string, idTorrent: string) { - const videoHttp = await server.videos.get({ id: idHttp }) - - expect(videoHttp.name).to.equal('small video - youtube') - // FIXME: youtube-dl seems broken - // expect(videoHttp.category.label).to.equal('News & Politics') - // expect(videoHttp.licence.label).to.equal('Attribution') - expect(videoHttp.language.label).to.equal('Unknown') - expect(videoHttp.nsfw).to.be.false - expect(videoHttp.description).to.equal('this is a super description') - expect(videoHttp.tags).to.deep.equal([ 'tag1', 'tag2' ]) - expect(videoHttp.files).to.have.lengthOf(1) - - const originallyPublishedAt = new Date(videoHttp.originallyPublishedAt) - expect(originallyPublishedAt.getDate()).to.equal(14) - expect(originallyPublishedAt.getMonth()).to.equal(0) - expect(originallyPublishedAt.getFullYear()).to.equal(2019) - - const videoMagnet = await server.videos.get({ id: idMagnet }) - const videoTorrent = await server.videos.get({ id: idTorrent }) - - for (const video of [ videoMagnet, videoTorrent ]) { - expect(video.category.label).to.equal('Misc') - expect(video.licence.label).to.equal('Unknown') - expect(video.language.label).to.equal('Unknown') - expect(video.nsfw).to.be.false - expect(video.description).to.equal('this is a super torrent description') - expect(video.tags).to.deep.equal([ 'tag_torrent1', 'tag_torrent2' ]) - expect(video.files).to.have.lengthOf(1) - } + function runSuite (mode: 'youtube-dl' | 'yt-dlp') { - expect(videoTorrent.name).to.contain('你好 世界 720p.mp4') - expect(videoMagnet.name).to.contain('super peertube2 video') + describe('Import ' + mode, function () { + let servers: PeerTubeServer[] = [] - const bodyCaptions = await server.captions.list({ videoId: idHttp }) - expect(bodyCaptions.total).to.equal(2) - } + before(async function () { + this.timeout(30_000) - async function checkVideoServer2 (server: PeerTubeServer, id: number | string) { - const video = await server.videos.get({ id }) + // Run servers + servers = await createMultipleServers(2, { + import: { + videos: { + http: { + youtube_dl_release: { + url: mode === 'youtube-dl' + ? 'https://yt-dl.org/downloads/latest/youtube-dl' + : 'https://api.github.com/repos/yt-dlp/yt-dlp/releases', - expect(video.name).to.equal('my super name') - expect(video.category.label).to.equal('Entertainment') - expect(video.licence.label).to.equal('Public Domain Dedication') - expect(video.language.label).to.equal('English') - expect(video.nsfw).to.be.false - expect(video.description).to.equal('my super description') - expect(video.tags).to.deep.equal([ 'supertag1', 'supertag2' ]) + name: mode + } + } + } + } + }) - expect(video.files).to.have.lengthOf(1) + await setAccessTokensToServers(servers) + await setDefaultVideoChannel(servers) - const bodyCaptions = await server.captions.list({ videoId: id }) - expect(bodyCaptions.total).to.equal(2) - } + await doubleFollow(servers[0], servers[1]) + }) - before(async function () { - this.timeout(30_000) + it('Should import videos on server 1', async function () { + this.timeout(60_000) - // Run servers - servers = await createMultipleServers(2) + const baseAttributes = { + channelId: servers[0].store.channel.id, + privacy: VideoPrivacy.PUBLIC + } - await setAccessTokensToServers(servers) + { + const attributes = { ...baseAttributes, targetUrl: FIXTURE_URLS.youtube } + const { video } = await servers[0].imports.importVideo({ attributes }) + expect(video.name).to.equal('small video - youtube') - { - const { videoChannels } = await servers[0].users.getMyInfo() - channelIdServer1 = videoChannels[0].id - } + { + expect(video.thumbnailPath).to.match(new RegExp(`^/static/thumbnails/.+.jpg$`)) + expect(video.previewPath).to.match(new RegExp(`^/lazy-static/previews/.+.jpg$`)) - { - const { videoChannels } = await servers[1].users.getMyInfo() - channelIdServer2 = videoChannels[0].id - } + const suffix = mode === 'yt-dlp' + ? '_yt_dlp' + : '' - await doubleFollow(servers[0], servers[1]) - }) + await testImage(servers[0].url, 'video_import_thumbnail' + suffix, video.thumbnailPath) + await testImage(servers[0].url, 'video_import_preview' + suffix, video.previewPath) + } - it('Should import videos on server 1', async function () { - this.timeout(60_000) + const bodyCaptions = await servers[0].captions.list({ videoId: video.id }) + const videoCaptions = bodyCaptions.data + expect(videoCaptions).to.have.lengthOf(2) - const baseAttributes = { - channelId: channelIdServer1, - privacy: VideoPrivacy.PUBLIC - } + { + const enCaption = videoCaptions.find(caption => caption.language.id === 'en') + expect(enCaption).to.exist + expect(enCaption.language.label).to.equal('English') + expect(enCaption.captionPath).to.match(new RegExp(`^/lazy-static/video-captions/.+-en.vtt$`)) - { - const attributes = { ...baseAttributes, targetUrl: FIXTURE_URLS.youtube } - const { video } = await servers[0].imports.importVideo({ attributes }) - expect(video.name).to.equal('small video - youtube') + const regex = `WEBVTT[ \n]+Kind: captions[ \n]+Language: en[ \n]+00:00:01.600 --> 00:00:04.200[ \n]+English \\(US\\)[ \n]+` + + `00:00:05.900 --> 00:00:07.999[ \n]+This is a subtitle in American English[ \n]+` + + `00:00:10.000 --> 00:00:14.000[ \n]+Adding subtitles is very easy to do` + await testCaptionFile(servers[0].url, enCaption.captionPath, new RegExp(regex)) + } - expect(video.thumbnailPath).to.match(new RegExp(`^/static/thumbnails/.+.jpg$`)) - expect(video.previewPath).to.match(new RegExp(`^/lazy-static/previews/.+.jpg$`)) + { + const frCaption = videoCaptions.find(caption => caption.language.id === 'fr') + expect(frCaption).to.exist + expect(frCaption.language.label).to.equal('French') + expect(frCaption.captionPath).to.match(new RegExp(`^/lazy-static/video-captions/.+-fr.vtt`)) - await testImage(servers[0].url, 'video_import_thumbnail', video.thumbnailPath) - await testImage(servers[0].url, 'video_import_preview', video.previewPath) + const regex = `WEBVTT[ \n]+Kind: captions[ \n]+Language: fr[ \n]+00:00:01.600 --> 00:00:04.200[ \n]+` + + `Français \\(FR\\)[ \n]+00:00:05.900 --> 00:00:07.999[ \n]+C'est un sous-titre français[ \n]+` + + `00:00:10.000 --> 00:00:14.000[ \n]+Ajouter un sous-titre est vraiment facile` - const bodyCaptions = await servers[0].captions.list({ videoId: video.id }) - const videoCaptions = bodyCaptions.data - expect(videoCaptions).to.have.lengthOf(2) + await testCaptionFile(servers[0].url, frCaption.captionPath, new RegExp(regex)) + } + } - const enCaption = videoCaptions.find(caption => caption.language.id === 'en') - expect(enCaption).to.exist - expect(enCaption.language.label).to.equal('English') - expect(enCaption.captionPath).to.match(new RegExp(`^/lazy-static/video-captions/.+-en.vtt$`)) - await testCaptionFile(servers[0].url, enCaption.captionPath, `WEBVTT -Kind: captions -Language: en + { + const attributes = { + ...baseAttributes, + magnetUri: FIXTURE_URLS.magnet, + description: 'this is a super torrent description', + tags: [ 'tag_torrent1', 'tag_torrent2' ] + } + const { video } = await servers[0].imports.importVideo({ attributes }) + expect(video.name).to.equal('super peertube2 video') + } -00:00:01.600 --> 00:00:04.200 -English (US) + { + const attributes = { + ...baseAttributes, + torrentfile: 'video-720p.torrent' as any, + description: 'this is a super torrent description', + tags: [ 'tag_torrent1', 'tag_torrent2' ] + } + const { video } = await servers[0].imports.importVideo({ attributes }) + expect(video.name).to.equal('你好 世界 720p.mp4') + } + }) -00:00:05.900 --> 00:00:07.999 -This is a subtitle in American English + it('Should list the videos to import in my videos on server 1', async function () { + const { total, data } = await servers[0].videos.listMyVideos({ sort: 'createdAt' }) -00:00:10.000 --> 00:00:14.000 -Adding subtitles is very easy to do`) + expect(total).to.equal(3) - const frCaption = videoCaptions.find(caption => caption.language.id === 'fr') - expect(frCaption).to.exist - expect(frCaption.language.label).to.equal('French') - expect(frCaption.captionPath).to.match(new RegExp(`^/lazy-static/video-captions/.+-fr.vtt`)) - await testCaptionFile(servers[0].url, frCaption.captionPath, `WEBVTT -Kind: captions -Language: fr + expect(data).to.have.lengthOf(3) + expect(data[0].name).to.equal('small video - youtube') + expect(data[1].name).to.equal('super peertube2 video') + expect(data[2].name).to.equal('你好 世界 720p.mp4') + }) -00:00:01.600 --> 00:00:04.200 -Français (FR) + it('Should list the videos to import in my imports on server 1', async function () { + const { total, data: videoImports } = await servers[0].imports.getMyVideoImports({ sort: '-createdAt' }) + expect(total).to.equal(3) -00:00:05.900 --> 00:00:07.999 -C'est un sous-titre français + expect(videoImports).to.have.lengthOf(3) -00:00:10.000 --> 00:00:14.000 -Ajouter un sous-titre est vraiment facile`) - } + expect(videoImports[2].targetUrl).to.equal(FIXTURE_URLS.youtube) + expect(videoImports[2].magnetUri).to.be.null + expect(videoImports[2].torrentName).to.be.null + expect(videoImports[2].video.name).to.equal('small video - youtube') - { - const attributes = { - ...baseAttributes, - magnetUri: FIXTURE_URLS.magnet, - description: 'this is a super torrent description', - tags: [ 'tag_torrent1', 'tag_torrent2' ] - } - const { video } = await servers[0].imports.importVideo({ attributes }) - expect(video.name).to.equal('super peertube2 video') - } + expect(videoImports[1].targetUrl).to.be.null + expect(videoImports[1].magnetUri).to.equal(FIXTURE_URLS.magnet) + expect(videoImports[1].torrentName).to.be.null + expect(videoImports[1].video.name).to.equal('super peertube2 video') - { - const attributes = { - ...baseAttributes, - torrentfile: 'video-720p.torrent' as any, - description: 'this is a super torrent description', - tags: [ 'tag_torrent1', 'tag_torrent2' ] - } - const { video } = await servers[0].imports.importVideo({ attributes }) - expect(video.name).to.equal('你好 世界 720p.mp4') - } - }) + expect(videoImports[0].targetUrl).to.be.null + expect(videoImports[0].magnetUri).to.be.null + expect(videoImports[0].torrentName).to.equal('video-720p.torrent') + expect(videoImports[0].video.name).to.equal('你好 世界 720p.mp4') + }) - it('Should list the videos to import in my videos on server 1', async function () { - const { total, data } = await servers[0].videos.listMyVideos({ sort: 'createdAt' }) + it('Should have the video listed on the two instances', async function () { + this.timeout(120_000) - expect(total).to.equal(3) + await waitJobs(servers) - expect(data).to.have.lengthOf(3) - expect(data[0].name).to.equal('small video - youtube') - expect(data[1].name).to.equal('super peertube2 video') - expect(data[2].name).to.equal('你好 世界 720p.mp4') - }) + for (const server of servers) { + const { total, data } = await server.videos.list() + expect(total).to.equal(3) + expect(data).to.have.lengthOf(3) - it('Should list the videos to import in my imports on server 1', async function () { - const { total, data: videoImports } = await servers[0].imports.getMyVideoImports({ sort: '-createdAt' }) - expect(total).to.equal(3) + const [ videoHttp, videoMagnet, videoTorrent ] = data + await checkVideosServer1(server, videoHttp.uuid, videoMagnet.uuid, videoTorrent.uuid) + } + }) + + it('Should import a video on server 2 with some fields', async function () { + this.timeout(60_000) + + const attributes = { + targetUrl: FIXTURE_URLS.youtube, + channelId: servers[1].store.channel.id, + privacy: VideoPrivacy.PUBLIC, + category: 10, + licence: 7, + language: 'en', + name: 'my super name', + description: 'my super description', + tags: [ 'supertag1', 'supertag2' ] + } + const { video } = await servers[1].imports.importVideo({ attributes }) + expect(video.name).to.equal('my super name') + }) - expect(videoImports).to.have.lengthOf(3) + it('Should have the videos listed on the two instances', async function () { + this.timeout(120_000) - expect(videoImports[2].targetUrl).to.equal(FIXTURE_URLS.youtube) - expect(videoImports[2].magnetUri).to.be.null - expect(videoImports[2].torrentName).to.be.null - expect(videoImports[2].video.name).to.equal('small video - youtube') + await waitJobs(servers) - expect(videoImports[1].targetUrl).to.be.null - expect(videoImports[1].magnetUri).to.equal(FIXTURE_URLS.magnet) - expect(videoImports[1].torrentName).to.be.null - expect(videoImports[1].video.name).to.equal('super peertube2 video') + for (const server of servers) { + const { total, data } = await server.videos.list() + expect(total).to.equal(4) + expect(data).to.have.lengthOf(4) - expect(videoImports[0].targetUrl).to.be.null - expect(videoImports[0].magnetUri).to.be.null - expect(videoImports[0].torrentName).to.equal('video-720p.torrent') - expect(videoImports[0].video.name).to.equal('你好 世界 720p.mp4') - }) + await checkVideoServer2(server, data[0].uuid) - it('Should have the video listed on the two instances', async function () { - this.timeout(120_000) + const [ , videoHttp, videoMagnet, videoTorrent ] = data + await checkVideosServer1(server, videoHttp.uuid, videoMagnet.uuid, videoTorrent.uuid) + } + }) - await waitJobs(servers) + it('Should import a video that will be transcoded', async function () { + this.timeout(240_000) - for (const server of servers) { - const { total, data } = await server.videos.list() - expect(total).to.equal(3) - expect(data).to.have.lengthOf(3) + const attributes = { + name: 'transcoded video', + magnetUri: FIXTURE_URLS.magnet, + channelId: servers[1].store.channel.id, + privacy: VideoPrivacy.PUBLIC + } + const { video } = await servers[1].imports.importVideo({ attributes }) + const videoUUID = video.uuid - const [ videoHttp, videoMagnet, videoTorrent ] = data - await checkVideosServer1(server, videoHttp.uuid, videoMagnet.uuid, videoTorrent.uuid) - } - }) + await waitJobs(servers) - it('Should import a video on server 2 with some fields', async function () { - this.timeout(60_000) - - const attributes = { - targetUrl: FIXTURE_URLS.youtube, - channelId: channelIdServer2, - privacy: VideoPrivacy.PUBLIC, - category: 10, - licence: 7, - language: 'en', - name: 'my super name', - description: 'my super description', - tags: [ 'supertag1', 'supertag2' ] - } - const { video } = await servers[1].imports.importVideo({ attributes }) - expect(video.name).to.equal('my super name') - }) + for (const server of servers) { + const video = await server.videos.get({ id: videoUUID }) - it('Should have the videos listed on the two instances', async function () { - this.timeout(120_000) + expect(video.name).to.equal('transcoded video') + expect(video.files).to.have.lengthOf(4) + } + }) + + it('Should import no HDR version on a HDR video', async function () { + this.timeout(300_000) + + const config = { + transcoding: { + enabled: true, + resolutions: { + '240p': true, + '360p': false, + '480p': false, + '720p': false, + '1080p': false, // the resulting resolution shouldn't be higher than this, and not vp9.2/av01 + '1440p': false, + '2160p': false + }, + webtorrent: { enabled: true }, + hls: { enabled: false } + }, + import: { + videos: { + http: { + enabled: true + }, + torrent: { + enabled: true + } + } + } + } + await servers[0].config.updateCustomSubConfig({ newConfig: config }) - await waitJobs(servers) + const attributes = { + name: 'hdr video', + targetUrl: FIXTURE_URLS.youtubeHDR, + channelId: servers[0].store.channel.id, + privacy: VideoPrivacy.PUBLIC + } + const { video: videoImported } = await servers[0].imports.importVideo({ attributes }) + const videoUUID = videoImported.uuid + + await waitJobs(servers) + + // test resolution + const video = await servers[0].videos.get({ id: videoUUID }) + expect(video.name).to.equal('hdr video') + const maxResolution = Math.max.apply(Math, video.files.map(function (o) { return o.resolution.id })) + expect(maxResolution, 'expected max resolution not met').to.equals(VideoResolution.H_240P) + }) + + it('Should import a peertube video', async function () { + this.timeout(120_000) + + // TODO: include peertube_short when https://github.com/ytdl-org/youtube-dl/pull/29475 is merged + for (const targetUrl of [ FIXTURE_URLS.peertube_long ]) { + // for (const targetUrl of [ FIXTURE_URLS.peertube_long, FIXTURE_URLS.peertube_short ]) { + await servers[0].config.disableTranscoding() + + const attributes = { + targetUrl, + channelId: servers[0].store.channel.id, + privacy: VideoPrivacy.PUBLIC + } + const { video } = await servers[0].imports.importVideo({ attributes }) + const videoUUID = video.uuid - for (const server of servers) { - const { total, data } = await server.videos.list() - expect(total).to.equal(4) - expect(data).to.have.lengthOf(4) + await waitJobs(servers) - await checkVideoServer2(server, data[0].uuid) + for (const server of servers) { + const video = await server.videos.get({ id: videoUUID }) - const [ , videoHttp, videoMagnet, videoTorrent ] = data - await checkVideosServer1(server, videoHttp.uuid, videoMagnet.uuid, videoTorrent.uuid) - } - }) + expect(video.name).to.equal('E2E tests') + } + } + }) - it('Should import a video that will be transcoded', async function () { - this.timeout(240_000) + after(async function () { + await cleanupTests(servers) + }) + }) + } - const attributes = { - name: 'transcoded video', - magnetUri: FIXTURE_URLS.magnet, - channelId: channelIdServer2, - privacy: VideoPrivacy.PUBLIC - } - const { video } = await servers[1].imports.importVideo({ attributes }) - const videoUUID = video.uuid + runSuite('youtube-dl') - await waitJobs(servers) + runSuite('yt-dlp') - for (const server of servers) { - const video = await server.videos.get({ id: videoUUID }) + describe('Auto update', function () { + let server: PeerTubeServer - expect(video.name).to.equal('transcoded video') - expect(video.files).to.have.lengthOf(4) + function quickPeerTubeImport () { + const attributes = { + targetUrl: FIXTURE_URLS.peertube_long, + channelId: server.store.channel.id, + privacy: VideoPrivacy.PUBLIC + } + + return server.imports.importVideo({ attributes }) } - }) - it('Should import no HDR version on a HDR video', async function () { - this.timeout(300_000) - - const config = { - transcoding: { - enabled: true, - resolutions: { - '240p': true, - '360p': false, - '480p': false, - '720p': false, - '1080p': false, // the resulting resolution shouldn't be higher than this, and not vp9.2/av01 - '1440p': false, - '2160p': false - }, - webtorrent: { enabled: true }, - hls: { enabled: false } - }, - import: { - videos: { - http: { - enabled: true - }, - torrent: { - enabled: true + async function testBinaryUpdate (releaseUrl: string, releaseName: string) { + await remove(join(server.servers.buildDirectory('bin'), releaseName)) + + await server.kill() + await server.run({ + import: { + videos: { + http: { + youtube_dl_release: { + url: releaseUrl, + name: releaseName + } + } } } - } - } - await servers[0].config.updateCustomSubConfig({ newConfig: config }) + }) + + await quickPeerTubeImport() - const attributes = { - name: 'hdr video', - targetUrl: FIXTURE_URLS.youtubeHDR, - channelId: channelIdServer1, - privacy: VideoPrivacy.PUBLIC + expect(await pathExists(join(server.servers.buildDirectory('bin'), releaseName))).to.be.true } - const { video: videoImported } = await servers[0].imports.importVideo({ attributes }) - const videoUUID = videoImported.uuid - await waitJobs(servers) + before(async function () { + this.timeout(30_000) - // test resolution - const video = await servers[0].videos.get({ id: videoUUID }) - expect(video.name).to.equal('hdr video') - const maxResolution = Math.max.apply(Math, video.files.map(function (o) { return o.resolution.id })) - expect(maxResolution, 'expected max resolution not met').to.equals(VideoResolution.H_240P) - }) + // Run servers + server = await createSingleServer(1) - it('Should import a peertube video', async function () { - this.timeout(120_000) + await setAccessTokensToServers([ server ]) + await setDefaultVideoChannel([ server ]) + }) - // TODO: include peertube_short when https://github.com/ytdl-org/youtube-dl/pull/29475 is merged - for (const targetUrl of [ FIXTURE_URLS.peertube_long ]) { - // for (const targetUrl of [ FIXTURE_URLS.peertube_long, FIXTURE_URLS.peertube_short ]) { - await servers[0].config.disableTranscoding() + it('Should update youtube-dl from github URL', async function () { + this.timeout(120_000) - const attributes = { - targetUrl, - channelId: channelIdServer1, - privacy: VideoPrivacy.PUBLIC - } - const { video } = await servers[0].imports.importVideo({ attributes }) - const videoUUID = video.uuid + await testBinaryUpdate('https://api.github.com/repos/ytdl-org/youtube-dl/releases', 'youtube-dl') + }) - await waitJobs(servers) + it('Should update youtube-dl from raw URL', async function () { + this.timeout(120_000) - for (const server of servers) { - const video = await server.videos.get({ id: videoUUID }) + await testBinaryUpdate('https://yt-dl.org/downloads/latest/youtube-dl', 'youtube-dl') + }) - expect(video.name).to.equal('E2E tests') - } - } - }) + it('Should update youtube-dl from youtube-dl fork', async function () { + this.timeout(120_000) - after(async function () { - await cleanupTests(servers) + await testBinaryUpdate('https://api.github.com/repos/yt-dlp/yt-dlp/releases', 'yt-dlp') + }) }) }) diff --git a/server/tests/fixtures/video_import_preview_yt_dlp.jpg b/server/tests/fixtures/video_import_preview_yt_dlp.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9e8833bf9424964a91d8ce38718f780ca016a057 GIT binary patch literal 15844 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 BN_qeQ literal 0 HcmV?d00001 diff --git a/server/tools/peertube-import-videos.ts b/server/tools/peertube-import-videos.ts index 758b561e1..54ac910e6 100644 --- a/server/tools/peertube-import-videos.ts +++ b/server/tools/peertube-import-videos.ts @@ -4,13 +4,9 @@ registerTSPaths() import { program } from 'commander' import { accessSync, constants } from 'fs' import { remove } from 'fs-extra' -import { truncate } from 'lodash' import { join } from 'path' -import { promisify } from 'util' -import { YoutubeDL } from '@server/helpers/youtube-dl' import { sha256 } from '../helpers/core-utils' import { doRequestAndSaveToFile } from '../helpers/requests' -import { CONSTRAINTS_FIELDS } from '../initializers/constants' import { assignToken, buildCommonVideoOptions, @@ -19,8 +15,8 @@ import { getLogger, getServerCredentials } from './cli' -import { PeerTubeServer } from '@shared/extra-utils' - +import { wait } from '@shared/extra-utils' +import { YoutubeDLCLI, YoutubeDLInfo, YoutubeDLInfoBuilder } from '@server/helpers/youtube-dl' import prompt = require('prompt') const processOptions = { @@ -73,7 +69,7 @@ getServerCredentials(command) async function run (url: string, username: string, password: string) { if (!password) password = await promptPassword() - const youtubeDLBinary = await YoutubeDL.safeGetYoutubeDL() + const youtubeDLBinary = await YoutubeDLCLI.safeGet() let info = await getYoutubeDLInfo(youtubeDLBinary, options.targetUrl, command.args) @@ -96,8 +92,6 @@ async function run (url: string, username: string, password: string) { } else if (options.last) { infoArray = infoArray.slice(-options.last) } - // Normalize utf8 fields - infoArray = infoArray.map(i => normalizeObject(i)) log.info('Will download and upload %d videos.\n', infoArray.length) @@ -105,8 +99,9 @@ async function run (url: string, username: string, password: string) { try { if (index > 0 && options.waitInterval) { log.info("Wait for %d seconds before continuing.", options.waitInterval / 1000) - await new Promise(res => setTimeout(res, options.waitInterval)) + await wait(options.waitInterval) } + await processVideo({ cwd: options.tmpdir, url, @@ -131,29 +126,26 @@ async function processVideo (parameters: { youtubeInfo: any }) { const { youtubeInfo, cwd, url, username, password } = parameters - const youtubeDL = new YoutubeDL('', []) log.debug('Fetching object.', youtubeInfo) const videoInfo = await fetchObject(youtubeInfo) log.debug('Fetched object.', videoInfo) - const originallyPublishedAt = youtubeDL.buildOriginallyPublishedAt(videoInfo) - - if (options.since && originallyPublishedAt && originallyPublishedAt.getTime() < options.since.getTime()) { - log.info('Video "%s" has been published before "%s", don\'t upload it.\n', videoInfo.title, formatDate(options.since)) + if (options.since && videoInfo.originallyPublishedAt && videoInfo.originallyPublishedAt.getTime() < options.since.getTime()) { + log.info('Video "%s" has been published before "%s", don\'t upload it.\n', videoInfo.name, formatDate(options.since)) return } - if (options.until && originallyPublishedAt && originallyPublishedAt.getTime() > options.until.getTime()) { - log.info('Video "%s" has been published after "%s", don\'t upload it.\n', videoInfo.title, formatDate(options.until)) + if (options.until && videoInfo.originallyPublishedAt && videoInfo.originallyPublishedAt.getTime() > options.until.getTime()) { + log.info('Video "%s" has been published after "%s", don\'t upload it.\n', videoInfo.name, formatDate(options.until)) return } const server = buildServer(url) const { data } = await server.search.advancedVideoSearch({ search: { - search: videoInfo.title, + search: videoInfo.name, sort: '-match', searchTarget: 'local' } @@ -161,28 +153,32 @@ async function processVideo (parameters: { log.info('############################################################\n') - if (data.find(v => v.name === videoInfo.title)) { - log.info('Video "%s" already exists, don\'t reupload it.\n', videoInfo.title) + if (data.find(v => v.name === videoInfo.name)) { + log.info('Video "%s" already exists, don\'t reupload it.\n', videoInfo.name) return } const path = join(cwd, sha256(videoInfo.url) + '.mp4') - log.info('Downloading video "%s"...', videoInfo.title) + log.info('Downloading video "%s"...', videoInfo.name) - const youtubeDLOptions = [ '-f', youtubeDL.getYoutubeDLVideoFormat(), ...command.args, '-o', path ] try { - const youtubeDLBinary = await YoutubeDL.safeGetYoutubeDL() - const youtubeDLExec = promisify(youtubeDLBinary.exec).bind(youtubeDLBinary) - const output = await youtubeDLExec(videoInfo.url, youtubeDLOptions, processOptions) + const youtubeDLBinary = await YoutubeDLCLI.safeGet() + const output = await youtubeDLBinary.download({ + url: videoInfo.url, + format: YoutubeDLCLI.getYoutubeDLVideoFormat([]), + output: path, + additionalYoutubeDLArgs: command.args, + processOptions + }) + log.info(output.join('\n')) await uploadVideoOnPeerTube({ - youtubeDL, cwd, url, username, password, - videoInfo: normalizeObject(videoInfo), + videoInfo, videoPath: path }) } catch (err) { @@ -191,57 +187,34 @@ async function processVideo (parameters: { } async function uploadVideoOnPeerTube (parameters: { - youtubeDL: YoutubeDL - videoInfo: any + videoInfo: YoutubeDLInfo videoPath: string cwd: string url: string username: string password: string }) { - const { youtubeDL, videoInfo, videoPath, cwd, url, username, password } = parameters + const { videoInfo, videoPath, cwd, url, username, password } = parameters const server = buildServer(url) await assignToken(server, username, password) - const category = await getCategory(server, videoInfo.categories) - const licence = getLicence(videoInfo.license) - let tags = [] - if (Array.isArray(videoInfo.tags)) { - tags = videoInfo.tags - .filter(t => t.length < CONSTRAINTS_FIELDS.VIDEOS.TAG.max && t.length > CONSTRAINTS_FIELDS.VIDEOS.TAG.min) - .map(t => t.normalize()) - .slice(0, 5) - } - - let thumbnailfile - if (videoInfo.thumbnail) { - thumbnailfile = join(cwd, sha256(videoInfo.thumbnail) + '.jpg') + let thumbnailfile: string + if (videoInfo.thumbnailUrl) { + thumbnailfile = join(cwd, sha256(videoInfo.thumbnailUrl) + '.jpg') - await doRequestAndSaveToFile(videoInfo.thumbnail, thumbnailfile) + await doRequestAndSaveToFile(videoInfo.thumbnailUrl, thumbnailfile) } - const originallyPublishedAt = youtubeDL.buildOriginallyPublishedAt(videoInfo) - - const defaultAttributes = { - name: truncate(videoInfo.title, { - length: CONSTRAINTS_FIELDS.VIDEOS.NAME.max, - separator: /,? +/, - omission: ' […]' - }), - category, - licence, - nsfw: isNSFW(videoInfo), - description: videoInfo.description, - tags - } - - const baseAttributes = await buildVideoAttributesFromCommander(server, program, defaultAttributes) + const baseAttributes = await buildVideoAttributesFromCommander(server, program, videoInfo) const attributes = { ...baseAttributes, - originallyPublishedAt: originallyPublishedAt ? originallyPublishedAt.toISOString() : null, + originallyPublishedAt: videoInfo.originallyPublishedAt + ? videoInfo.originallyPublishedAt.toISOString() + : null, + thumbnailfile, previewfile: thumbnailfile, fixture: videoPath @@ -266,67 +239,26 @@ async function uploadVideoOnPeerTube (parameters: { await remove(videoPath) if (thumbnailfile) await remove(thumbnailfile) - log.warn('Uploaded video "%s"!\n', attributes.name) + log.info('Uploaded video "%s"!\n', attributes.name) } /* ---------------------------------------------------------- */ -async function getCategory (server: PeerTubeServer, categories: string[]) { - if (!categories) return undefined - - const categoryString = categories[0] - - if (categoryString === 'News & Politics') return 11 - - const categoriesServer = await server.videos.getCategories() - - for (const key of Object.keys(categoriesServer)) { - const categoryServer = categoriesServer[key] - if (categoryString.toLowerCase() === categoryServer.toLowerCase()) return parseInt(key, 10) - } - - return undefined -} - -function getLicence (licence: string) { - if (!licence) return undefined - - if (licence.includes('Creative Commons Attribution licence')) return 1 - - return undefined -} - -function normalizeObject (obj: any) { - const newObj: any = {} - - for (const key of Object.keys(obj)) { - // Deprecated key - if (key === 'resolution') continue - - const value = obj[key] - - if (typeof value === 'string') { - newObj[key] = value.normalize() - } else { - newObj[key] = value - } - } +async function fetchObject (info: any) { + const url = buildUrl(info) - return newObj -} + const youtubeDLCLI = await YoutubeDLCLI.safeGet() + const result = await youtubeDLCLI.getInfo({ + url, + format: YoutubeDLCLI.getYoutubeDLVideoFormat([]), + processOptions + }) -function fetchObject (info: any) { - const url = buildUrl(info) + const builder = new YoutubeDLInfoBuilder(result) - return new Promise(async (res, rej) => { - const youtubeDL = await YoutubeDL.safeGetYoutubeDL() - youtubeDL.getInfo(url, undefined, processOptions, (err, videoInfo) => { - if (err) return rej(err) + const videoInfo = builder.getInfo() - const videoInfoWithUrl = Object.assign(videoInfo, { url }) - return res(normalizeObject(videoInfoWithUrl)) - }) - }) + return { ...videoInfo, url } } function buildUrl (info: any) { @@ -340,10 +272,6 @@ function buildUrl (info: any) { return 'https://www.youtube.com/watch?v=' + info.id } -function isNSFW (info: any) { - return info.age_limit && info.age_limit >= 16 -} - function normalizeTargetUrl (url: string) { let normalizedUrl = url.replace(/\/+$/, '') @@ -404,14 +332,11 @@ function exitError (message: string, ...meta: any[]) { process.exit(-1) } -function getYoutubeDLInfo (youtubeDL: any, url: string, args: string[]) { - return new Promise((res, rej) => { - const options = [ '-j', '--flat-playlist', '--playlist-reverse', ...args ] - - youtubeDL.getInfo(url, options, processOptions, (err, info) => { - if (err) return rej(err) - - return res(info) - }) +function getYoutubeDLInfo (youtubeDLCLI: YoutubeDLCLI, url: string, args: string[]) { + return youtubeDLCLI.getInfo({ + url, + format: YoutubeDLCLI.getYoutubeDLVideoFormat([]), + additionalYoutubeDLArgs: [ '-j', '--flat-playlist', '--playlist-reverse', ...args ], + processOptions }) } diff --git a/shared/extra-utils/miscs/tests.ts b/shared/extra-utils/miscs/tests.ts index 6299a48f5..658fe5fd3 100644 --- a/shared/extra-utils/miscs/tests.ts +++ b/shared/extra-utils/miscs/tests.ts @@ -20,7 +20,7 @@ const FIXTURE_URLS = { youtubeHDR: 'https://www.youtube.com/watch?v=RQgnBB9z_N4', // eslint-disable-next-line max-len - magnet: 'magnet:?xs=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Ftorrents%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.torrent&xt=urn:btih:0f498834733e8057ed5c6f2ee2b4efd8d84a76ee&dn=super+peertube2+video&tr=wss%3A%2F%2Fpeertube2.cpy.re%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fpeertube2.cpy.re%2Ftracker%2Fannounce&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fwebseed%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.mp4', + magnet: 'magnet:?xs=https%3A%2F%2Fpeertube2.cpy.re%2Flazy-static%2Ftorrents%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.torrent&xt=urn:btih:0f498834733e8057ed5c6f2ee2b4efd8d84a76ee&dn=super+peertube2+video&tr=https%3A%2F%2Fpeertube2.cpy.re%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fpeertube2.cpy.re%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fwebseed%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.mp4', badVideo: 'https://download.cpy.re/peertube/bad_video.mp4', goodVideo: 'https://download.cpy.re/peertube/good_video.mp4', diff --git a/shared/extra-utils/videos/captions.ts b/shared/extra-utils/videos/captions.ts index fc44cd250..35e722408 100644 --- a/shared/extra-utils/videos/captions.ts +++ b/shared/extra-utils/videos/captions.ts @@ -2,12 +2,16 @@ import { expect } from 'chai' import request from 'supertest' import { HttpStatusCode } from '@shared/models' -async function testCaptionFile (url: string, captionPath: string, containsString: string) { +async function testCaptionFile (url: string, captionPath: string, toTest: RegExp | string) { const res = await request(url) .get(captionPath) .expect(HttpStatusCode.OK_200) - expect(res.text).to.contain(containsString) + if (toTest instanceof RegExp) { + expect(res.text).to.match(toTest) + } else { + expect(res.text).to.contain(toTest) + } } // --------------------------------------------------------------------------- diff --git a/support/docker/production/config/production.yaml b/support/docker/production/config/production.yaml index 54d619b1a..2a9117242 100644 --- a/support/docker/production/config/production.yaml +++ b/support/docker/production/config/production.yaml @@ -44,6 +44,7 @@ redis: # From the project root directory storage: tmp: '../data/tmp/' # Use to download data (imports etc), store uploaded files before and during processing... + bin: '../data/bin/' avatars: '../data/avatars/' videos: '../data/videos/' streaming_playlists: '../data/streaming-playlists' diff --git a/yarn.lock b/yarn.lock index d49f52b43..4e18fbbe1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2064,11 +2064,6 @@ array-differ@^3.0.0: resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== -array-find-index@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" - integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= - array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -2104,11 +2099,6 @@ arraybuffer.slice@~0.0.7: resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675" integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog== -arrify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" - integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= - arrify@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" @@ -2296,32 +2286,6 @@ better-assert@~1.0.0: dependencies: callsite "1.0.0" -bin-version-check-cli@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/bin-version-check-cli/-/bin-version-check-cli-2.0.0.tgz#7d45a23dc55024bbf741b8e66dc5c0afbac7d738" - integrity sha512-wPASWpdpQuY/qkiT0hOLpTT/siAkmM/GXkuuQ/kgF1HuO4LEoIR6CgjnmuEv6lCbOSh2CWxAqmeyynp2OA1qhQ== - dependencies: - arrify "^1.0.1" - bin-version-check "^4.0.0" - meow "^5.0.0" - -bin-version-check@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/bin-version-check/-/bin-version-check-4.0.0.tgz#7d819c62496991f80d893e6e02a3032361608f71" - integrity sha512-sR631OrhC+1f8Cvs8WyVWOA33Y8tgwjETNPyyD/myRBXLkfS/vl74FmH/lFcRl9KY3zwGh7jFhvyk9vV3/3ilQ== - dependencies: - bin-version "^3.0.0" - semver "^5.6.0" - semver-truncate "^1.1.2" - -bin-version@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/bin-version/-/bin-version-3.1.0.tgz#5b09eb280752b1bd28f0c9db3f96f2f43b6c0839" - integrity sha512-Mkfm4iE1VFt4xd4vH+gx+0/71esbfus2LsnCGe8Pi4mndSPyT+NGES/Eg99jx8/lUGWfu3z2yuB/bt5UB+iVbQ== - dependencies: - execa "^1.0.0" - find-versions "^3.0.0" - binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" @@ -2666,25 +2630,11 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camelcase-keys@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-4.2.0.tgz#a2aa5fb1af688758259c32c141426d78923b9b77" - integrity sha1-oqpfsa9oh1glnDLBQUJteJI7m3c= - dependencies: - camelcase "^4.1.0" - map-obj "^2.0.0" - quick-lru "^1.0.0" - camelcase@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== -camelcase@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" - integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= - camelcase@^5.0.0: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" @@ -3215,17 +3165,6 @@ cross-argv@^1.0.0: resolved "https://registry.yarnpkg.com/cross-argv/-/cross-argv-1.0.0.tgz#e7221e9ff73092a80496c699c8c45efb20f6486c" integrity sha512-uAVe/bgNHlPdP1VE4Sk08u9pAJ7o1x/tVQtX77T5zlhYhuwOWtVkPBEtHdvF5cq48VzeCG5i1zN4dQc8pwLYrw== -cross-spawn@^6.0.0: - version "6.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== - dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" - cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -3261,13 +3200,6 @@ css-what@^5.0.0, css-what@^5.0.1: resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe" integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw== -currently-unhandled@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" - integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= - dependencies: - array-find-index "^1.0.1" - cycle@1.0.x: version "1.0.3" resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2" @@ -3355,15 +3287,7 @@ decache@^4.6.0: dependencies: callsite "^1.0.0" -decamelize-keys@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" - integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk= - dependencies: - decamelize "^1.1.0" - map-obj "^1.0.0" - -decamelize@^1.1.0, decamelize@^1.2.0: +decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= @@ -3737,13 +3661,6 @@ err-code@^3.0.1: resolved "https://registry.yarnpkg.com/err-code/-/err-code-3.0.1.tgz#a444c7b992705f2b120ee320b09972eef331c920" integrity sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA== -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - es-abstract@^1.19.0, es-abstract@^1.19.1: version "1.19.1" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" @@ -4084,23 +4001,10 @@ event-target-shim@^5.0.0: resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== -execa@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" - integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== - dependencies: - cross-spawn "^6.0.0" - get-stream "^4.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - -execa@~5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-5.0.1.tgz#aee63b871c9b2cb56bc9addcd3c70a785c6bf0d1" - integrity sha512-4hFTjFbFzQa3aCLobpbPJR/U+VoL1wdV5ozOWjeet0AWDeYr9UFGM1eUFWHX+VtOWFq4p0xXUXfW1YxUaP4fpw== +execa@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== dependencies: cross-spawn "^7.0.3" get-stream "^6.0.0" @@ -4306,7 +4210,7 @@ find-up@5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -find-up@^2.0.0, find-up@^2.1.0: +find-up@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= @@ -4321,13 +4225,6 @@ find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" -find-versions@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/find-versions/-/find-versions-3.2.0.tgz#10297f98030a786829681690545ef659ed1d254e" - integrity sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww== - dependencies: - semver-regex "^2.0.0" - flat-cache@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" @@ -4516,7 +4413,7 @@ get-stdin@^8.0.0: resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== -get-stream@^4.0.0, get-stream@^4.1.0: +get-stream@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== @@ -4630,7 +4527,7 @@ globby@^11.0.3: merge2 "^1.3.0" slash "^3.0.0" -got@^11.8.2, got@~11.8.1: +got@^11.8.2: version "11.8.2" resolved "https://registry.yarnpkg.com/got/-/got-11.8.2.tgz#7abb3959ea28c31f3576f1576c1effce23f33599" integrity sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ== @@ -4759,18 +4656,6 @@ helmet@^4.1.0: resolved "https://registry.yarnpkg.com/helmet/-/helmet-4.6.0.tgz#579971196ba93c5978eb019e4e8ec0e50076b4df" integrity sha512-HVqALKZlR95ROkrnesdhbbZJFi/rIVSoNq6f3jA/9u6MIbTsPh3xZwihjeI5+DO/2sOV6HMHooXcEOuwskHpTg== -hh-mm-ss@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/hh-mm-ss/-/hh-mm-ss-1.2.0.tgz#6d0f0b8280824a634cb1d1f20e0bc7bc8b689948" - integrity sha512-f4I9Hz1dLpX/3mrEs7yq30+FiuO3tt5NWAqAGeBTaoeoBfB8vhcQ3BphuDc5DjZb/K809agqrAaFlP0jhEU/8w== - dependencies: - zero-fill "^2.2.3" - -hosted-git-info@^2.1.4: - version "2.8.9" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" - integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== - hpagent@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/hpagent/-/hpagent-0.1.2.tgz#cab39c66d4df2d4377dbd212295d878deb9bdaa9" @@ -5008,11 +4893,6 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= -indent-string@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" - integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok= - indexof@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" @@ -5114,11 +4994,6 @@ ipv6-normalize@1.0.1: resolved "https://registry.yarnpkg.com/ipv6-normalize/-/ipv6-normalize-1.0.1.tgz#1b3258290d365fa83239e89907dde4592e7620a8" integrity sha1-GzJYKQ02X6gyOeiZB93kWS52IKg= -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= - is-arrayish@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" @@ -5294,11 +5169,6 @@ is-path-inside@^3.0.2: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== -is-plain-obj@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" - integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= - is-plain-obj@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" @@ -5327,11 +5197,6 @@ is-shared-array-buffer@^1.0.1: resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6" integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA== -is-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= - is-stream@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" @@ -5466,11 +5331,6 @@ json-buffer@3.0.1: resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== -json-parse-better-errors@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== - json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -5703,16 +5563,6 @@ load-ip-set@^2.2.1: simple-get "^4.0.0" split "^1.0.1" -load-json-file@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" - integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= - dependencies: - graceful-fs "^4.1.2" - parse-json "^4.0.0" - pify "^3.0.0" - strip-bom "^3.0.0" - locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -5794,14 +5644,6 @@ logform@^2.2.0: safe-stable-stringify "^1.1.0" triple-beam "^1.3.0" -loud-rejection@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" - integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= - dependencies: - currently-unhandled "^0.4.1" - signal-exit "^3.0.0" - lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" @@ -5937,16 +5779,6 @@ manage-path@^2.0.0: resolved "https://registry.yarnpkg.com/manage-path/-/manage-path-2.0.0.tgz#f4cf8457b926eeee2a83b173501414bc76eb9597" integrity sha1-9M+EV7km7u4qg7FzUBQUvHbrlZc= -map-obj@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" - integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= - -map-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9" - integrity sha1-plzSkIepJZi4eRJXpSPgISIqwfk= - markdown-it-emoji@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/markdown-it-emoji/-/markdown-it-emoji-2.0.0.tgz#3164ad4c009efd946e98274f7562ad611089a231" @@ -6022,21 +5854,6 @@ mensch@^0.3.4: resolved "https://registry.yarnpkg.com/mensch/-/mensch-0.3.4.tgz#770f91b46cb16ea5b204ee735768c3f0c491fecd" integrity sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g== -meow@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/meow/-/meow-5.0.0.tgz#dfc73d63a9afc714a5e371760eb5c88b91078aa4" - integrity sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig== - dependencies: - camelcase-keys "^4.0.0" - decamelize-keys "^1.0.0" - loud-rejection "^1.0.0" - minimist-options "^3.0.1" - normalize-package-data "^2.3.4" - read-pkg-up "^3.0.0" - redent "^2.0.0" - trim-newlines "^2.0.0" - yargs-parser "^10.0.0" - merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -6135,14 +5952,6 @@ minimatch@3.0.4, minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimist-options@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-3.0.2.tgz#fba4c8191339e13ecf4d61beb03f070103f3d954" - integrity sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ== - dependencies: - arrify "^1.0.1" - is-plain-obj "^1.1.0" - minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" @@ -6180,7 +5989,7 @@ mkdirp@^0.5.1, mkdirp@^0.5.4: dependencies: minimist "^1.2.5" -mkdirp@^1.0.3, mkdirp@~1.0.4: +mkdirp@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== @@ -6392,11 +6201,6 @@ next-tick@~1.0.0: resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - node-addon-api@^3.1.0: version "3.2.1" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" @@ -6481,16 +6285,6 @@ nopt@~1.0.10: dependencies: abbrev "1" -normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: - version "2.5.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== - dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" @@ -6506,13 +6300,6 @@ normalize-url@^6.0.1: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== -npm-run-path@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" - integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= - dependencies: - path-key "^2.0.0" - npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" @@ -6813,14 +6600,6 @@ parse-headers@^2.0.0: resolved "https://registry.yarnpkg.com/parse-headers/-/parse-headers-2.0.4.tgz#9eaf2d02bed2d1eff494331ce3df36d7924760bf" integrity sha512-psZ9iZoCNFLrgRjZ1d8mn0h9WRqJwFxM9q3x7iUjN/YT2OksthDJ5TiPCu2F38kS4zutqfW+YdVVkBZZx3/1aw== -parse-json@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" - integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= - dependencies: - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - parse-srcset@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" @@ -6908,11 +6687,6 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= -path-key@^2.0.0, path-key@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= - path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" @@ -6928,13 +6702,6 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= -path-type@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" - integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== - dependencies: - pify "^3.0.0" - path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -7043,11 +6810,6 @@ piece-length@^2.0.1: resolved "https://registry.yarnpkg.com/piece-length/-/piece-length-2.0.1.tgz#dbed4e78976955f34466d0a65304d0cb21914ac9" integrity sha512-dBILiDmm43y0JPISWEmVGKBETQjwJe6mSU9GND+P9KW0SJGUwoU/odyH1nbalOP9i8WSYuqf1lQnaj92Bhw+Ug== -pify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" - integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= - pify@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" @@ -7371,11 +7133,6 @@ queue-tick@^1.0.0: resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.0.tgz#011104793a3309ae86bfeddd54e251dc94a36725" integrity sha512-ULWhjjE8BmiICGn3G8+1L9wFpERNxkf8ysxkAer4+TFdRefDaXOCV5m92aMB9FtBVmn/8sETXLXY6BfW7hyaWQ== -quick-lru@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" - integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g= - quick-lru@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" @@ -7470,23 +7227,6 @@ rdf-canonize@^3.0.0: dependencies: setimmediate "^1.0.5" -read-pkg-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" - integrity sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc= - dependencies: - find-up "^2.0.0" - read-pkg "^3.0.0" - -read-pkg@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" - integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= - dependencies: - load-json-file "^4.0.0" - normalize-package-data "^2.3.2" - path-type "^3.0.0" - read@1.0.x: version "1.0.7" resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4" @@ -7555,14 +7295,6 @@ record-cache@^1.0.2: resolved "https://registry.yarnpkg.com/record-cache/-/record-cache-1.1.1.tgz#ba3088a489f50491a4af7b14d410822c394fb811" integrity sha512-L5hZlgWc7CmGbztnemQoKE1bLu9rtI2skOB0ttE4C5+TVszLE8Rd0YLTROSgvXKLAqPumS/soyN5tJW5wJLmJQ== -redent@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-2.0.0.tgz#c1b2007b42d57eb1389079b3c8333639d5e1ccaa" - integrity sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo= - dependencies: - indent-string "^3.0.0" - strip-indent "^2.0.0" - redis-commands@1.7.0, redis-commands@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89" @@ -7655,7 +7387,7 @@ resolve-from@^4.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -resolve@^1.10.0, resolve@^1.10.1, resolve@^1.15.1, resolve@^1.20.0: +resolve@^1.10.1, resolve@^1.15.1, resolve@^1.20.0: version "1.20.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== @@ -7801,19 +7533,7 @@ semver-diff@^3.1.1: dependencies: semver "^6.3.0" -semver-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-2.0.0.tgz#a93c2c5844539a770233379107b38c7b4ac9d338" - integrity sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw== - -semver-truncate@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/semver-truncate/-/semver-truncate-1.1.2.tgz#57f41de69707a62709a7e0104ba2117109ea47e8" - integrity sha1-V/Qd5pcHpicJp+AQS6IRcQnqR+g= - dependencies: - semver "^5.3.0" - -"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1: +semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -7917,13 +7637,6 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= - dependencies: - shebang-regex "^1.0.0" - shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -7931,11 +7644,6 @@ shebang-command@^2.0.0: dependencies: shebang-regex "^3.0.0" -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= - shebang-regex@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" @@ -8176,32 +7884,6 @@ spawn-command@^0.0.2-1: resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0" integrity sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A= -spdx-correct@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" - integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" - integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== - -spdx-expression-parse@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" - integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.10" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz#0d9becccde7003d6c658d487dd48a32f0bf3014b" - integrity sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA== - speed-limiter@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/speed-limiter/-/speed-limiter-1.0.2.tgz#e4632f476a1d25d32557aad7bd089b3a0d948116" @@ -8331,11 +8013,6 @@ stream-with-known-length-to-buffer@^1.0.4: dependencies: once "^1.4.0" -streamify@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/streamify/-/streamify-1.0.0.tgz#c80a1347d6d3b905c0382011adac67402a3b1e2b" - integrity sha512-pe2ZoxE+ie5wAjRgKWb5Ur4R5Oa++eoQmHLqGGy4nQn/8BetJcpHkHXRuP3ZIJ/Ptl/rbd76fdn9aQJNys8cKA== - streamsearch@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" @@ -8444,21 +8121,11 @@ strip-bom@^3.0.0: resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= -strip-eof@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" - integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= - strip-final-newline@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== -strip-indent@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" - integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g= - strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" @@ -8726,11 +8393,6 @@ tree-kill@^1.2.2: resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== -trim-newlines@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20" - integrity sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA= - triple-beam@^1.2.0, triple-beam@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" @@ -8896,7 +8558,7 @@ unique-string@^2.0.0: dependencies: crypto-random-string "^2.0.0" -universalify@^2.0.0, universalify@~2.0.0: +universalify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== @@ -9051,14 +8713,6 @@ valid-data-url@^3.0.0: resolved "https://registry.yarnpkg.com/valid-data-url/-/valid-data-url-3.0.1.tgz#826c1744e71b5632e847dd15dbd45b9fb38aa34f" integrity sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA== -validate-npm-package-license@^3.0.1: - version "3.0.4" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" - integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - validator@^13.0.0, validator@^13.6.0: version "13.6.0" resolved "https://registry.yarnpkg.com/validator/-/validator-13.6.0.tgz#1e71899c14cdc7b2068463cb24c1cc16f6ec7059" @@ -9203,7 +8857,7 @@ which@2.0.2, which@^2.0.1, which@^2.0.2: dependencies: isexe "^2.0.0" -which@^1.1.1, which@^1.2.9: +which@^1.1.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== @@ -9429,13 +9083,6 @@ yargs-parser@20.2.4: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== -yargs-parser@^10.0.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" - integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== - dependencies: - camelcase "^4.1.0" - yargs-parser@^18.1.2: version "18.1.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" @@ -9504,19 +9151,6 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -youtube-dl@^3.0.2: - version "3.5.0" - resolved "https://registry.yarnpkg.com/youtube-dl/-/youtube-dl-3.5.0.tgz#75e7be8647128de34244cb74606edf87b9ce60fa" - integrity sha512-+I9o908rD154LqaVdP1f9Xlu+qYp3/m/bZeUwaxsAV7nR8W0IObqz0oAyxvE1Qrn7oTCvvg6MZ1oqkHIA8LA+g== - dependencies: - bin-version-check-cli "~2.0.0" - execa "~5.0.0" - got "~11.8.1" - hh-mm-ss "~1.2.0" - mkdirp "~1.0.4" - streamify "~1.0.0" - universalify "~2.0.0" - z-schema@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/z-schema/-/z-schema-5.0.1.tgz#f4d4efb1e8763c968b5539e42d11b6a47e91da62" @@ -9527,8 +9161,3 @@ z-schema@^5.0.1: validator "^13.6.0" optionalDependencies: commander "^2.7.1" - -zero-fill@^2.2.3: - version "2.2.4" - resolved "https://registry.yarnpkg.com/zero-fill/-/zero-fill-2.2.4.tgz#b041320973dbcb03cd90193270ac8d4a3da05fc1" - integrity sha512-/N5GEDauLHz2uGnuJXWO1Wfib4EC+q4yp9C1jojM7RubwEKADqIqMcYpETMm1lRop403fi3v1qTOdgDE8DIOdw== -- 2.41.0