Kanon-Bot 개발 기록: node + Typescript로 Youtube-dl 사용하기

도입

최근 여유 시간에 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가 하게 하면 될 것이다.

  1. 확장자가 mp4인 video를 지정한 경로에 다운로드
  2. 확장자가 m4a인 audio를 지정한 경로에 다운로드
  3. video와 audio를 mp4로 인코딩하되, 파일 사이즈는 50MB 미만으로 해서 지정한 경로에 저장

위 조건을 명령어로 하면 다음과 같다.

youtube-dl -f (bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4)[filesize<48M] -o {output_path} {url}

이에 맞춘 파일이 output_path에 최종적으로 저장되며, 아래와 같은 응답으로 오게 된다.

 OPMZTg1k8r0: Downloading webpage
 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가 아래와 같은 작업을 하게 하면 된다.

  1. 확장자가 webm인 audio를 지정한 경로에 다운로드
  2. webm인 audio를 mp3로 변환

위 조건을 명령어로 하면 다음과 같다.

youtube-dl -f bestaudio -o {output_path} -x --audio-format mp3 {url}

응답은 다음과 같이 오게 된다.

 OPMZTg1k8r0: Downloading webpage
 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를 해볼지도.