Un script pour facebook.com permettant de télécharger des vidéos ou des collections de vidéos.
// ==UserScript==
// @name FB Video Saver
// @match https://www.facebook.com/*
// @match https://cobalt.tools/*
// @match https://*.fbcdn.net/*
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_download
// @version 2.0
// @author Macxzew
// @description Un script pour facebook.com permettant de télécharger des vidéos ou des collections de vidéos.
// @license MIT
// @namespace https://greasyfork.org/users/1425005
// ==/UserScript==
(async () => {
'use strict';
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const HOST = location.hostname;
const KEY_PENDING_URL = 'fbVS_pending_url';
const KEY_DONE_URLS = 'fbVS_done_urls';
const KEY_DONE_FBCDN = 'fbVS_done_fbcdn';
const isSavedPage = (url) =>
url.includes('/saved/');
const isVideoUrl = (url) =>
url.includes('/watch/') ||
url.includes('/reel/') ||
url.includes('/videos/');
const loadDoneList = async () => {
const arr = await GM_getValue(KEY_DONE_URLS, []);
return Array.isArray(arr) ? arr : [];
};
const saveDoneList = (arr) =>
GM_setValue(KEY_DONE_URLS, arr);
const addToDoneList = async (url) => {
if (!url)
return;
const arr = await loadDoneList();
if (!arr.includes(url)) {
arr.push(url);
await saveDoneList(arr);
console.log('[FB Video Saver] URL ajoutée à DONE :', url);
}
};
const createNotification = (message) => {
const notification = document.createElement('div');
notification.textContent = message;
Object.assign(notification.style, {
position: 'fixed',
top: '20px',
right: '20px',
backgroundColor: 'rgba(0, 128, 0, 0.9)',
color: 'white',
padding: '10px 20px',
borderRadius: '5px',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
fontSize: '14px',
zIndex: '9999',
animation: 'fadeout 3s forwards',
});
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 3000);
const styleId = 'fb-video-saver-notif-style';
if (!document.getElementById(styleId)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
@keyframes fadeout {
0% { opacity: 1; }
80% { opacity: 1; }
100% { opacity: 0; }
}
`;
document.head.appendChild(style);
}
};
const extractVideoKey = (href) => {
if (!href)
return '';
try {
const u = new URL(href);
const path = u.pathname;
if (path.startsWith('/watch')) {
const v = u.searchParams.get('v');
if (v)
return 'watch:' + v;
}
const mReel = path.match(/\/reel[s]?\/([^\/?&#]+)/);
if (mReel)
return 'reel:' + mReel[1];
const mVid = path.match(/\/videos\/([^\/?&#]+)/);
if (mVid)
return 'videos:' + mVid[1];
return 'url:' + u.origin + path;
} catch (_) {
const m = href.match(/\/(reel[s]?|videos)\/([^\/?&#]+)/);
if (m)
return m[1] + ':' + m[2];
const vMatch = href.match(/[?&]v=([^&]+)/);
if (vMatch)
return 'watch:' + vMatch[1];
return 'url:' + href.split('#')[0].split('?')[0];
}
};
const sendToCobalt = async (urlToProcess) => {
if (!urlToProcess)
return false;
try {
await GM_setValue(KEY_PENDING_URL, urlToProcess);
const w = window.open('https://cobalt.tools/', '_blank', 'noopener,noreferrer');
if (!w)
console.warn('[FB Video Saver] impossible d’ouvrir un onglet cobalt');
console.log('[FB Video Saver] Cobalt ouvert pour :', urlToProcess);
return true;
} catch (e) {
console.error('[FB Video Saver] sendToCobalt error', e);
return false;
}
};
const refreshPageContent = async () => {
let lastHeight = document.body.scrollHeight;
let timer;
while (true) {
window.scrollTo(0, document.body.scrollHeight);
await new Promise(resolve => {
clearTimeout(timer);
timer = setTimeout(resolve, 5000);
});
const currentHeight = document.body.scrollHeight;
if (currentHeight === lastHeight)
break;
lastHeight = currentHeight;
}
};
const collectAllVideoLinks = (doneSet) => {
const map = new Map();
for (const a of document.querySelectorAll('a')) {
const href = a.href;
if (!href || !isVideoUrl(href))
continue;
if (doneSet.has(href)) {
console.log('[FB Video Saver] Lien déjà dans DONE, ignoré :', href);
continue;
}
const key = extractVideoKey(href);
if (!key || map.has(key))
continue;
map.set(key, { href, key });
}
return Array.from(map.values());
};
const downloadVisibleVideo = async () => {
const url = window.location.href;
if (!isVideoUrl(url)) {
alert('Aucune vidéo détectée sur cette page.');
return false;
}
const ok = await sendToCobalt(url);
if (ok)
createNotification('Vidéo envoyée à Cobalt (onglet séparé).');
return ok;
};
const processSavedVideos = async () => {
const url = window.location.href;
if (!isSavedPage(url))
return;
const doneArr = await loadDoneList();
const doneSet = new Set(doneArr);
let lastSentKey = null;
const processedKeys = new Set();
createNotification('Scan des vidéos enregistrées en cours...');
await refreshPageContent();
const links = collectAllVideoLinks(doneSet);
console.log('[FB Video Saver] Liens uniques (par ID vidéo) à traiter :', links.length);
if (!links.length) {
createNotification('Aucune vidéo détectée sur cette page.');
return;
}
let index = 0;
const processNext = async () => {
if (index >= links.length) {
console.log('[FB Video Saver] File de liens terminée.');
createNotification('Collection terminée, actualisation /saved...');
setTimeout(() => window.location.reload(), 3000);
return;
}
const { href: link, key } = links[index++];
if (processedKeys.has(key)) {
console.log('[FB Video Saver] ID vidéo déjà traité dans ce run, skip :', key, link);
setTimeout(processNext, 0);
return;
}
if (key === lastSentKey) {
console.log('[FB Video Saver] Même ID vidéo que le précédent, skip :', key, link);
setTimeout(processNext, 0);
return;
}
processedKeys.add(key);
console.log('[FB Video Saver] Envoi à Cobalt (queue 20s, anti-doublon ID) :', key, link);
const success = await sendToCobalt(link);
if (!success)
console.warn('[FB Video Saver] Echec sendToCobalt pour :', link);
lastSentKey = key;
setTimeout(processNext, 20000);
};
processNext();
};
const addUIButton = () => {
setTimeout(() => {
if (document.getElementById('fb-video-saver-button'))
return;
const button = document.createElement('button');
button.id = 'fb-video-saver-button';
button.textContent = 'Télécharger';
Object.assign(button.style, {
position: 'fixed',
top: '1%',
right: '20%',
backgroundColor: '#007bff',
color: 'white',
padding: '10px 20px',
border: 'none',
borderRadius: '5px',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
cursor: 'pointer',
zIndex: '9999',
});
button.addEventListener('click', async () => {
const url = window.location.href;
if (isSavedPage(url)) {
await GM_setValue(KEY_PENDING_URL, '');
await GM_setValue(KEY_DONE_URLS, []);
await GM_setValue(KEY_DONE_FBCDN, []);
console.log('[FB Video Saver] Réinitialisation file, DONE fb et DONE fbcdn');
await processSavedVideos();
} else if (isVideoUrl(url)) {
await downloadVisibleVideo();
} else {
alert('Aucune action disponible pour cette page.');
}
});
document.body.appendChild(button);
}, 2500);
};
const previousUrl = { value: window.location.href };
const checkUrlChange = () => {
const currentUrl = window.location.href;
if (isSavedPage(currentUrl) && !isSavedPage(previousUrl.value)) {
console.log('[FB Video Saver] Passage vers /saved, reload forcé...');
window.location.reload();
}
previousUrl.value = currentUrl;
};
((history) => {
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = function (...args) {
const result = originalPushState.apply(this, args);
window.dispatchEvent(new Event('pushstate'));
return result;
};
history.replaceState = function (...args) {
const result = originalReplaceState.apply(this, args);
window.dispatchEvent(new Event('replacestate'));
return result;
};
})(window.history);
// fbcdn: auto-download + close, avec anti-doublon path (ignore les query)
if (HOST.endsWith('.fbcdn.net')) {
try {
const rawUrl = window.location.href;
let canon;
try {
const u = new URL(rawUrl);
canon = u.origin + u.pathname; // même fichier si même path, même si query diff
} catch {
canon = rawUrl.split('?')[0].split('#')[0];
}
const doneArr = await GM_getValue(KEY_DONE_FBCDN, []);
if (doneArr.includes(canon)) {
console.log('[FB Video Saver] fbcdn déjà téléchargé (canonique), skip :', canon);
} else {
doneArr.push(canon);
await GM_setValue(KEY_DONE_FBCDN, doneArr);
await GM_download({ url: rawUrl, name: 'video.mp4' });
console.log('[FB Video Saver] Download video.mp4 depuis fbcdn :', canon);
}
} catch (e) {
console.error('[FB Video Saver] GM_download error', e);
}
setTimeout(() => {
try { window.close(); } catch (_) {}
}, 1000);
return;
}
// cobalt.tools: remplir le champ + marquer comme DONE + lancer download
if (HOST === 'cobalt.tools') {
const urlToProcess = await GM_getValue(KEY_PENDING_URL, '');
if (!urlToProcess)
return;
await GM_setValue(KEY_PENDING_URL, '');
const tryFill = () => {
const input = document.querySelector('#link-area');
if (!input)
return false;
input.value = urlToProcess;
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
console.log('[FB Video Saver] URL collée dans #link-area');
addToDoneList(urlToProcess).catch(() => {});
return true;
};
let filled = false;
for (let i = 0; i < 20; i++) {
filled = tryFill();
if (filled)
break;
await sleep(500);
}
if (!filled) {
console.warn('[FB Video Saver] Impossible de remplir le champ sur Cobalt');
return;
}
window.__fbVS_dlClicked = false;
window.__fbVS_saveClicked = false;
setTimeout(async () => {
let clicked = false;
for (let i = 0; i < 20 && !clicked; i++) {
const btn =
document.querySelector('button#download-button.svelte-1s9ornv') ||
document.querySelector('#download-button');
if (btn) {
if (!window.__fbVS_dlClicked) {
btn.click();
window.__fbVS_dlClicked = true;
console.log('[FB Video Saver] Clic sur #download-button (une seule fois)');
}
clicked = true;
break;
}
await sleep(200);
}
if (!clicked)
console.warn('[FB Video Saver] #download-button introuvable');
setTimeout(async () => {
let clickedSave = false;
for (let j = 0; j < 30 && !clickedSave; j++) {
const saveBtn =
document.querySelector('button#button-save-download') ||
document.querySelector('#button-save-download');
if (saveBtn) {
if (!window.__fbVS_saveClicked) {
saveBtn.click();
window.__fbVS_saveClicked = true;
console.log('[FB Video Saver] Clic sur #button-save-download (1 fois)');
}
clickedSave = true;
break;
}
await sleep(300);
}
if (!clickedSave)
console.warn('[FB Video Saver] #button-save-download non trouvé (OK si non proposé)');
setTimeout(() => {
console.log('[FB Video Saver] Fermeture auto de Cobalt');
try { window.close(); } catch (_) {}
}, 5000);
}, 4000);
}, 2000);
return;
}
if (!HOST.includes('facebook.com'))
return;
window.addEventListener('load', addUIButton);
window.addEventListener('pushstate', checkUrlChange);
window.addEventListener('replacestate', checkUrlChange);
window.addEventListener('popstate', checkUrlChange);
setInterval(checkUrlChange, 500);
GM_registerMenuCommand('Download Video (via Cobalt, onglet séparé)', () => {
const url = window.location.href;
if (!isSavedPage(url) && !isVideoUrl(url)) {
alert('Aucune action disponible pour cette page.');
} else {
downloadVisibleVideo();
}
});
GM_registerMenuCommand('Process Saved Videos (via Cobalt, queue 20s)', processSavedVideos);
})();