Last active
October 15, 2025 13:38
-
-
Save chriskyfung/6c3e97e31c71493b222e482ed1f15f56 to your computer and use it in GitHub Desktop.
Userscript for batch downloading Facebook photos from facebook with the post link and captions
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Facebook photos bulk downloader | |
| // @namespace https://chriskyfung.github.io | |
| // @version 1.1.0 | |
| // @description Bulk-download photos from Facebook's photo viewer. Click "Fetch" to start downloading a batch of photos. | |
| // @license AGPL-3.0 License | |
| // @icon https://www.facebook.com/favicon.ico | |
| // @author Chris KY Fung | |
| // @match https://www.facebook.com/*/photos/* | |
| // @match https://www.facebook.com/photo.php?* | |
| // @match https://www.facebook.com/photo?* | |
| // @match https://www.facebook.com/photo/* | |
| // @grant GM_download | |
| // @grant GM_registerMenuCommand | |
| // ==/UserScript== | |
| (() => { | |
| 'use strict'; | |
| // -- Configurable Defaults -- | |
| let defaultBatchSize = 3; | |
| let isDebug = false; | |
| GM_registerMenuCommand('Settings', () => { | |
| // Create dialog | |
| const dialog = document.createElement('div'); | |
| dialog.style.cssText = 'position:fixed;top:40%;left:50%;transform:translate(-50%,-50%);background:white;padding:24px 32px;z-index:9999;border-radius:10px;box-shadow:0 2px 16px rgba(0,0,0,0.2);font-size:16px;text-align:center;'; | |
| dialog.innerHTML = ` | |
| <div style="margin-bottom:16px;"> | |
| <label> | |
| Default batch size: | |
| <input id="batch-size-input" type="number" min="1" value="${defaultBatchSize}" style="width:60px;margin-left:8px;"> | |
| </label> | |
| </div> | |
| <div style="margin-bottom:16px;"> | |
| <span>Debug mode:</span> | |
| <label style="margin-left:12px;"> | |
| <input type="radio" name="debug-mode" value="on" ${isDebug ? 'checked' : ''}> On | |
| </label> | |
| <label style="margin-left:12px;"> | |
| <input type="radio" name="debug-mode" value="off" ${!isDebug ? 'checked' : ''}> Off | |
| </label> | |
| </div> | |
| <button id="settings-ok" style="padding:6px 18px;margin-right:12px;">OK</button> | |
| <button id="settings-cancel" style="padding:6px 18px;">Cancel</button> | |
| `; | |
| document.body.appendChild(dialog); | |
| dialog.querySelector('#settings-ok').onclick = () => { | |
| const batchInput = dialog.querySelector('#batch-size-input'); | |
| const n = parseInt(batchInput.value, 10); | |
| if (!isNaN(n) && n > 0) defaultBatchSize = n; | |
| const debugValue = dialog.querySelector('input[name="debug-mode"]:checked').value; | |
| isDebug = debugValue === 'on'; | |
| dialog.remove(); | |
| alert('Settings updated.'); | |
| }; | |
| dialog.querySelector('#settings-cancel').onclick = () => { | |
| dialog.remove(); | |
| }; | |
| }); | |
| // -- Selectors & State -- | |
| const MAIN_CONTENT_SELECTOR = '[role="main"]'; | |
| const NEXT_BTN_SELECTOR = '[aria-label="下一張相片"]'; | |
| const BULK_FETCH_BTN_ID = 'fb-bulk-fetch'; | |
| const BULK_CANCEL_BTN_ID = 'fb-bulk-cancel'; | |
| const IMG_SELECTOR = 'img[data-visualcompletion]'; | |
| let bulkFetchBtn = null; | |
| let bulkCancelBtn = null; | |
| let batchSize = defaultBatchSize; | |
| let count = 0; | |
| let isRunning = false; | |
| let isCancelled = false; | |
| // -- Utility: Wait for an element or timeout -- | |
| function waitFor(selector, timeout = 5000) { | |
| return new Promise((resolve, reject) => { | |
| const deadline = Date.now() + timeout; | |
| (function poll() { | |
| const el = document.querySelector(selector); | |
| if (el) return resolve(el); | |
| if (Date.now() > deadline) return reject(`Timeout waiting for ${selector}`); | |
| setTimeout(poll, 200); | |
| })(); | |
| }); | |
| } | |
| // -- Insert "Fetch" button when photo dialog appears -- | |
| function initFetchButton() { | |
| const observer = new MutationObserver(() => { | |
| const mainContent = document.querySelector(MAIN_CONTENT_SELECTOR); | |
| if (mainContent) { | |
| injectUI(); | |
| } | |
| }); | |
| observer.observe(document.body, { childList: true, subtree: true }); | |
| // Initial injection in case the element is already present | |
| injectUI(); | |
| } | |
| // -- Create UI: Fetch, Cancel & Progress Indicator -- | |
| function injectUI() { | |
| const mainContent = document.querySelector(MAIN_CONTENT_SELECTOR); | |
| if (!mainContent) { | |
| if (isDebug) console.log('Main content not found, waiting...'); | |
| return; | |
| } | |
| const toolbar = mainContent.firstChild.lastChild.firstChild; | |
| if (!toolbar) { | |
| if (isDebug) console.log('Toolbar not found, waiting...'); | |
| return; | |
| } | |
| // Prevent duplicate button | |
| if (toolbar.querySelector(`#${BULK_FETCH_BTN_ID}`)) { | |
| bulkFetchBtn = toolbar.querySelector(`#${BULK_FETCH_BTN_ID}`); | |
| bulkCancelBtn = toolbar.querySelector(`#${BULK_CANCEL_BTN_ID}`); | |
| return; | |
| } | |
| const fetchBtn = document.createElement("div"); | |
| fetchBtn.id = BULK_FETCH_BTN_ID; | |
| fetchBtn.role = 'button'; | |
| fetchBtn.ariaLabel = 'Fetch photos'; | |
| fetchBtn.textContent = isRunning ? `⬇ ${count + 1}/${batchSize}` : '⬇ Fetch'; | |
| fetchBtn.style.cssText = ` | |
| margin-left: 4px; | |
| padding: 6px 16px; | |
| background: #1877f2; | |
| opacity: ${isRunning ? '1' : '0.5'}; | |
| color: #fff; | |
| border: none; | |
| border-radius: 15px; | |
| align-self:center; | |
| cursor: pointer; | |
| font-size: 14px; | |
| transition: opacity 0.2s; | |
| `; | |
| fetchBtn.disabled = isRunning; | |
| fetchBtn.addEventListener('mouseenter', () => { | |
| fetchBtn.style.opacity = '0.6'; | |
| }); | |
| fetchBtn.addEventListener('mouseleave', () => { | |
| fetchBtn.style.opacity = '0.5'; | |
| }); | |
| const cancelBtn = document.createElement('div'); | |
| cancelBtn.id = BULK_CANCEL_BTN_ID; | |
| cancelBtn.role = 'button'; | |
| cancelBtn.ariaLabel = 'Cancel download'; | |
| cancelBtn.textContent = '✕ Cancel'; | |
| cancelBtn.style.cssText = ` | |
| margin-left: 4px; | |
| padding: 6px 16px; | |
| background: #f04747; | |
| color: #fff; | |
| border: none; | |
| border-radius: 15px; | |
| cursor: pointer; | |
| align-self:center; | |
| font-size: 14px; | |
| display: ${isRunning ? 'block' : 'none'}; | |
| `; | |
| fetchBtn.addEventListener("click", () => { | |
| if (isDebug) console.log("Fetch button clicked"); | |
| batchSize = parseInt(prompt("How many photos to fetch:", defaultBatchSize), 10) || defaultBatchSize; | |
| startBatch(batchSize); | |
| }); | |
| cancelBtn.addEventListener('click', () => { | |
| if (isDebug) console.log("Cancel button clicked"); | |
| isCancelled = true; | |
| cancelBtn.style.display = 'none'; | |
| fetchBtn.textContent = '⬇ Fetch'; | |
| }); | |
| toolbar.prepend(fetchBtn); | |
| toolbar.prepend(cancelBtn); | |
| bulkFetchBtn = fetchBtn; | |
| bulkCancelBtn = cancelBtn; | |
| }; | |
| // -- Get button by ID with error handling -- | |
| async function getButton(id) { | |
| const button = await waitFor(`#${id}`); | |
| if (!button) { | |
| console.error(`${id} not found, cannot continue.`); | |
| alert(`${id} not found. Please reload the page.`); | |
| return; | |
| } | |
| return button; | |
| } | |
| // -- Download Logic -- | |
| async function downloadImage(url) { | |
| const filename = extractFilename(url); | |
| await GM_download({ 'url': url, 'name': filename, 'saveAs': false }); | |
| } | |
| // -- Helper: Extract filename from URL -- | |
| function extractFilename(url) { | |
| // Extract filename via URL API | |
| const path = new URL(url).pathname; // e.g. "/photos/a.123.456.jpg" | |
| return path.substring(path.lastIndexOf('/') + 1); | |
| } | |
| // -- Get current image URL -- | |
| async function getImageUrl() { | |
| const img = await waitFor(IMG_SELECTOR); | |
| return img.src; | |
| }; | |
| // -- Click "Next" button to navigate to the next photo -- | |
| async function clickNextPhoto() { | |
| const nextBtn = await waitFor(NEXT_BTN_SELECTOR); | |
| if (!nextBtn) throw new Error("Next button not found"); | |
| nextBtn.click(); | |
| } | |
| // -- Batch Download Logic -- | |
| async function startBatch(total) { | |
| isRunning = true; | |
| isCancelled = false; | |
| bulkFetchBtn.style.opacity = '1'; | |
| count = 0; | |
| try { | |
| for (; count < total; count++) { | |
| if (isCancelled) break; | |
| if (isDebug) console.log(`Processing photo ${count + 1}/${total}`); | |
| // Wait for the image to load | |
| const url = await getImageUrl(); | |
| if (!url) throw new Error('Image URL not found'); | |
| // Download the image | |
| await downloadImage(url); | |
| // Update UI | |
| bulkFetchBtn.textContent = `⬇ ${count + 1}/${total}`; | |
| // Click "Next" and wait a moment | |
| await clickNextPhoto(); | |
| await new Promise(res => setTimeout(res, 600)); | |
| } | |
| } catch (error) { | |
| console.error("Error during batch processing:", error); | |
| alert("An error occurred while processing the images. Please try again."); | |
| } finally { | |
| // Reset count after batch completion | |
| isRunning = false; | |
| isCancelled = false; | |
| bulkFetchBtn.disabled = false; | |
| bulkFetchBtn.textContent = '⬇ Fetch'; | |
| bulkCancelBtn.style.display = 'none'; | |
| } | |
| }; | |
| // -- Initialize on page load -- | |
| initFetchButton(); | |
| })(); |
Author
Hi
I am Udara working as Social Media Analyst. I looked for a tool which I can used to download photos of facebook published posts (public) and ads for me to understand and analyze how different creativities has been worked.
Can you please tell me will this tool does that?
Cheers
Author
Hi @udara86,
Thank you for reaching out. The script on this page is not designed for downloading Facebook ads, but rather for bulk downloading photos from Facebook posts. Additionally, please note that this script is intended for non-commercial use.
I want to kindly remind you that web-scraping media from Facebook may not comply with Facebook's Terms of Service. It's important to be mindful of these rules to avoid potential issues with your account.
Hi Chris,
Yep, this seems not going to work with my usecase. By the way, thanks for
the reply..
>Best Regards From
* Bob Pathirage **[**Bob]*
…On Fri, Feb 28, 2025 at 2:25 PM Chris K.Y. FUNG ***@***.***> wrote:
***@***.**** commented on this gist.
------------------------------
Hi @udara86 <https://github.com/udara86>,
Thank you for reaching out. The script on this page is not designed for
downloading Facebook ads, but rather for bulk downloading photos from
Facebook posts. Additionally, please note that this script is intended for
non-commercial use.
I want to kindly remind you that web-scraping media from Facebook may not
comply with Facebook's Terms of Service. It's important to be mindful of
these rules to avoid potential issues with your account.
—
Reply to this email directly, view it on GitHub
<https://gist.github.com/chriskyfung/6c3e97e31c71493b222e482ed1f15f56#gistcomment-5460547>
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAIH5GL5XE75VHBGPNPW5RD2R63HJBFKMF2HI4TJMJ2XIZLTSKBKK5TBNR2WLJDUOJ2WLJDOMFWWLO3UNBZGKYLEL5YGC4TUNFRWS4DBNZ2F6YLDORUXM2LUPGBKK5TBNR2WLJDHNFZXJJDOMFWWLK3UNBZGKYLEL52HS4DFVRZXKYTKMVRXIX3UPFYGLK2HNFZXIQ3PNVWWK3TUUZ2G64DJMNZZDAVEOR4XAZNEM5UXG5FFOZQWY5LFVEYTANBQGI2TEOBQU52HE2LHM5SXFJTDOJSWC5DF>
.
You are receiving this email because you were mentioned.
Triage notifications on the go with GitHub Mobile for iOS
<https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675>
or Android
<https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub>
.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
相關文章: Tampermonkey自動化批量下載Facebook相片 - 數碼文明推廣教室 - Medium