Story Downloader - Facebook and Instagram

Download stories (videos and images) from Facebook and Instagram.

  1. // ==UserScript==
  2. // @name Story Downloader - Facebook and Instagram
  3. // @namespace https://github.com/oscar370
  4. // @version 2.0.3
  5. // @description Download stories (videos and images) from Facebook and Instagram.
  6. // @author oscar370
  7. // @match *.facebook.com/*
  8. // @match *.instagram.com/*
  9. // @grant none
  10. // @license GPL3
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. "use strict";
  15.  
  16. const SAFETY_DELAY = 2000;
  17.  
  18. class StoryDownloader {
  19. constructor() {
  20. this.mediaUrl = null;
  21. this.detectedVideo = null;
  22. this.init();
  23. }
  24.  
  25. init() {
  26. this.setupMutationObserver();
  27. }
  28.  
  29. setupMutationObserver() {
  30. const observer = new MutationObserver(() => {
  31. this.checkPageStructure();
  32. });
  33.  
  34. observer.observe(document.body, { childList: true, subtree: true });
  35. }
  36.  
  37. get isFacebookPage() {
  38. return /(facebook)/.test(window.location.href);
  39. }
  40.  
  41. checkPageStructure() {
  42. const btn = document.getElementById("downloadBtn");
  43.  
  44. if (/(\/stories\/)/.test(window.location.href)) {
  45. this.injectGlobalStyles();
  46. setTimeout(() => this.createButton(), SAFETY_DELAY);
  47. } else if (btn) {
  48. btn.remove();
  49. }
  50. }
  51.  
  52. injectGlobalStyles() {
  53. const style = document.createElement("style");
  54.  
  55. style.textContent = `
  56. #downloadBtn {
  57. border: none;
  58. background: transparent;
  59. color: white;
  60. cursor: pointer;
  61. z-index: 9999
  62. }
  63. `;
  64.  
  65. document.head.appendChild(style);
  66. }
  67.  
  68. createButton() {
  69. if (document.getElementById("downloadBtn")) return;
  70.  
  71. const topBars = this.isFacebookPage
  72. ? Array.from(document.querySelectorAll("div.xtotuo0"))
  73. : Array.from(document.querySelectorAll("div.x1xmf6yo"));
  74. const topBar = topBars.find((bar) => bar.offsetHeight > 0);
  75.  
  76. const btn = document.createElement("button");
  77. btn.id = "downloadBtn";
  78. btn.innerHTML = `
  79. <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-file-arrow-down-fill" viewBox="0 0 16 16">
  80. <path xmlns="http://www.w3.org/2000/svg" d="M12 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2M8 5a.5.5 0 0 1 .5.5v3.793l1.146-1.147a.5.5 0 0 1 .708.708l-2 2a.5.5 0 0 1-.708 0l-2-2a.5.5 0 1 1 .708-.708L7.5 9.293V5.5A.5.5 0 0 1 8 5"/>
  81. </svg>
  82. `;
  83. btn.addEventListener("click", () => this.handleDownload());
  84.  
  85. topBar.appendChild(btn);
  86. }
  87.  
  88. async handleDownload() {
  89. try {
  90. await this.detectMedia();
  91.  
  92. if (!this.mediaUrl) throw new Error("No multimedia content was found");
  93.  
  94. const filename = this.generateFileName();
  95.  
  96. await this.downloadMedia(this.mediaUrl, filename);
  97. } catch (error) {
  98. console.log(error);
  99. }
  100. }
  101.  
  102. async detectMedia() {
  103. return new Promise((resolve) => {
  104. const mediaDetector = () => {
  105. const video = this.findVideo();
  106. const image = this.findImage();
  107.  
  108. if (video) {
  109. this.mediaUrl = video;
  110. resolve();
  111. } else if (image) {
  112. this.mediaUrl = image.src;
  113. resolve();
  114. }
  115. };
  116. mediaDetector();
  117. });
  118. }
  119.  
  120. findVideo() {
  121. const videos = Array.from(document.querySelectorAll("video")).filter(
  122. (v) => v.offsetHeight > 0
  123. );
  124.  
  125. if (videos.length !== 0) {
  126. for (const video of videos) {
  127. const videoUrl = this.searchVideoSource(video);
  128. if (videoUrl) {
  129. this.detectedVideo = true;
  130. return videoUrl;
  131. }
  132. }
  133. }
  134.  
  135. return null;
  136. }
  137.  
  138. searchVideoSource(video) {
  139. const reactFiberKey = Object.keys(video).find((key) =>
  140. key.startsWith("__reactFiber")
  141. );
  142. if (!reactFiberKey) return null;
  143.  
  144. const reactKey = reactFiberKey.replace("__reactFiber", "");
  145. const parentElement =
  146. video.parentElement?.parentElement?.parentElement?.parentElement;
  147. const reactProps = parentElement?.[`__reactProps${reactKey}`];
  148.  
  149. const implementations =
  150. reactProps?.children?.[0]?.props?.children?.props?.implementations ||
  151. reactProps?.children?.props?.children?.props?.implementations;
  152.  
  153. let videoUrl = null;
  154.  
  155. if (implementations) {
  156. for (const index of [1, 0, 2]) {
  157. const source = implementations[index]?.data;
  158. if (source) {
  159. videoUrl =
  160. source.hdSrc || source.sdSrc || source.hd_src || source.sd_src;
  161. if (videoUrl) break;
  162. }
  163. }
  164. }
  165.  
  166. if (!videoUrl) {
  167. const videoData =
  168. video[reactFiberKey]?.return?.stateNode?.props?.videoData?.$1;
  169. videoUrl = videoData?.hd_src || videoData?.sd_src;
  170. }
  171.  
  172. return videoUrl;
  173. }
  174.  
  175. findImage() {
  176. const images = Array.from(document.querySelectorAll("img")).filter(
  177. (img) => img.offsetHeight > 0 && img.src.includes("cdn")
  178. );
  179.  
  180. return images.find((img) => {
  181. const naturalSize = img.naturalWidth * img.naturalHeight;
  182. return naturalSize >= 500000;
  183. });
  184. }
  185.  
  186. generateFileName() {
  187. const timestamp = new Date().toISOString().split("T")[0];
  188.  
  189. let userName;
  190.  
  191. if (this.isFacebookPage) {
  192. userName =
  193. Array.from(document.querySelectorAll("span.xlyipyv")).find(
  194. (e) => e.offsetWidth > 0
  195. ).innerText || "uknown";
  196. } else {
  197. userName =
  198. Array.from(document.querySelectorAll(".x1i10hfl"))
  199. .find((user) => user.offsetHeight > 0 && user.offsetHeight < 35)
  200. .pathname.replace(/\//g, "") || "uknown";
  201. }
  202.  
  203. const extension = this.detectedVideo ? "mp4" : "jpg";
  204.  
  205. return `${userName}-${timestamp}.${extension}`;
  206. }
  207.  
  208. async downloadMedia(url, filename) {
  209. try {
  210. const response = await fetch(url);
  211. const blob = await response.blob();
  212.  
  213. const link = document.createElement("a");
  214. link.href = URL.createObjectURL(blob);
  215. link.download = filename;
  216. document.body.appendChild(link);
  217. link.click();
  218. document.body.removeChild(link);
  219.  
  220. URL.revokeObjectURL(link.href);
  221. } catch (error) {
  222. console.error("Download error:", error);
  223. }
  224. }
  225. }
  226.  
  227. new StoryDownloader();
  228. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址