Skip to content

Instantly share code, notes, and snippets.

@chriskyfung
Last active October 15, 2025 13:38
Show Gist options
  • Select an option

  • Save chriskyfung/6c3e97e31c71493b222e482ed1f15f56 to your computer and use it in GitHub Desktop.

Select an option

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
// ==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();
})();
@chriskyfung
Copy link
Author

chriskyfung commented Dec 3, 2023

@udara86
Copy link

udara86 commented Feb 27, 2025

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

@chriskyfung
Copy link
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.

@udara86
Copy link

udara86 commented Mar 1, 2025 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment