Facebook Videos Detector (Generates ffmpeg download command)

This script will help you to list all facebook videos that were loaded during your session

当前为 2024-11-30 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Facebook Videos Detector (Generates ffmpeg download command)
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.1
  5. // @description This script will help you to list all facebook videos that were loaded during your session
  6. // @author Mahmoud Khudairi
  7. // @match https://www.facebook.com/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=facebook.com
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (function () {
  13. "use strict";
  14.  
  15. const loaderFunction = function () {
  16. const videos = document.getElementById("videos");
  17. const domParser = new DOMParser();
  18. const contents = JSON.parse(
  19. document.getElementById("contents").textContent
  20. );
  21. const dashURL = new URL(
  22. "https://www.facebook.com/video/playback/dash_mpd_debug.mpd"
  23. );
  24. function getContent(videoId, userInserted = false) {
  25. const videoEl = document.createElement("div");
  26. videoEl.className = "video";
  27. videos.append(videoEl);
  28. (async function () {
  29. dashURL.searchParams.set("v", videoId);
  30. const mpdData = await fetch(dashURL.toString()).then((res) =>
  31. res.text()
  32. );
  33. const mpdDom = domParser.parseFromString(mpdData, "text/xml");
  34. const sources = Array.from(
  35. mpdDom.documentElement
  36. .querySelector('AdaptationSet[contentType="video"], AdaptationSet:has(Representation[mimeType^="video"])')
  37. ?.querySelectorAll("Representation") || []
  38. ).map((r) => ({
  39. id: r.getAttribute("id"),
  40. width: +r.getAttribute("width"),
  41. height: +r.getAttribute("height"),
  42. bitrate: +r.getAttribute("bandwidth"),
  43. frameRate: r.parentElement
  44. .getAttribute("frameRate")
  45. .split("/")
  46. .reduce((a, c) => a / c),
  47. url: r.textContent,
  48. initRange: r.querySelector("Initialization").getAttribute("range"),
  49. indexRange: r.querySelector("SegmentBase").getAttribute("indexRange"),
  50. firstSegmentRange: r
  51. .querySelector("SegmentBase")
  52. .getAttribute("FBFirstSegmentRange"),
  53. mimeType: r.getAttribute("mimeType"),
  54. }));
  55. const minSource = sources[0];
  56. if (!minSource) {
  57. videoEl.remove();
  58.  
  59. if (userInserted) alert(`Your requested video: ${videoId} was not found`);
  60.  
  61. return;
  62. }
  63. const maxSource = sources.at(-1);
  64. const segmentUrl = new URL(minSource.url);
  65. segmentUrl.searchParams.set("bytestart", "0");
  66. segmentUrl.searchParams.set(
  67. "byteend",
  68. minSource.firstSegmentRange.split("-")[1]
  69. );
  70. const video = document.createElement("video");
  71. video.src = segmentUrl.toString();
  72. await new Promise((rs) =>
  73. video.addEventListener("canplay", rs, { once: 1 })
  74. );
  75. const canvas = document.createElement("canvas");
  76. const ctx = canvas.getContext("2d");
  77. canvas.width = minSource.width;
  78. canvas.height = minSource.height;
  79. canvas.style.aspectRatio = `${minSource.width}/${minSource.height}`;
  80. const bufStart = video.buffered.start(0);
  81. const bufEnd = video.buffered.end(0);
  82. const bufDuration = bufEnd - bufStart;
  83. const randomPosition = bufStart + Math.random() * bufDuration;
  84. video.currentTime = randomPosition;
  85. await new Promise((rs) =>
  86. video.addEventListener("seeked", rs, { once: 1 })
  87. );
  88. ctx.drawImage(
  89. video,
  90. 0,
  91. 0,
  92. canvas.width,
  93. canvas.height,
  94. 0,
  95. 0,
  96. canvas.width,
  97. canvas.height
  98. );
  99. videoEl.append(canvas);
  100. const maxAudio = Array.from(
  101. mpdDom.documentElement
  102. .querySelector('AdaptationSet[contentType="audio"], AdaptationSet:has(Representation[mimeType^="audio"])')
  103. ?.querySelectorAll("Representation") || []
  104. )
  105. .map((r) => ({
  106. url: r.textContent,
  107. bitrate: +r.getAttribute("bandwidth"),
  108. }))
  109. .at(-1);
  110. const downloadCommand = ["ffmpeg"];
  111. downloadCommand.push("-i", `"${maxSource.url}"`);
  112. if (maxAudio) downloadCommand.push("-i", `"${maxAudio.url}"`);
  113. downloadCommand.push("-c:v", "h264", "-b:v", maxSource.bitrate);
  114. if (maxAudio)
  115. downloadCommand.push("-c:a", "aac", "-b:a", maxAudio.bitrate);
  116. downloadCommand.push(`${videoId}.mp4`);
  117. const downloadCommandText = downloadCommand.join(" ");
  118. videoEl.addEventListener("click", function () {
  119. navigator.clipboard
  120. .writeText(downloadCommandText)
  121. .then(() => alert("Commad copied!"))
  122. .catch(() => {
  123. console.log(downloadCommandText);
  124. alert(
  125. "Could not copy command, but it was printed in browser's console"
  126. );
  127. });
  128. });
  129. })();
  130. }
  131. for (const content of contents) getContent(content)
  132. const addbtn = document.getElementById('addbtn');
  133. addbtn.addEventListener("click", () => {
  134. const id = prompt('FB Video ID:');
  135. if (id) getContent(id, true);
  136. });
  137. };
  138.  
  139. const xml = new window.XMLSerializer();
  140. const dom = window.document.implementation.createDocument(
  141. "http://www.w3.org/1999/xhtml",
  142. "html",
  143. null
  144. );
  145.  
  146. const head = dom.createElement("head");
  147. const body = dom.createElement("body");
  148.  
  149. const style = dom.createElement("style");
  150. style.innerHTML = `* {box-sizing: border-box;}
  151. body {
  152. font-family: Segoe UI;
  153. margin: 0 20px;
  154. }
  155. #videos {
  156. display: grid;
  157. grid-template-columns: repeat(auto-fit, 320px);
  158. gap: 10px;
  159. margin-bottom: 20px;
  160. }
  161. #videos .video {
  162. background-color: black;
  163. height: 180px;
  164. cursor: pointer
  165. }
  166. #videos .video canvas {
  167. width: 100%;
  168. height: 100%;
  169. object-fit: contain;
  170. }`;
  171. head.append(style);
  172.  
  173. const contents = document.createElement("script");
  174. contents.id = "contents";
  175. contents.type = "application/json";
  176. contents.textContent = "[]";
  177. head.append(contents);
  178.  
  179. const script = document.createElement("script");
  180. script.src = URL.createObjectURL(
  181. new Blob(
  182. [`window.addEventListener("load", ${loaderFunction.toString()});`],
  183. { type: "application/javascript" }
  184. )
  185. );
  186. head.append(script);
  187.  
  188. body.innerHTML =
  189. '<h1>Facebook Videos Detector</h1><p>Videos found on your facebook session:</p><div id="videos"></div><button id="addbtn">Add new video</button>';
  190. dom.firstChild.append(head, body);
  191. const cache = new Set();
  192. window.addEventListener("keyup", function (e) {
  193. viewing: if ((e.altKey || (e.ctrlKey && e.shiftKey)) && e.code === "KeyV") {
  194. e.preventDefault();
  195. // if (!cache.size) {
  196. // this.alert("There is no videos found");
  197. // break viewing;
  198. // }
  199. contents.textContent = JSON.stringify(Array.from(cache));
  200. dom.title = `Facebook Videos Detector - (${cache.size} video${
  201. cache.size > 1 ? "s" : ""
  202. } ${cache.size > 1 ? "were" : "was"} found)`;
  203. this.open(
  204. URL.createObjectURL(
  205. new Blob([xml.serializeToString(dom)], { type: dom.contentType })
  206. ),
  207. "_blank"
  208. );
  209. }
  210. });
  211. window.fetch = new Proxy(window.fetch, {
  212. async apply(target, arg, args) {
  213. const url = new URL(args[0]);
  214. caching: if (
  215. /\.mp4$/.test(url.pathname.split("/").at(-1)) &&
  216. url.searchParams.has("efg")
  217. ) {
  218. const efg = JSON.parse(window.atob(url.searchParams.get("efg")));
  219. const id = efg.video_id;
  220. if (cache.has(id)) break caching;
  221. cache.add(id);
  222. }
  223. return target.apply(arg, args);
  224. },
  225. });
  226. })();