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