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.