YouTube downloader - 실전편(2)

trevor.ph·2021년 9월 6일
3

의식의흐름대로

목록 보기
4/4
post-thumbnail

[0] 개요

위에 그림과 같이 youtube영상을 download할수 있는 chrome extension 개발.
원래 Electron에 ffmpeg를 탑재하여 YouTube 영상을 편하게 다운로드 받을 수 있는 앱을 만들려고 했으나, 오픈 소스로 공개된 youtube-dl와 node-ytdl을 분석하다보니, 좀 더 간편한 방법을 발견하여 chrome extension으로 구현하였다.

[1] youtube-dl 분석

youtube-dl의 간단 사용법과 내부 구조를 잠깐 분석해본다. 우선 github에 접속하여 소스를 clone하고 해당 디렉토리로 이동.
shell을 열고 다음과 같이 다운로드 하고싶은 YouTube 영상의 주소를 입력하면 된다
python3 -m youtube_dl https://www.youtube.com/watch?v=abcdef
이렇게 하면 youtube-dl 바이너리가 아닌 소스로부터 debug를 할 수 있으므로, print()를 찍어보며 어떻게 돌아가는지 가늠해볼 수 있다. 중간중간 README.md도 같이 봐주면 좋다.
동영상을 다운로드 하기까지 크게 extract 와 download 두 과정으로 구성된다.

extract

주어진 URL로부터 동영상의 제목, 업로더 원본 동영상의 주소 등의 metadata를 extract하는 과정이다
ex)

URL: https://www.youtube.com/watch?v=abcdef

-----extract----

"title": "귀여운 고양이 동영상 모음",
"author": "집사3312",
"size": 1080
"source": "https://r1---sn-3u-bh2lz.googlevideo.com/videoplayback?.....",

title, author와 같은 정보는 URL에 request하여 response HTML을 파싱하여 얻을 수 있다

source는 원본 동영상의 소스로 이 주소를 복사한 후 브라우저에 복붙하면 바로 다운이 가능한 값이다. 이 값을 얻는 과정이 관건인데 일단 나중에 살펴보기로 한다.

download

extract 과정에서 얻은 원본 동영상의 source로부터 실제 download를 시작 하여 저장 공간에 write 하고, 필요할 경우 ffmpeg로 인코딩을 함께 한다
"https://r1---sn-3u-bh2lz.googlevideo.com/videoplayback?....."
-> "귀여운 고양이 동영상 모음 - 집사.mp4"

[2] node-ytdl

youtube-dl과 같은 역할을 하는 YouTube downloader이지만, node.js로 구현되어있고 최근에도 활발히 release되고 있다는 차이가 있다. 내부 구조는 youtube-dl처럼 extract/download로 구성되어 있지 않지만, 각각에 해당하는 기능은 여기저기 구현이 되어있다. extract/download 두 기능을 생각하며 소스를 분석하면 금방 볼 수 있다. javascript로 chrome extension을 개발하기 위해 이쪽 소스를 많이 참조하였다. coreCLI의 repository가 분리되어있어 깔끔하다

[3] Extract

이 두가지 오픈소스 라이브러리에서는 Extract과정에서 어떻게 동영상 source url을 알아내는 것일까? 아까 말했듯이 source url를 알면 여기에 request를 날려 바로 다운로드 할 수 있으므로 서버에서 쉽게 이 URL을 알려주지는 않을 것이다.

결론부터 말하자면 YouTube 서버에서는 source url이 담겨있는 상자와 열쇠를 client에게 넘겨준다.

[3-1] Easy Extract

시작이 반이라고 일단 다음과 같이 curl하나 날려보자
curl https://www.youtube.com/watch\?v=abcdef > output.html
위와 같이 터미널에서 수행하면 html 파일이 하나 생기는데, 파일을 열고 videoplayback을 검색해보면 아래 보이는 script 에 원본 source url이 hard coding되어있다

videoplayback로 검색했냐고?? 저번 글에서 살펴봤듯이 개발자도구의 Network탭에서 알아낸 source url의 값이 videoplayback으로 시작하기 때문이다

cURL을 치기 귀찮으신 분들을 위해 개발자 도구를 열고 다음과 같이 입력하면 된다
ytInitialPlayerResponse.streamingData.formats

파란 선으로 표시된 값을 복사하여 새 창에 열면 원본 동영상을 다운로드 할 수 있다..!
등잔 밑이 어둡다더니 생각보다 쉽게 보물상자를 열어버렸다. 최근에 업로드된 영상은 이 방법으로 뚫리지만, 좀 예전 영상들은 이 방법으로 안 열린다. 즉 키를 꽂아야 열리는 영상들인데 차차 살펴보자.

[3-2] Normal Extract

이제 키를 꽂아야 열리는 보물상자 차례인데, 어떤 키를 어떻게 꽂아야 열리는 건지 살펴보자.

아까처럼 url필드가 없다. source url이 없지만 signatureCipher라는 값이 보인다.
이 값과 스크립트 상에 여기저기 숨겨져 있는 값을 찾아서 적절하게 섞어주면 우리가 원하는 키(key)가 완성된다. 여기서 cipher라는 키워드가 보이는데 암호화 방법중에 하나이고 이 값을 잘 복호화해주면 우리가 원하는 source URL이 나온다.

cipher

YouTube 소스를 열어 base.js를 보면 어딘가에 이렇게 생긴 함수가 있다

function(d,e){e=(e%d.length+d.length)%d.length;d.splice(-e).reverse().forEach(function(f){d.unshift(f)})}

또는

여기에 breakpoint를 걸면 이 함수 signatureCipher값이 source URL로 바뀌는 것을 볼 수 있는데. 이 함수가 곧 decipher함수, 즉 보물상자 열쇠인 셈이다. 이 복호화 함수는 client에 따라, 시점에 따라 바뀌므로 base.js를 잘 파악해둬야 한다.

node-ytdl-core 주석에 따르면 복호화 과정은 3가지 케이스가 있다고 한다
1. string reverse()
2. string의 처음 n 개의 character를 자운다(splice)
3. string의 첫 character를 n 번째 character와 swap(splice)
그런데 이 복호화 함수를 어떻게 찾았냐고? base.js에서 정규표현식을 통해 reverse()와 splice()가 있는 함수를 찾는다고 한다..😱

youtube-dl같은 경우 python으로 돌고 있기 때문에 이 함수를 찾은 뒤에는 js interpreter를 사용하여 이 함수를 python에서 쓸수 있는 과정이 추가적으로 필요하다. 고맙게도 복호화 함수를 pickup하는 부분이 오픈소스로 공개되어있기 때문에 이를 그대로 이용한다.

cipher/decipher에 대해 이제 더 이상의 자세한 설명은 생략하고, 이걸 이용해서 구한 source URL로 영상을 어떻게 다운받을지 구현 해보기로 한다.

[4] 설계 및 구현

Extract에 대한 고민은 끝났고 이제 이거를 어떻게 Download 할지 생각해보자.
source URL를 파악했으니 이걸 <a>태그에 href로 꽂아서 버튼만 만들어주면 된다

이렇게 다운로드 버튼이 생기고 이걸 누르면 새로운 창이 뜬다


점점점 버튼을 누르면 다운로드가 가능하다🚀🥳😚🤭

이거를 chrome extension에서 실행되도록 구현하였다.
content script에서 다음과 같은 작업을 수행하도록 한다.

  1. index.html에서 ytInitialPlayerResponse.streamingData.formats 값을 읽는다
    1-1. chrome extension에서는 window객체에 접근할수 없으므로 index.html에 <script> 태그를 뒤져 위의 값을 찾는다
    1-2. script태그를 찾아 eval를 하면 바로 script를 실행시켜 저 값을 얻을 수 있지만 chrome extension 에서는 eval()이 안된다
    1-3. regEx로 위의 값을 찾아낸다
  2. 찾은 값에 source URL이 박혀있으면(easy extract) 이 값을 이용하여 DOWNLOAD버튼 생성
  3. 찾은 값에 sourceSignature값이 있으면 decipher를 한다(normal extract).
    3-1. 다시 index.html에서 base.js를 찾는다.
    3-2. base.js의 script text로 부터 decipher 함수 찾는다
    3-3. decipher함수를 돌려 source URL를 구해낸다
  4. 버튼을 하나 만들고 a태그와 URL를 박는다
  5. 다른 동영상 클릭했을때도 마찬가지로 동작을 위해 mutation observer로 DOM을 관찰하며 1-4 작업을 하며 버튼을 만들어준다.

[5] YouTube 분석(뇌피셜)


지금까지 알아본 바를 도식화 해보면 위와 같다
사용자가 client app에 접속하면 source URL를 얻을수있다. 이 값은 일시적인 값으로 시간이 지나면 expire하는 값이다. 또한 영상 화질, 재생속도, audio여부, app version에 따라 여러가지 값을 갖고 있다. 이는 다양한 서버에 동적으로 mapping하여 서버 부하를 분산시키고 더 나은 사용자 경험을 제공하기 위함인것 같다.

[6] 생각할 거리

다음 번에는 진짜로 이론적으로 살펴보겠다.
Cipher/Decipher
음성/영상이 나눠져있는 format의 경우 chrome Extension에서 어떻게 해결할 건지?

[7] 참고

https://github.com/ytdl-org/youtube-dl/blob/master/CONTRIBUTING.md
https://github.com/fent/node-ytdl-core
https://stackoverflow.com/questions/21510857/best-approach-to-decode-youtube-cipher-signature-using-php-or-js

profile
It's just a shot away

1개의 댓글

comment-user-thumbnail
2023년 7월 20일

야근하다 읽어보는데 저랑은 전혀 상관없는 분야인데 재미있네요 잘보고갑니다

답글 달기