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