위에 그림과 같이 youtube영상을 download할수 있는 chrome extension 개발.
원래 Electron에 ffmpeg를 탑재하여 YouTube 영상을 편하게 다운로드 받을 수 있는 앱을 만들려고 했으나, 오픈 소스로 공개된 youtube-dl와 node-ytdl을 분석하다보니, 좀 더 간편한 방법을 발견하여 chrome extension으로 구현하였다.
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 두 과정으로 구성된다.
주어진 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는 원본 동영상의 소스로 이 주소를 복사한 후 브라우저에 복붙하면 바로 다운이 가능한 값이다. 이 값을 얻는 과정이 관건인데 일단 나중에 살펴보기로 한다.
extract 과정에서 얻은 원본 동영상의 source로부터 실제 download를 시작 하여 저장 공간에 write 하고, 필요할 경우 ffmpeg로 인코딩을 함께 한다
"https://r1---sn-3u-bh2lz.googlevideo.com/videoplayback?....."
-> "귀여운 고양이 동영상 모음 - 집사.mp4"
youtube-dl과 같은 역할을 하는 YouTube downloader이지만, node.js로 구현되어있고 최근에도 활발히 release되고 있다는 차이가 있다. 내부 구조는 youtube-dl처럼 extract/download로 구성되어 있지 않지만, 각각에 해당하는 기능은 여기저기 구현이 되어있다. extract/download 두 기능을 생각하며 소스를 분석하면 금방 볼 수 있다. javascript로 chrome extension을 개발하기 위해 이쪽 소스를 많이 참조하였다. core와 CLI의 repository가 분리되어있어 깔끔하다
이 두가지 오픈소스 라이브러리에서는 Extract과정에서 어떻게 동영상 source url을 알아내는 것일까? 아까 말했듯이 source url를 알면 여기에 request를 날려 바로 다운로드 할 수 있으므로 서버에서 쉽게 이 URL을 알려주지는 않을 것이다.
결론부터 말하자면 YouTube 서버에서는 source url이 담겨있는 상자와 열쇠를 client에게 넘겨준다.
시작이 반이라고 일단 다음과 같이 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
파란 선으로 표시된 값을 복사하여 새 창에 열면 원본 동영상을 다운로드 할 수 있다..!
등잔 밑이 어둡다더니 생각보다 쉽게 보물상자를 열어버렸다. 최근에 업로드된 영상은 이 방법으로 뚫리지만, 좀 예전 영상들은 이 방법으로 안 열린다. 즉 키를 꽂아야 열리는 영상들인데 차차 살펴보자.
이제 키를 꽂아야 열리는 보물상자 차례인데, 어떤 키를 어떻게 꽂아야 열리는 건지 살펴보자.
아까처럼 url필드가 없다. source url이 없지만 signatureCipher라는 값이 보인다.
이 값과 스크립트 상에 여기저기 숨겨져 있는 값을 찾아서 적절하게 섞어주면 우리가 원하는 키(key)가 완성된다. 여기서 cipher라는 키워드가 보이는데 암호화 방법중에 하나이고 이 값을 잘 복호화해주면 우리가 원하는 source URL이 나온다.
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로 영상을 어떻게 다운받을지 구현 해보기로 한다.
Extract에 대한 고민은 끝났고 이제 이거를 어떻게 Download 할지 생각해보자.
source URL를 파악했으니 이걸 <a>
태그에 href로 꽂아서 버튼만 만들어주면 된다
이렇게 다운로드 버튼이 생기고 이걸 누르면 새로운 창이 뜬다
점점점 버튼을 누르면 다운로드가 가능하다🚀🥳😚🤭
이거를 chrome extension에서 실행되도록 구현하였다.
content script에서 다음과 같은 작업을 수행하도록 한다.
ytInitialPlayerResponse.streamingData.formats
값을 읽는다<script>
태그를 뒤져 위의 값을 찾는다
지금까지 알아본 바를 도식화 해보면 위와 같다
사용자가 client app에 접속하면 source URL를 얻을수있다. 이 값은 일시적인 값으로 시간이 지나면 expire하는 값이다. 또한 영상 화질, 재생속도, audio여부, app version에 따라 여러가지 값을 갖고 있다. 이는 다양한 서버에 동적으로 mapping하여 서버 부하를 분산시키고 더 나은 사용자 경험을 제공하기 위함인것 같다.
다음 번에는 진짜로 이론적으로 살펴보겠다.
Cipher/Decipher
음성/영상이 나눠져있는 format의 경우 chrome Extension에서 어떻게 해결할 건지?
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
야근하다 읽어보는데 저랑은 전혀 상관없는 분야인데 재미있네요 잘보고갑니다