您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Download any video on Facebook (post/chat/comment) and stories
// ==UserScript== // @name Facebook Video & Story Downloader // @namespace King1x32 // @icon https://www.facebook.com/favicon.ico // @match https://www.facebook.com/* // @exclude-match https://www.facebook.com/ // @version 2.1 // @description Download any video on Facebook (post/chat/comment) and stories // @license MIT // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @grant GM_download // ==/UserScript== (() => { function getOverlapScore(el) { const rect = el.getBoundingClientRect(); return ( Math.min(rect.bottom, window.innerHeight || document.documentElement.clientHeight) - Math.max(0, rect.top) ); } function getVideoIdFromVideoElement(video) { try { const src = video.src; if (src && src.includes('blob:')) { const wrapper = video.closest('[data-instancekey]'); if (wrapper) { const instanceKey = wrapper.getAttribute('data-instancekey'); if (instanceKey) { const match = instanceKey.match(/id-vpuid-([a-f0-9-]+)/); if (match) return match[1]; } } } } catch (e) {} try { let currentElement = video; for (let i = 0; i < 10; i++) { if (!currentElement) break; for (let k in currentElement) { if (k.startsWith("__reactProps") || k.startsWith("__reactInternalInstance")) { try { const props = currentElement[k]; if (props && props.children && props.children.props) { const videoFBID = props.children.props.videoFBID || props.children.props.videoId; if (videoFBID) return videoFBID; } if (props && props.videoFBID) return props.videoFBID; if (props && props.videoId) return props.videoId; } catch (e) {} } } currentElement = currentElement.parentElement; } } catch (e) {} try { const url = window.location.href; const videoIdMatch = url.match(/\/videos\/(\d+)/); if (videoIdMatch) return videoIdMatch[1]; const watchMatch = url.match(/\/watch\/?\?.*[&?]v=(\d+)/); if (watchMatch) return watchMatch[1]; } catch (e) {} try { let parent = video.parentElement; for (let i = 0; i < 20; i++) { if (!parent) break; const videoId = parent.getAttribute('data-video-id') || parent.getAttribute('data-video-fbid') || parent.getAttribute('data-videoid'); if (videoId) return videoId; parent = parent.parentElement; } } catch (e) {} try { const scripts = document.querySelectorAll('script'); for (const script of scripts) { if (script.textContent) { const match = script.textContent.match(/"videoFBID":"(\d+)"/); if (match) return match[1]; const match2 = script.textContent.match(/"video_id":"(\d+)"/); if (match2) return match2[1]; } } } catch (e) {} return null; } function findVideoIdInPageData() { try { if (window.__initialData) { const dataStr = JSON.stringify(window.__initialData); const match = dataStr.match(/"videoFBID":"(\d+)"/); if (match) return [match[1]]; } if (window.require && typeof window.require.getData === 'function') { const data = window.require.getData(); if (data) { const dataStr = JSON.stringify(data); const matches = dataStr.match(/"videoFBID":"(\d+)"/g) || []; return matches.map(m => m.match(/"videoFBID":"(\d+)"/)[1]); } } const pageHtml = document.documentElement.innerHTML; const matches = pageHtml.match(/"videoFBID":"(\d+)"/g) || []; const videoIds = matches.map(m => m.match(/"videoFBID":"(\d+)"/)[1]); if (videoIds.length > 0) return [...new Set(videoIds)]; } catch (e) {} return []; } async function getWatchingVideoId() { const allVideos = Array.from(document.querySelectorAll("video")); const result = []; for (const video of allVideos) { const videoId = getVideoIdFromVideoElement(video); if (videoId) { result.push({ videoId, overlapScore: getOverlapScore(video), playing: video.currentTime > 0 && !video.paused && !video.ended && video.readyState > 2, }); } } if (result.length === 0) { const pageVideoIds = findVideoIdInPageData(); for (const videoId of pageVideoIds) { result.push({ videoId, overlapScore: 100, playing: false }); } } const playingVideo = result.find((_) => _.playing); if (playingVideo) return [playingVideo.videoId]; return result .filter((_) => _.videoId && (_.overlapScore > 0 || _.playing)) .sort((a, b) => b.overlapScore - a.overlapScore) .map((_) => _.videoId); } async function getVideoUrlFromVideoId(videoId) { const dtsg = await getDtsg(); try { return await getLinkFbVideo2(videoId, dtsg); } catch (e) { try { return await getLinkFbVideo1(videoId, dtsg); } catch (e2) { throw new Error(`Both download methods failed for video ${videoId}`); } } } async function getLinkFbVideo2(videoId, dtsg) { const res = await fetch( "https://www.facebook.com/video/video_data_async/?video_id=" + videoId, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded", "x-requested-with": "XMLHttpRequest", }, body: stringifyVariables({ __a: "1", fb_dtsg: dtsg, }), } ); let text = await res.text(); text = text.replace("for (;;);", ""); const json = JSON.parse(text); const { hd_src, hd_src_no_ratelimit, sd_src, sd_src_no_ratelimit } = json?.payload || {}; const videoUrl = hd_src_no_ratelimit || hd_src || sd_src_no_ratelimit || sd_src; if (!videoUrl) throw new Error('No video URL found in response'); return videoUrl; } async function getLinkFbVideo1(videoId, dtsg) { const res = await fetchGraphQl( "5279476072161634", { UFI2CommentsProvider_commentsKey: "CometTahoeSidePaneQuery", caller: "CHANNEL_VIEW_FROM_PAGE_TIMELINE", videoID: videoId, }, dtsg ); const text = await res.text(); const lines = text.split("\n"); if (lines.length === 0) throw new Error('Empty response from GraphQL'); const a = JSON.parse(lines[0]); if (!a.data || !a.data.video) throw new Error('No video data in GraphQL response'); const videoUrl = a.data.video.playable_url_quality_hd || a.data.video.playable_url; if (!videoUrl) throw new Error('No playable URL found in video data'); return videoUrl; } function fetchGraphQl(doc_id, variables, dtsg) { return fetch("https://www.facebook.com/api/graphql/", { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded", "x-requested-with": "XMLHttpRequest", }, body: stringifyVariables({ doc_id: doc_id, variables: JSON.stringify(variables), fb_dtsg: dtsg, server_timestamps: true, }), }); } function stringifyVariables(d, e) { const f = []; for (const a in d) { if (d.hasOwnProperty(a)) { const g = e ? e + "[" + a + "]" : a, b = d[a]; f.push( b !== null && typeof b === "object" ? stringifyVariables(b, g) : encodeURIComponent(g) + "=" + encodeURIComponent(b) ); } } return f.join("&"); } async function getDtsg() { try { if (window.require) { return require("DTSGInitialData").token; } } catch (e) {} try { const dtsgMatch = document.documentElement.innerHTML.match(/"token":"([^"]+)"/); if (dtsgMatch) return dtsgMatch[1]; } catch (e) {} try { const scripts = document.querySelectorAll('script'); for (const script of scripts) { if (script.textContent && script.textContent.includes('DTSGInitialData')) { const match = script.textContent.match(/"token":"([^"]+)"/); if (match) return match[1]; } } } catch (e) {} throw new Error('Could not find DTSG token'); } async function downloadURL(url, name) { if (typeof GM_download !== 'undefined') { GM_download({ url: url, name: name, saveAs: false, onload: () => console.log('Download completed: ' + name), onerror: (error) => { console.error('GM_download failed:', error); downloadUsingFetch(url, name); } }); return; } downloadUsingFetch(url, name); } async function downloadUsingFetch(url, name) { try { if (typeof GM_xmlhttpRequest !== 'undefined') { GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'blob', onload: function(response) { const blob = response.response; const blobUrl = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = blobUrl; link.download = name; link.style.display = 'none'; document.body.appendChild(link); link.click(); document.body.removeChild(link); setTimeout(() => URL.revokeObjectURL(blobUrl), 100); }, onerror: function(error) { console.error('Download failed:', error); window.open(url, '_blank'); } }); } else { const response = await fetch(url); const blob = await response.blob(); const blobUrl = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = blobUrl; link.download = name; link.style.display = 'none'; document.body.appendChild(link); link.click(); document.body.removeChild(link); setTimeout(() => URL.revokeObjectURL(blobUrl), 100); } } catch (error) { console.error('Download failed:', error); if (confirm('Direct download failed. Would you like to open the video in a new tab? You can right-click and save from there.')) { window.open(url, '_blank'); } } } async function downloadWatchingVideo() { try { const listVideoId = await getWatchingVideoId(); if (!listVideoId?.length) { throw Error("No video found on the page."); } let downloadCount = 0; for (const videoId of listVideoId) { try { const videoUrl = await getVideoUrlFromVideoId(videoId); if (videoUrl) { await downloadURL(videoUrl, `fb_video_${videoId}.mp4`); downloadCount++; } } catch (e) {} } if (downloadCount === 0) { throw new Error("Could not download any videos."); } } catch (e) { alert("ERROR: " + e.message); } } function createDownloadIcon(storySaver = null) { const icon = document.createElement('div'); icon.className = 'fb-video-download-icon'; icon.innerHTML = ` <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> <polyline points="7,10 12,15 17,10"></polyline> <line x1="12" y1="15" x2="12" y2="3"></line> </svg> `; icon.style.cssText = ` position: absolute; top: 8px; right: 8px; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; background: rgba(0, 0, 0, 0.6); color: white; border-radius: 50%; cursor: pointer; transition: all 0.2s ease; z-index: 1001; backdrop-filter: blur(4px); opacity: 0.8; `; icon.addEventListener('mouseenter', () => { icon.style.opacity = '1'; icon.style.background = 'rgba(0, 0, 0, 0.8)'; icon.style.transform = 'scale(1.1)'; }); icon.addEventListener('mouseleave', () => { if (!icon.classList.contains('downloading')) { icon.style.opacity = '0.8'; icon.style.background = 'rgba(0, 0, 0, 0.6)'; icon.style.transform = 'scale(1)'; } }); icon.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); if (icon.classList.contains('downloading')) return; icon.classList.add('downloading'); icon.innerHTML = ` <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="animation: spin 1s linear infinite;"> <circle cx="12" cy="12" r="10"></circle> </svg> `; icon.style.background = 'rgba(66, 165, 245, 0.9)'; icon.style.opacity = '1'; try { if (/\/stories\//.test(window.location.href) && storySaver) { await storySaver.handleDownload(); } else { await downloadWatchingVideo(); } icon.innerHTML = ` <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"> <path d="M20 6L9 17l-5-5"></path> </svg> `; icon.style.background = 'rgba(76, 175, 80, 0.9)'; setTimeout(() => { icon.innerHTML = ` <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> <polyline points="7,10 12,15 17,10"></polyline> <line x1="12" y1="15" x2="12" y2="3"></line> </svg> `; icon.style.background = 'rgba(0, 0, 0, 0.6)'; icon.style.opacity = '0.8'; icon.classList.remove('downloading'); }, 2000); } catch (error) { icon.innerHTML = ` <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <circle cx="12" cy="12" r="10"></circle> <line x1="15" y1="9" x2="9" y2="15"></line> <line x1="9" y1="9" x2="15" y2="15"></line> </svg> `; icon.style.background = 'rgba(244, 67, 54, 0.9)'; setTimeout(() => { icon.innerHTML = ` <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> <polyline points="7,10 12,15 17,10"></polyline> <line x1="12" y1="15" x2="12" y2="3"></line> </svg> `; icon.style.background = 'rgba(0, 0, 0, 0.6)'; icon.style.opacity = '0.8'; icon.classList.remove('downloading'); }, 3000); } }); return icon; } function shouldShowDownloadButton() { const url = window.location.href; if (/\/watch\?v=/.test(url)) return false; if (/facebook\.com\/stories\//.test(url)) return false; return true; } function addDownloadIconToVideo(videoElement) { if (!shouldShowDownloadButton()) return; if (videoElement.parentElement.querySelector('.fb-video-download-icon')) return; const videoContainer = videoElement.closest('[data-instancekey]') || videoElement.closest('[class*="x5yr21d"][class*="x1n2onr6"]') || videoElement.parentElement; if (videoContainer) { const containerStyle = window.getComputedStyle(videoContainer); if (containerStyle.position === 'static') { videoContainer.style.position = 'relative'; } const icon = createDownloadIcon(); videoContainer.appendChild(icon); videoContainer.addEventListener('mouseenter', () => { icon.style.opacity = '0.8'; }); videoContainer.addEventListener('mouseleave', () => { if (!icon.classList.contains('downloading')) { icon.style.opacity = '0.6'; } }); icon.style.opacity = '0.6'; } } function observeForVideos() { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { const videos = node.querySelectorAll ? node.querySelectorAll('video') : []; if (node.tagName === 'VIDEO') { addDownloadIconToVideo(node); } videos.forEach(video => { setTimeout(() => addDownloadIconToVideo(video), 300); }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); return observer; } class StorySaver { constructor() { this.mediaUrl = null; this.detectedVideo = null; this.init(); } init() { this.setupMutationObserver(); } setupMutationObserver() { const observer = new MutationObserver(() => this.checkPageStructure()); observer.observe(document.body, { childList: true, subtree: true }); } get isFacebookPage() { return /(facebook)/.test(window.location.href); } checkPageStructure() { const btn = document.getElementById("downloadBtn"); if (/(\/stories\/)/.test(window.location.href)) { this.createButtonWithPolling(); } else if (btn) { btn.remove(); } } createButtonWithPolling() { let attempts = 0; const interval = setInterval(() => { if (document.getElementById("downloadBtn")) { clearInterval(interval); return; } const createdBtn = this.createButton(); if (createdBtn || attempts >= 5) clearInterval(interval); attempts++; }, 500); } createButton() { if (document.getElementById("downloadBtn")) return null; const topBars = this.isFacebookPage ? Array.from(document.querySelectorAll("div.xtotuo0")) : Array.from(document.querySelectorAll("div.x1xmf6yo")); const topBar = topBars.find( (bar) => bar instanceof HTMLElement && bar.offsetHeight > 0 ); if (!topBar) return null; const btn = document.createElement("button"); btn.id = "downloadBtn"; btn.textContent = "⬇"; btn.style.fontSize = "20px"; btn.style.background = "transparent"; btn.style.border = "none"; btn.style.color = "white"; btn.style.cursor = "pointer"; btn.style.zIndex = "9999"; btn.addEventListener("click", () => this.handleDownload()); topBar.appendChild(btn); return btn; } async handleDownload() { try { await this.detectMedia(); if (!this.mediaUrl) return; const filename = this.generateFileName(); await this.downloadMedia(this.mediaUrl, filename); } catch {} } async detectMedia() { const video = this.findVideo(); const image = this.findImage(); if (video) { this.mediaUrl = video; this.detectedVideo = true; } else if (image) { this.mediaUrl = image.src; this.detectedVideo = false; } } findVideo() { const videos = Array.from(document.querySelectorAll("video")).filter( (v) => v.offsetHeight > 0 ); for (const video of videos) { const url = this.searchVideoSource(video); if (url) return url; } return null; } searchVideoSource(video) { const reactFiberKey = Object.keys(video).find( (key) => key.startsWith("__reactFiber") ); if (!reactFiberKey) return null; const reactKey = reactFiberKey.replace("__reactFiber", ""); const parent = video.parentElement?.parentElement?.parentElement?.parentElement; const reactProps = parent?.[`__reactProps${reactKey}`]; const implementations = reactProps?.children?.[0]?.props?.children?.props?.implementations ?? reactProps?.children?.props?.children?.props?.implementations; if (implementations) { for (const index of [1, 0, 2]) { const source = implementations[index]?.data; const url = source?.hdSrc || source?.sdSrc || source?.hd_src || source?.sd_src; if (url) return url; } } const videoData = video[reactFiberKey]?.return?.stateNode?.props?.videoData?.$1; return videoData?.hd_src || videoData?.sd_src || null; } findImage() { const images = Array.from(document.querySelectorAll("img")).filter( (img) => img.offsetHeight > 0 && img.src.includes("cdn") ); return images.find((img) => img.height > 400) || null; } generateFileName() { const timestamp = new Date().toISOString().split("T")[0]; let userName = "unknown"; if (this.isFacebookPage) { const user = Array.from( document.querySelectorAll("span.xuxw1ft.xlyipyv") ).find((e) => e instanceof HTMLElement && e.offsetWidth > 0); userName = user?.innerText || userName; } else { const user = Array.from(document.querySelectorAll(".x1i10hfl")).find( (u) => u instanceof HTMLAnchorElement && u.offsetHeight > 0 && u.offsetHeight < 35 ); userName = user?.pathname.replace(/\//g, "") || userName; } const extension = this.detectedVideo ? "mp4" : "jpg"; return `${userName}-${timestamp}.${extension}`; } async downloadMedia(url, filename) { try { // Use the same improved download method await downloadURL(url, filename); } catch (error) { console.error('Story download failed:', error); } } } const style = document.createElement('style'); style.textContent = ` @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } `; document.head.appendChild(style); setTimeout(() => { const existingVideos = document.querySelectorAll('video'); existingVideos.forEach(video => { addDownloadIconToVideo(video); }); }, 1000); observeForVideos(); if (/\/stories\//.test(window.location.href)) { new StorySaver(); } GM_registerMenuCommand("Download Video", downloadWatchingVideo); document.addEventListener('keydown', function(e) { if (e.ctrlKey && e.shiftKey && e.key === 'D') { e.preventDefault(); downloadWatchingVideo(); } }); })();