import { move, pathExists, readdir, remove } from 'fs-extra' import { dirname, join } from 'path' import { inspect } from 'util' 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 * 30 // 30MB } class YoutubeDLWrapper { constructor ( private readonly url: string, private readonly enabledResolutions: number[], private readonly useBestFormat: boolean ) { } async getInfoForDownload (youtubeDLArgs: string[] = []): Promise { const youtubeDL = await YoutubeDLCLI.safeGet() const info = await youtubeDL.getInfo({ url: this.url, format: YoutubeDLCLI.getYoutubeDLVideoFormat(this.enabledResolutions, this.useBestFormat), additionalYoutubeDLArgs: youtubeDLArgs, processOptions }) if (!info) throw new Error(`YoutubeDL could not get info from ${this.url}`) if (info.is_live === true) throw new Error('Cannot download a live streaming.') const infoBuilder = new YoutubeDLInfoBuilder(info) return infoBuilder.getInfo() } async getInfoForListImport (options: { latestVideosCount?: number }) { const youtubeDL = await YoutubeDLCLI.safeGet() const list = await youtubeDL.getListInfo({ url: this.url, latestVideosCount: options.latestVideosCount, processOptions }) if (!Array.isArray(list)) throw new Error(`YoutubeDL could not get list info from ${this.url}: ${inspect(list)}`) return list.map(info => info.webpage_url) } 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?.[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, '') logger.info('Importing youtubeDL video %s to %s', this.url, pathWithoutExtension, lTags()) const youtubeDL = await YoutubeDLCLI.safeGet() try { await youtubeDL.download({ url: this.url, format: YoutubeDLCLI.getYoutubeDLVideoFormat(this.enabledResolutions, this.useBestFormat), output: pathWithoutExtension, timeout, processOptions }) // 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) } catch (err) { this.guessVideoPathWithExtension(pathWithoutExtension, fileExt) .then(path => { logger.debug('Error in youtube-dl import, deleting file %s.', path, { err, ...lTags() }) return remove(path) }) .catch(innerErr => logger.error('Cannot remove file in youtubeDL error.', { innerErr, ...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 }