ニジカ投稿局 https://tv.nizika.tv
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

youtube-dl-wrapper.ts 4.7 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. import { move, pathExists, remove } from 'fs-extra/esm'
  2. import { readdir } from 'fs/promises'
  3. import { dirname, join } from 'path'
  4. import { inspect } from 'util'
  5. import { VideoResolutionType } from '@peertube/peertube-models'
  6. import { CONFIG } from '@server/initializers/config.js'
  7. import { isVideoFileExtnameValid } from '../custom-validators/videos.js'
  8. import { logger, loggerTagsFactory } from '../logger.js'
  9. import { generateVideoImportTmpPath } from '../utils.js'
  10. import { YoutubeDLCLI } from './youtube-dl-cli.js'
  11. import { YoutubeDLInfo, YoutubeDLInfoBuilder } from './youtube-dl-info-builder.js'
  12. const lTags = loggerTagsFactory('youtube-dl')
  13. export type YoutubeDLSubs = {
  14. language: string
  15. filename: string
  16. path: string
  17. }[]
  18. const processOptions = {
  19. maxBuffer: 1024 * 1024 * 30 // 30MB
  20. }
  21. class YoutubeDLWrapper {
  22. constructor (
  23. private readonly url: string,
  24. private readonly enabledResolutions: VideoResolutionType[],
  25. private readonly useBestFormat: boolean
  26. ) {
  27. }
  28. async getInfoForDownload (youtubeDLArgs: string[] = []): Promise<YoutubeDLInfo> {
  29. const youtubeDL = await YoutubeDLCLI.safeGet()
  30. const info = await youtubeDL.getInfo({
  31. url: this.url,
  32. format: YoutubeDLCLI.getYoutubeDLVideoFormat(this.enabledResolutions, this.useBestFormat),
  33. additionalYoutubeDLArgs: youtubeDLArgs,
  34. processOptions
  35. })
  36. if (!info) throw new Error(`YoutubeDL could not get info from ${this.url}`)
  37. if (info.is_live === true) throw new Error('Cannot download a live streaming.')
  38. const infoBuilder = new YoutubeDLInfoBuilder(info)
  39. return infoBuilder.getInfo()
  40. }
  41. async getInfoForListImport (options: {
  42. latestVideosCount?: number
  43. }) {
  44. const youtubeDL = await YoutubeDLCLI.safeGet()
  45. const list = await youtubeDL.getListInfo({
  46. url: this.url,
  47. latestVideosCount: options.latestVideosCount,
  48. processOptions
  49. })
  50. if (!Array.isArray(list)) throw new Error(`YoutubeDL could not get list info from ${this.url}: ${inspect(list)}`)
  51. return list.map(info => info.webpage_url)
  52. }
  53. async getSubtitles (): Promise<YoutubeDLSubs> {
  54. const cwd = CONFIG.STORAGE.TMP_DIR
  55. const youtubeDL = await YoutubeDLCLI.safeGet()
  56. const files = await youtubeDL.getSubs({ url: this.url, format: 'vtt', processOptions: { cwd } })
  57. if (!files) return []
  58. logger.debug('Get subtitles from youtube dl.', { url: this.url, files, ...lTags() })
  59. const subtitles = files.reduce((acc, filename) => {
  60. const matched = filename.match(/\.([a-z]{2})(-[a-z]+)?\.(vtt|ttml)/i)
  61. if (!matched?.[1]) return acc
  62. return [
  63. ...acc,
  64. {
  65. language: matched[1],
  66. path: join(cwd, filename),
  67. filename
  68. }
  69. ]
  70. }, [])
  71. return subtitles
  72. }
  73. async downloadVideo (fileExt: string, timeout: number): Promise<string> {
  74. // Leave empty the extension, youtube-dl will add it
  75. const pathWithoutExtension = generateVideoImportTmpPath(this.url, '')
  76. logger.info('Importing youtubeDL video %s to %s', this.url, pathWithoutExtension, lTags())
  77. const youtubeDL = await YoutubeDLCLI.safeGet()
  78. try {
  79. await youtubeDL.download({
  80. url: this.url,
  81. format: YoutubeDLCLI.getYoutubeDLVideoFormat(this.enabledResolutions, this.useBestFormat),
  82. output: pathWithoutExtension,
  83. timeout,
  84. processOptions
  85. })
  86. // If youtube-dl did not guess an extension for our file, just use .mp4 as default
  87. if (await pathExists(pathWithoutExtension)) {
  88. await move(pathWithoutExtension, pathWithoutExtension + '.mp4')
  89. }
  90. return this.guessVideoPathWithExtension(pathWithoutExtension, fileExt)
  91. } catch (err) {
  92. this.guessVideoPathWithExtension(pathWithoutExtension, fileExt)
  93. .then(path => {
  94. logger.debug('Error in youtube-dl import, deleting file %s.', path, { err, ...lTags() })
  95. return remove(path)
  96. })
  97. .catch(innerErr => logger.error('Cannot remove file in youtubeDL error.', { innerErr, ...lTags() }))
  98. throw err
  99. }
  100. }
  101. private async guessVideoPathWithExtension (tmpPath: string, sourceExt: string) {
  102. if (!isVideoFileExtnameValid(sourceExt)) {
  103. throw new Error('Invalid video extension ' + sourceExt)
  104. }
  105. const extensions = [ sourceExt, '.mp4', '.mkv', '.webm' ]
  106. for (const extension of extensions) {
  107. const path = tmpPath + extension
  108. if (await pathExists(path)) return path
  109. }
  110. const directoryContent = await readdir(dirname(tmpPath))
  111. throw new Error(`Cannot guess path of ${tmpPath}. Directory content: ${directoryContent.join(', ')}`)
  112. }
  113. }
  114. // ---------------------------------------------------------------------------
  115. export {
  116. YoutubeDLWrapper
  117. }