DEV Community

wimdenherder
wimdenherder

Posted on • Updated on

Download YouTube subtitles with a few lines of code

Paste the following in the javascript developer console while being on a YouTube video page. It will download the subtitles to a .txt file. You can change the language simply on the line on the bottom

async function getSubs(langCode = 'en') {
  let ct = JSON.parse((await (await fetch(window.location.href)).text()).split('ytInitialPlayerResponse = ')[1].split(';var')[0]).captions.playerCaptionsTracklistRenderer.captionTracks, findCaptionUrl = x => ct.find(y => y.vssId.indexOf(x) === 0)?.baseUrl, firstChoice = findCaptionUrl("." + langCode), url = firstChoice ? firstChoice + "&fmt=json3" : (findCaptionUrl(".") || findCaptionUrl("a." + langCode) || ct[0].baseUrl) + "&fmt=json3&tlang=" + langCode;
  return (await (await fetch(url)).json()).events.map(x => ({...x, text: x.segs?.map(x => x.utf8)?.join(" ")?.replace(/\n/g,' ')?.replace(/♪|'|"|\.{2,}|\<[\s\S]*?\>|\{[\s\S]*?\}|\[[\s\S]*?\]/g,'')?.trim() || ''}));
}
async function logSubs(langCode) {
  const subs = await getSubs(langCode);
  const text = subs.map(x => x.text).join('\n');
  console.log(text);
  return text;
}
async function downloadSubs(langCode) {
  const text = await logSubs(langCode);
  const blob = new Blob([text], {type: 'text/plain'});
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'subs.txt';
  a.click();
  URL.revokeObjectURL(url);
}

downloadSubs('en');
Enter fullscreen mode Exit fullscreen mode

Retrieving the subtitles (getSubs function)

If you type await getSubs('de') in the console (assuming you pasted the code above), you will see an array with all subtitles in german! You can ask for any language, because the YouTube API just translates everything for you, cool he! We don't even have to translate it.

Let's break down the code into smaller parts to better understand each step:

Retrieve the caption tracks object (ct):

let ct = JSON.parse((await (await fetch(window.location.href)).text()).split('ytInitialPlayerResponse = ')[1].split(';var')[0]).captions.playerCaptionsTracklistRenderer.captionTracks;
Enter fullscreen mode Exit fullscreen mode

If you are on a YouTube page in the dev console, type ytInitialPlayerResponse and big chance you'll find an object with lots of information (including subtitles, click "captions") about this video!
I decided to retrieve this variable directly from the video page's HTML and extract the ytInitialPlayerResponse from it. Why? Because, if you watch another video, the global variable ytInitialPlayerResponse does not get updated automatically. You cannot rely on it, you have to fetch the new video page and extract the object from it.

Define a helper function (findCaptionUrl):

let findCaptionUrl = x => ct.find(y => y.vssId.indexOf(x) === 0)?.baseUrl;
Enter fullscreen mode Exit fullscreen mode

This helper function searches in the caption tracks and returns the base URL of the right caption track.
The vssId looks very similar to the languageCode as we will see in the following line.

Build the subtitles URL (url):

let firstChoice = findCaptionUrl("." + langCode);
let url = firstChoice ? firstChoice + "&fmt=json3" : (findCaptionUrl(".") || findCaptionUrl("a." + langCode) || ct[0].baseUrl) + "&fmt=json3&tlang=" + langCode;
Enter fullscreen mode Exit fullscreen mode

In order of preference we would like to retrieve:

  1. a written subtitle in our language (our firstChoice)
  2. any written subtitle (that we translate to our language)
  3. an automatic subtitle in our language
  4. any subtitle (that we translate to our language)

Note that the URL also includes the format (json3) and translate language (tlang) query parameters. The Youtube API does this for us :-)

You can skip this detail: If we find an automatic subtitle in our language (3.), we will mute the voice-over in this case automatically, because the video is already in the preferred language. This happens by coincidence, because if you translate (tlang=) to its own language it returns blank subtitles :-)

Fetch and process the subtitles:

return (await (await fetch(url)).json()).events.map(x => ({...x, text: x.segs?.map(x => x.utf8)?.join(" ")?.replace(/
/g,' ')?.replace(/♪|'|"|.{2,}|<[sS]*?>|{[sS]*?}|[[sS]*?]/g,'')?.trim() || ''}));
Enter fullscreen mode Exit fullscreen mode

This line

  • fetches the subtitles from the previously constructed URL
  • parses them as JSON
  • processes the events to extract the text of each subtitle.
  • It also cleans the text by removing unnecessary characters and formatting.

Let's break down the code into smaller parts to better understand each step:

Fetch the subtitles and convert them to JSON:

await (await fetch(url)).json()
Enter fullscreen mode Exit fullscreen mode

This part of the line fetches the subtitles from the provided URL and converts the response into a JSON object.

Process the events:

.events.map(x => ...)
Enter fullscreen mode Exit fullscreen mode

This part of the line maps over the events array of the JSON object to process each event.

Create a new object for each event:

({...x, text: ...})
Enter fullscreen mode Exit fullscreen mode

For each event, a new object is created that includes all the original properties of the event (...x) and a new text property that will contain the cleaned subtitle text.

Extract and clean the subtitle text:

x.segs?.map(x => x.utf8)?.join(" ")?.replace(/
/g,' ')?.replace(/♪|'|"|.{2,}|<[sS]*?>|{[sS]*?}|[[sS]*?]/g,'')?.trim() || ''
Enter fullscreen mode Exit fullscreen mode

This part of the line extracts the subtitle text from the event's segs property and performs the following operations:

  1. Maps over the segs array and retrieves the utf8 property of each segment.
  2. Joins the segments into a single string with spaces between them.
  3. Replaces newline characters with spaces.
  4. Removes special characters, quotes, multiple periods, and any content within angle brackets, curly braces, or square brackets.
  5. Trims whitespace from the beginning and end of the string. PS: If any of these operations fail (e.g., because the segs property is undefined), an empty string is returned (|| '').

Top comments (0)