// ==UserScript==
// @name Facebook video downloader
// @icon https://www.facebook.com/favicon.ico
// @namespace Violentmonkey Scripts
// @match https://www.facebook.com/*
// @match https://web.facebook.com/*
// @grant GM_registerMenuCommand
// @version 1.3
// @author https://github.com/HoangTran0410
// @description Download any video on Facebook (post/chat/comment)
// @license MIT
// ==/UserScript==
(() => {
function getOverlapScore(el) {
var rect = el.getBoundingClientRect();
return (
Math.min(
rect.bottom,
window.innerHeight || document.documentElement.clientHeight
) - Math.max(0, rect.top)
);
}
function getVideoIdFromVideoElement(video) {
try {
for (let k in video.parentElement) {
if (k.startsWith("__reactProps")) {
return video.parentElement[k].children.props.videoFBID;
}
}
} catch (e) {
return null;
}
}
async function getWatchingVideoId() {
let allVideos = Array.from(document.querySelectorAll("video"));
let result = [];
for (let video of allVideos) {
let videoId = getVideoIdFromVideoElement(video);
if (videoId) {
result.push({
videoId,
overlapScore: getOverlapScore(video),
playing: !!(
video.currentTime > 0 &&
!video.paused &&
!video.ended &&
video.readyState > 2
),
});
}
}
// if there is playing video => return that
let playingVideo = result.find((_) => _.playing);
if (playingVideo) return [playingVideo.videoId];
// else return all videos in-viewport
return result
.filter((_) => _.videoId && (_.overlapScore > 0 || _.playing))
.sort((a, b) => b.overlapScore - a.overlapScore)
.map((_) => _.videoId);
}
async function getVideoUrlFromVideoId(videoId) {
let dtsg = await getDtsg();
try {
return await getLinkFbVideo2(videoId, dtsg);
} catch (e) {
return await getLinkFbVideo1(videoId, dtsg);
}
}
async function getLinkFbVideo2(videoId, dtsg) {
let res = await fetch(
"https://www.facebook.com/video/video_data_async/?video_id=" + videoId,
{
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: stringifyVariables({
__a: "1",
fb_dtsg: dtsg,
}),
}
);
let text = await res.text();
text = text.replace("for (;;);", "");
let json = JSON.parse(text);
const { hd_src, hd_src_no_ratelimit, sd_src, sd_src_no_ratelimit } =
json?.payload || {};
return hd_src_no_ratelimit || hd_src || sd_src_no_ratelimit || sd_src;
}
async function getLinkFbVideo1(videoId, dtsg) {
let res = await fetchGraphQl("5279476072161634", {
UFI2CommentsProvider_commentsKey: "CometTahoeSidePaneQuery",
caller: "CHANNEL_VIEW_FROM_PAGE_TIMELINE",
displayCommentsContextEnableComment: null,
displayCommentsContextIsAdPreview: null,
displayCommentsContextIsAggregatedShare: null,
displayCommentsContextIsStorySet: null,
displayCommentsFeedbackContext: null,
feedbackSource: 41,
feedLocation: "TAHOE",
focusCommentID: null,
privacySelectorRenderLocation: "COMET_STREAM",
renderLocation: "video_channel",
scale: 1,
streamChainingSection: !1,
useDefaultActor: !1,
videoChainingContext: null,
videoID: videoId,
}, dtsg);
let text = await res.text();
let a = JSON.parse(text.split("\n")[0]),
link = a.data.video.playable_url_quality_hd || a.data.video.playable_url;
return link;
}
function fetchGraphQl(doc_id, variables, dtsg) {
return fetch("https://www.facebook.com/api/graphql/", {
method: "POST",
headers: {
"content-type": "application/x-www-form-urlencoded",
},
body: stringifyVariables({
doc_id: doc_id,
variables: JSON.stringify(variables),
fb_dtsg: dtsg,
server_timestamps: !0,
}),
});
}
function stringifyVariables(d, e) {
let f = [],
a;
for (a in d)
if (d.hasOwnProperty(a)) {
let g = e ? e + "[" + a + "]" : a,
b = d[a];
f.push(
null !== b && "object" == typeof b
? stringifyVariables(b, g)
: encodeURIComponent(g) + "=" + encodeURIComponent(b)
);
}
return f.join("&");
}
async function getDtsg() {
return require("DTSGInitialData").token;
}
function downloadURL(url, name) {
var link = document.createElement("a");
link.target = "_blank";
link.download = name;
link.href = url;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
async function downloadWatchingVideo() {
try {
let listVideoId = await getWatchingVideoId();
if (!listVideoId?.length > 0) throw Error("No video found in the page");
console.log(listVideoId)
for (let videoId of listVideoId) {
let videoUrl = await getVideoUrlFromVideoId(videoId);
if (videoUrl) downloadURL(videoUrl, "fb_video.mp4");
}
} catch (e) {
alert("ERROR: " + e);
}
}
function resisterMenuCommand() {
GM_registerMenuCommand("Download watching video", downloadWatchingVideo);
}
resisterMenuCommand();
})();