이 글은 기존 운영했던 WordPress 블로그인 PyxisPub: Development Life (pyxispub.uzuki.live) 에서 가져온 글 입니다. 모든 글을 가져오지는 않으며, 작성 시점과 현재 시점에는 차이가 많이 존재합니다.
작성 시점: 2019-10-15
최근 여유 시간에 KanonDL-Bot 라고 하는 텔레그램 메신저 봇을 만들고 있는데, 해당 봇의 메인 기능은 'youtube-dl'로 영상이나 음악을 다운받아 바로 전송해주는 기능이다.
흔히 부르는 Y** to video/mp3 계열이긴 한데, 만든 사람들이 많아도 하나같이 신뢰할 수 없기에 ts도 다시 공부할겸 직접 만들고 있다.
따라서 금주의 기록에는 Youtube-DL를 node + typescript에서 사용하는 것을 정리하려고 한다.
기본적으로 Youtube-DL는 각 OS마다 바이너리를 제공하기는 하지만, 여기서는 기 제작된 드라이버인 node-youtube-dl를 사용한다.
npm install youtube-dl
해당 드라이버에서는 postinstall 로 youtubedl의 바이너리를 가져오고 설정하게 된다.
다만 youtube-dl 의 기능 중에서 ffmpeg를 의존하는 기능을 사용하려 할 경우, ffprobe or avconv is not installed 오류를 만나게 된다.
이 때는, 각 OS에 맞춰서 바이너리를 설치하면 된다.
choco install ffmpeg // chocolately (Windows 전용)
apt install ffmpeg // ubuntu (Dockerize 용)
해당 드라이버가 getInfo 메서드를 제공하기는 하지만, youtube-dl가 제공하는 모든 기능에 대해 제공하지는 않는다.
따라서 다른 기능도 마찬가지지만 드라이버에서 제공하는 기능을 사용하는 것이 아닌 직접 바이너리를 실행하는 youtube-dl exec
를 사용할 것이다.
먼저 필요한 기능인 '정보 가져오기'인 경우, youtube-dl는 자동으로 파일을 다운로드 받으므로 파일을 저장하지 않는 조건으로 가져올 필요가 있다. 해당 명령어는 아래와 같다.
youtube-dl -s -j {url}
여기서 -s는 비 다운로드 옵션, -j는 가져온 정보에 대해 json으로 덤프하는 기능이다.
이를 node-youtube-dl가 제공하는 드라이버로 실행하면 다음과 같다.
import youtubedl = require('youtube-dl');
export function extractInfo(url: string) {
return new Promise<Media.Info>((resolve, reject) => {
youtubedl.exec(url, ['-s', '-j'], {}, (err: any, output: string[]) => {
if (err) {
reject(err)
return;
}
let message = output.join('\n')
let info = JSON.parse(message)
resolve(info)
})
});
}
여기에서 Media.Info 는 결과를 정리한 Model이고, 이 쪽에서 참조할 수 있다.
사용하는 쪽에서는 다음과 같이 사용할 수 있다.
import * as YoutubeDLWrapper from '../core/youtubedl'
YoutubeDLWrapper.extractInfo(url)
.then((info: Media.Info) => {
// info - 추출한 정보들
});
기본적인 영상 다운로드 명렁어는 다음과 같다.
youtube-dl {url}
하지만 텔레그램이 지원하는 포맷은 50MB 이하의 mp4 파일 이므로 변환해줄 필요가 있다.
따라서, 아래의 과정을 youtube-dl가 하게 하면 될 것이다.
위 조건을 명령어로 하면 다음과 같다.
youtube-dl -f (bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4)[filesize<48M] -o {output_path} {url}
이에 맞춘 파일이 output_path에 최종적으로 저장되며, 아래와 같은 응답으로 오게 된다.
[youtube] OPMZTg1k8r0: Downloading webpage
[youtube] OPMZTg1k8r0: Downloading video info webpage
[download] Destination: C:\Users\winds\CodeProject\KanonDL-Bot\src\downloads\【公式】Poppin'Party「Dreamers Go!」ライブFull映像【Poppin'Party×SILENT SIREN 「NO GIRL NO CRY」DAY1】.f135.mp4
[download] 100% of 34.10MiB in 00:01
[download] Destination: C:\Users\winds\CodeProject\KanonDL-Bot\src\downloads\【公式】Poppin'Party「Dreamers Go!」ライブFull映像【Poppin'Party×SILENT SIREN 「NO GIRL NO CRY」DAY1】.f140.m4a
[download] 100% of 4.08MiB in 00:00
[ffmpeg] Merging formats into "C:\Users\winds\CodeProject\KanonDL-Bot\src\downloads\【
公式】Poppin'Party「Dreamers Go!」ライブFull映像【Poppin'Party×SILENT SIREN 「NO GIRL NO CRY」DAY1】.mp4"
Deleting original file C:\Users\winds\CodeProject\KanonDL-Bot\src\downloads\【公式】Poppin'Party「Dreamers Go!」ライブFull映像【Poppin'Party×SILENT SIREN 「NO GIRL NO CRY」DAY1】.f135.mp4 (pass -k to keep)
Deleting original file C:\Users\winds\CodeProject\KanonDL-Bot\src\downloads\【公式】Poppin'Party「Dreamers Go!」ライブFull映像【Poppin'Party×SILENT SIREN 「NO GIRL NO CRY」DAY1】.f140.m4a (pass -k to keep)
응답은 총 1회 호출되므로, 파일이 저장된 곳을 정규식으로 찾아 경로를 반환해주면 될 것이다.
따라서 이를 driver로 표현하면 다음과 같을 것이다.
export function downloadVideo(url: string) {
return new Promise<string>((resolve, reject) => {
youtubedl.exec(url, ['-f', '(bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4)[filesize<50M]', '-o', output_path], {}, (err: Error, output: string[]) => {
if (err) {
reject("")
return;
}
let message = output.join('\n')
let match = message.match(/[ffmpeg] Merging formats into (.+)/)
if (match != undefined) {
resolve(match[1].replace(/"/gi, ''))
}
});
});
};
음악도 영상과는 크게 다르지 않고, youtube-dl가 아래와 같은 작업을 하게 하면 된다.
위 조건을 명령어로 하면 다음과 같다.
youtube-dl -f bestaudio -o {output_path} -x --audio-format mp3 {url}
응답은 다음과 같이 오게 된다.
[youtube] OPMZTg1k8r0: Downloading webpage
[youtube] OPMZTg1k8r0: Downloading video info webpage
[download] Destination: C:\Users\winds\CodeProject\KanonDL-Bot\src\downloads\【公式】Poppin'Party「Dreamers Go!」ライブFull映像【Poppin'Party×SILENT SIREN 「NO GIRL NO CRY」DAY1】.webm
[download] 100% of 3.94MiB in 00:00
[ffmpeg] Destination: C:\Users\winds\CodeProject\KanonDL-Bot\src\downloads\【公式】Poppin'Party「Dreamers Go!」ライブFull映像【Poppin'Party×SILENT SIREN 「NO GIRL NO CRY」DAY1】.mp3
Deleting original file C:\Users\winds\CodeProject\KanonDL-Bot\src\downloads\【公式】Poppin'Party「Dreamers Go!」ライブFull映像【Poppin'Party×SILENT SIREN 「NO GIRL NO CRY」DAY1】.webm (pass -k to keep)
똑같이 저장된 경로를 정규식으로 찾는다면, Driver로는 아래와 같이 표현할 수 있다.
export function downloadAudio(tuple: url) {
return new Promise<string>((resolve, reject) => {
youtubedl.exec(url, ['-f', 'bestaudio', '-o', output_path, '-x', '--audio-format', 'mp3'], {}, (err: Error, output: string[]) => {
if (err) {
reject('')
return;
}
let message = output.join('\n')
let match = message.match(/[ffmpeg] Destination\: (.+)/)
if (match != undefined) {
resolve(match[1])
}
});
});
};
이로서 기본적인 youtube-dl 기능은 가져왔고, 이에 아이디어를 덧붙이면 괜찮은 봇이 만들어 질 것 같다.
현재까지 개발된 것 중에서 제일 문제라고 하면 멀티 프로세스 문제와 promise의 난해함이라고 할 수 있는데, 그나마 쉽게 적용 가능할 것 같은 RxJs로의 마이그레이션을 다음주쯤에 해볼 것 같다. 아니면 Dockerize를 해볼지도.