]>
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 | ||
42 | if (info.is_live === true) throw new Error('Cannot download a live streaming.') | |
43 | ||
44 | const infoBuilder = new YoutubeDLInfoBuilder(info) | |
45 | ||
46 | return infoBuilder.getInfo() | |
47 | } | |
48 | ||
2a491182 F |
49 | async getInfoForListImport (options: { |
50 | latestVideosCount?: number | |
51 | }) { | |
52 | const youtubeDL = await YoutubeDLCLI.safeGet() | |
53 | ||
54 | const list = await youtubeDL.getListInfo({ | |
55 | url: this.url, | |
56 | latestVideosCount: options.latestVideosCount, | |
57 | processOptions | |
58 | }) | |
59 | ||
60 | return list.map(info => { | |
61 | const infoBuilder = new YoutubeDLInfoBuilder(info) | |
62 | ||
63 | return infoBuilder.getInfo() | |
64 | }) | |
65 | } | |
66 | ||
62549e6c C |
67 | async getSubtitles (): Promise<YoutubeDLSubs> { |
68 | const cwd = CONFIG.STORAGE.TMP_DIR | |
69 | ||
70 | const youtubeDL = await YoutubeDLCLI.safeGet() | |
71 | ||
72 | const files = await youtubeDL.getSubs({ url: this.url, format: 'vtt', processOptions: { cwd } }) | |
73 | if (!files) return [] | |
74 | ||
75 | logger.debug('Get subtitles from youtube dl.', { url: this.url, files, ...lTags() }) | |
76 | ||
77 | const subtitles = files.reduce((acc, filename) => { | |
78 | const matched = filename.match(/\.([a-z]{2})(-[a-z]+)?\.(vtt|ttml)/i) | |
79 | if (!matched || !matched[1]) return acc | |
80 | ||
81 | return [ | |
82 | ...acc, | |
83 | { | |
84 | language: matched[1], | |
85 | path: join(cwd, filename), | |
86 | filename | |
87 | } | |
88 | ] | |
89 | }, []) | |
90 | ||
91 | return subtitles | |
92 | } | |
93 | ||
94 | async downloadVideo (fileExt: string, timeout: number): Promise<string> { | |
95 | // Leave empty the extension, youtube-dl will add it | |
96 | const pathWithoutExtension = generateVideoImportTmpPath(this.url, '') | |
97 | ||
62549e6c C |
98 | logger.info('Importing youtubeDL video %s to %s', this.url, pathWithoutExtension, lTags()) |
99 | ||
100 | const youtubeDL = await YoutubeDLCLI.safeGet() | |
101 | ||
7630e1c8 C |
102 | try { |
103 | await youtubeDL.download({ | |
104 | url: this.url, | |
5e2afe42 | 105 | format: YoutubeDLCLI.getYoutubeDLVideoFormat(this.enabledResolutions, this.useBestFormat), |
7630e1c8 C |
106 | output: pathWithoutExtension, |
107 | timeout, | |
108 | processOptions | |
62549e6c C |
109 | }) |
110 | ||
7630e1c8 C |
111 | // If youtube-dl did not guess an extension for our file, just use .mp4 as default |
112 | if (await pathExists(pathWithoutExtension)) { | |
113 | await move(pathWithoutExtension, pathWithoutExtension + '.mp4') | |
114 | } | |
d7ce63d3 | 115 | |
7630e1c8 C |
116 | return this.guessVideoPathWithExtension(pathWithoutExtension, fileExt) |
117 | } catch (err) { | |
118 | this.guessVideoPathWithExtension(pathWithoutExtension, fileExt) | |
119 | .then(path => { | |
120 | logger.debug('Error in youtube-dl import, deleting file %s.', path, { err, ...lTags() }) | |
62549e6c | 121 | |
7630e1c8 C |
122 | return remove(path) |
123 | }) | |
2a491182 | 124 | .catch(innerErr => logger.error('Cannot remove file in youtubeDL error.', { innerErr, ...lTags() })) |
7630e1c8 C |
125 | |
126 | throw err | |
127 | } | |
62549e6c C |
128 | } |
129 | ||
130 | private async guessVideoPathWithExtension (tmpPath: string, sourceExt: string) { | |
131 | if (!isVideoFileExtnameValid(sourceExt)) { | |
132 | throw new Error('Invalid video extension ' + sourceExt) | |
133 | } | |
134 | ||
135 | const extensions = [ sourceExt, '.mp4', '.mkv', '.webm' ] | |
136 | ||
137 | for (const extension of extensions) { | |
138 | const path = tmpPath + extension | |
139 | ||
140 | if (await pathExists(path)) return path | |
141 | } | |
142 | ||
143 | const directoryContent = await readdir(dirname(tmpPath)) | |
144 | ||
145 | throw new Error(`Cannot guess path of ${tmpPath}. Directory content: ${directoryContent.join(', ')}`) | |
146 | } | |
147 | } | |
148 | ||
149 | // --------------------------------------------------------------------------- | |
150 | ||
151 | export { | |
152 | YoutubeDLWrapper | |
153 | } |