Bored during the quarantine? Want to watch your favorite YouTube videos or play your favorite songs on loop, but don’t want to spend all your data on it? Here’s how you can write code to download it.
Disclaimer
Downloading videos from YouTube is against their policy. The purpose of the article is just to demonstrate how you can download content from the Internet, and to give you an insight into the working of the YouTube API.
Description
Every video on YouTube has a unique ID. The link to a YouTube video generally looks like:
The series of characters fJ9rUzIMcZQ
in this link represents the unique ID of this video, given by the GET query parameter v
.
1) Get information about the video
A route in the YouTube API that might be very useful is /get_video_info?video_id=${videoId}&eurl={eurl}
. This route sends back data about the video, which includes the videoId, title, length in seconds, links from where the video can be streamed in different qualities, etc. The eurl
is an important query parameter without which the video might return a playability status 'UNPLAYABLE'. An eurl
is of the format:
https://youtube.googleapis.com/v/
${videoId}
The data returned from the /get_video_info
route can be parsed using the following code snippet.
import axios from "axios";
import { URLSearchParams } from "url";
async function getVideoInfo() {
const response = await axios.get(
`https://www.youtube.com/get_video_info?video_id=${videoId}&el=embedded&eurl=${eurl}&sts=18333`
);
const parsedResponse = Object.fromEntries(new URLSearchParams(response.data));
}
getVideoInfo();
2) Parse the information
The player_response
field in the obtained parsedResponse
is the field we mainly require, particularly player_response.playabilityStatus
, player_response.videoDetails
, and player_response.streamingData
. A function may be written which returns only the required data.
import axios from 'axios';
import { URLSearchParams } from 'url';
import VideoInfo from './models/VideoInfo';
public static async getVideoInfo(videoId: string): Promise<VideoInfo> {
const videoIdRegex = /^[\w_-]+$/;
const eurl = `https://youtube.googleapis.com/v/${videoId}`;
if (!videoIdRegex.test(videoId)) {
throw new Error('Invalid videoId.');
}
const response = await axios.get(`https://www.youtube.com/get_video_info?video_id=${videoId}&el=embedded&eurl=${eurl}&sts=18333`);
const parsedResponse = Object.fromEntries(new URLSearchParams(response.data));
const jsonResponse = JSON.parse(parsedResponse.player_response);
const { playabilityStatus, videoDetails, streamingData } = jsonResponse;
const videoInfo = <VideoInfo> { playabilityStatus, videoDetails, streamingData };
return videoInfo;
}
The player_response
field in the obtained parsedResponse
is the field we mainly require, particularly player_response.playabilityStatus
, player_response.videoDetails
, and player_response.streamingData
. A function may be written which returns only the required data.This videoInfo
object has all the information you need to show details about the video or to download it. Now, you can get information about the video from videoInfo.videoDetails
, such as the title (videoInfo.videoDetails.title
), description (videoInfo.videoDetails.description
), etc.
To fetch the streaming links of these videos, you need the following two keys from the videoInfo
object:
-
videoInfo.streamingData.formats
-
videoInfo.streamingData.adaptiveFormats
3) Download content from streaming links
The links contained in the formats
array are used to stream videos having both audio and video. However, there are only a few options you have here, for example you may have to choose between 720p and 360p. You can now download content from a link of your choice (from videoInfo.streamingData.formats[index].url
) using a simple downloader function.
import axios from "axios";
import Headers from "./models/Headers";
async function download(url: string, filename: string, headers: Headers) {
return new Promise((resolve, reject) => {
axios({
method: "get",
url,
responseType: "stream",
headers,
}).then((response) => {
response.data
.pipe(fs.createWriteStream(filename))
.on("finish", (err: Error) => {
if (err) reject(err);
else resolve();
});
});
});
}
In adaptiveFormats
you can find some links which stream just the video (without the audio) and some which stream just the audio. Here, there are more options to choose from, starting from 144p to 1080p or higher, with various codecs and mime-types.
So, if you want to download a 1080p video, you may download the 1080p video-only stream and an audio-only stream, and then merge them using ‘ffmpeg’, as follows:
import ffmpeg from "fluent-ffmpeg";
export default async function mergeStreams(
videoFile: string,
audioFile: string,
outputFile: string
) {
return new Promise((resolve, reject) => {
ffmpeg(videoFile)
.input(audioFile)
.saveToFile(outputFile)
.on("error", (err) => {
reject(err);
})
.on("end", () => {
logger.info("Finished merging!");
resolve();
});
});
}
4) Doesn’t work for some videos?
Now, here's the catch. You might not be able to download some videos in this way. The videos that have a field called videoInfo.streamingData.formats[index].url
can be downloaded in this way, because in these links, a GET parameter sig
is present. However, in the videoInfo
of some videos, instead of this url
key in the formats and adaptiveFormats array elements, a key named cipher
is present. Now this cipher contains 3 things:
-
sp: The signature parameter
-
s: The signature data
-
url: The streaming link
This parameter s
has to be passed through a series of functions (reverses, swaps and splits) before it can be attached to the URL. These functions can be obtained from the JS files in the sources on https://www.youtube.com/watch?v=${videoId}
. The resultant value obtained may be attached to the url
using the query parameter as specified in the value of sp
. So, if sp = sig
, the query parameter will be attached in the fashion https://.../...&sig=sig
. This link will finally fetch you the requested video/audio stream.
The code snippets were obtained from here.