Scarica automaticamente tutti i media (foto e video) di un utente OnlyFans tramite auto-scroll e API scraping. Qualità massima garantita.
// ==UserScript==
// @name OnlyFans Batch Downloader
// @namespace https://t.me/aofmainhub
// @version 2.7.0
// @description Scarica automaticamente tutti i media (foto e video) di un utente OnlyFans tramite auto-scroll e API scraping. Qualità massima garantita.
// @author Rewritten by Antigravity
// @license GPL-3.0-or-later
// @match https://onlyfans.com/*
// @grant GM_download
// @grant GM_addStyle
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
// ==========================================
// 1. API INTERCEPTOR (Cattura URL originali)
// ==========================================
const collectedMedia = new Map(); // id -> mediaObject
let uiUpdateTimeout = null;
function extractUsernameFromUrl() {
const parts = window.location.pathname.split("/").filter(Boolean);
if (parts.length === 0) return "homepage";
if (parts[0] === "my" || ["settings", "notifications", "discover"].includes(parts[0])) return parts.join("_");
return parts[0];
}
// NUOVA FUNZIONE: Estrae gli URL ordinandoli dal migliore al peggiore
function findMediaUrlsInObject(obj) {
let urls = [];
let isVideo = false;
if (!obj || typeof obj !== 'object') return { urls, isVideo };
if (obj.type === 'video' || obj.videoSources) {
isVideo = true;
}
// 1. URL originale assoluto (Spesso il file raw, qualità massima in assoluto)
if (obj.source && obj.source.source) urls.push(obj.source.source);
if (obj.files && obj.files.source && obj.files.source.url) urls.push(obj.files.source.url);
if (obj.full) urls.push(obj.full);
// 2. Qualità Video Specifiche (es. 1080p, 720p, 240p)
if (obj.videoSources && typeof obj.videoSources === 'object') {
const keys = Object.keys(obj.videoSources).filter(k => obj.videoSources[k]);
// Ordiniamo le risoluzioni dalla più alta alla più bassa (es. 1080 -> 720 -> 240)
keys.sort((a, b) => {
const resA = parseInt(a) || 0;
const resB = parseInt(b) || 0;
return resB - resA;
});
// Aggiungiamo i link dei video in ordine di qualità decrescente
for (const key of keys) {
urls.push(obj.videoSources[key]);
}
}
// 3. Fallback disperato per i file bloccati (Anteprime/Thumbnail)
if (urls.length === 0) {
if (obj.preview) urls.push(obj.preview);
if (obj.thumb) urls.push(obj.thumb);
if (obj.files && obj.files.preview && obj.files.preview.url) {
urls.push(obj.files.preview.url);
}
}
return { urls, isVideo };
}
function processObject(item) {
if (!item || typeof item !== 'object') return;
// Gestione errori API di OnlyFans
if (item.error && item.error.message) {
const statusLabel = document.getElementById('of-md-status');
if (statusLabel) {
statusLabel.textContent = "Blocco OF: " + item.error.message;
statusLabel.style.color = "#ffc107";
setTimeout(() => { statusLabel.style.color = "white"; }, 5000);
}
return;
}
if (item.media && Array.isArray(item.media)) {
item.media.forEach(m => processObject(m));
return;
}
if (item.list && Array.isArray(item.list)) {
item.list.forEach(m => processObject(m));
return;
}
const { urls, isVideo } = findMediaUrlsInObject(item);
if (urls.length > 0) {
// Prendiamo sempre il primo della lista (che grazie all'ordinamento è quello a qualità massima)
const bestUrl = urls[0];
const mediaId = item.id || bestUrl.split('/').pop().split('?')[0];
if (bestUrl.includes('/thumbs/avatar') || bestUrl.includes('/badges/')) return;
const isLocked = (item.canView === false || (item.source && item.source.source === null));
if (!collectedMedia.has(mediaId)) {
collectedMedia.set(mediaId, {
id: mediaId,
url: bestUrl,
type: isVideo ? 'video' : (item.type || 'photo'),
postId: item.postId || mediaId,
date: item.postedAt || item.createdAt || new Date().toISOString(),
isLocked: isLocked
});
scheduleUIUpdate();
}
}
}
const origOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
this.addEventListener('load', function() {
if (typeof url === 'string' && (url.includes('/posts') || url.includes('/medias') || url.includes('/users/'))) {
try {
const data = JSON.parse(this.responseText);
if (Array.isArray(data)) {
data.forEach(item => processObject(item));
} else {
processObject(data);
}
} catch (e) {}
}
});
origOpen.apply(this, arguments);
};
const origFetch = window.fetch;
window.fetch = async function() {
const response = await origFetch.apply(this, arguments);
const clone = response.clone();
const url = arguments[0];
if (typeof url === 'string' && (url.includes('/posts') || url.includes('/medias') || url.includes('/users/'))) {
clone.json().then(data => {
if (Array.isArray(data)) {
data.forEach(item => processObject(item));
} else {
processObject(data);
}
}).catch(e => {});
}
return response;
};
// ==========================================
// 2. USER INTERFACE (Pannello Fluttuante)
// ==========================================
let panel, countLabel, lockedLabel, statusLabel, startScrollBtn, downloadBtn, clearBtn;
function createUI() {
GM_addStyle(`
#of-mass-downloader { position: fixed; bottom: 20px; right: 20px; width: 280px; background: rgba(15, 20, 25, 0.95); color: white; padding: 15px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.5); z-index: 999999; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; border: 1px solid #333; backdrop-filter: blur(5px); }
#of-mass-downloader h3 { margin: 0 0 10px 0; font-size: 16px; color: #00aff0; text-align: center; font-weight: 700; }
#of-mass-downloader .stat-row { display: flex; justify-content: space-between; margin-bottom: 5px; font-size: 13px; }
#of-mass-downloader button { width: 100%; padding: 10px; margin-top: 8px; border: none; border-radius: 6px; font-weight: bold; cursor: pointer; background: #333; color: white; transition: background 0.2s; font-size: 13px; }
#of-mass-downloader button:hover { background: #444; }
#of-mass-downloader button.primary { background: #00aff0; color: white; }
#of-mass-downloader button.primary:hover { background: #009ce0; }
#of-mass-downloader button.success { background: #28a745; color: white; }
#of-mass-downloader button.success:hover { background: #218838; }
#of-mass-downloader button.danger { background: #dc3545; color: white; }
#of-mass-downloader button.danger:hover { background: #c82333; }
.locked-text { color: #ffc107; font-weight: bold; }
.unlocked-text { color: #28a745; font-weight: bold; }
`);
panel = document.createElement('div');
panel.id = 'of-mass-downloader';
panel.innerHTML = `
<h3>OF Mass Downloader</h3>
<div class="stat-row"><span>Media Trovati:</span><span id="of-md-count" class="unlocked-text">0</span></div>
<div class="stat-row" title="Questi sono PPV o bloccati dall'abbonamento.">
<span>Di cui Bloccati:</span><span id="of-md-locked" class="locked-text">0</span>
</div>
<hr style="border-color:#333; margin: 10px 0;">
<div class="stat-row"><span>Stato:</span><span id="of-md-status">In attesa</span></div>
<button id="of-md-scroll" class="primary">Inizia Auto-Scroll</button>
<button id="of-md-download" class="success">Scarica Tutto</button>
<button id="of-md-clear" class="danger">Pulisci Memoria</button>
`;
document.body.appendChild(panel);
countLabel = document.getElementById('of-md-count');
lockedLabel = document.getElementById('of-md-locked');
statusLabel = document.getElementById('of-md-status');
startScrollBtn = document.getElementById('of-md-scroll');
downloadBtn = document.getElementById('of-md-download');
clearBtn = document.getElementById('of-md-clear');
startScrollBtn.addEventListener('click', toggleAutoScroll);
downloadBtn.addEventListener('click', startMassDownload);
clearBtn.addEventListener('click', () => { collectedMedia.clear(); updateUI(); statusLabel.textContent = "Memoria pulita"; });
}
function scheduleUIUpdate() {
if (!uiUpdateTimeout) {
uiUpdateTimeout = setTimeout(() => { updateUI(); uiUpdateTimeout = null; }, 500);
}
}
function updateUI() {
if (countLabel && lockedLabel) {
let unlocked = 0;
let locked = 0;
collectedMedia.forEach(m => {
if (m.isLocked) locked++;
else unlocked++;
});
countLabel.textContent = (unlocked + locked);
lockedLabel.textContent = locked;
}
}
// ==========================================
// 3. AUTO-SCROLLER
// ==========================================
let isScrolling = false;
let scrollInterval = null;
let noProgressCount = 0;
function toggleAutoScroll() {
if (isScrolling) stopAutoScroll();
else startAutoScroll();
}
function forceScroll() {
window.scrollBy(0, 3000);
const scrollers = document.querySelectorAll('.vue-recycle-scroller, .b-profile__content, main');
scrollers.forEach(s => {
if (s.scrollHeight > s.clientHeight) {
s.scrollTop += 3000;
}
});
document.dispatchEvent(new KeyboardEvent('keydown', {'key': 'End', 'code': 'End', 'bubbles': true}));
}
function startAutoScroll() {
isScrolling = true;
startScrollBtn.textContent = "Ferma Auto-Scroll";
startScrollBtn.className = "danger";
statusLabel.textContent = "Scrolling in corso...";
noProgressCount = 0;
let lastCount = collectedMedia.size;
scrollInterval = setInterval(() => {
forceScroll();
setTimeout(() => {
if (collectedMedia.size > lastCount) {
noProgressCount = 0;
lastCount = collectedMedia.size;
} else {
noProgressCount++;
}
if (noProgressCount >= 10) {
stopAutoScroll();
statusLabel.textContent = "Fine pagina raggiunta!";
}
}, 800);
}, 3000);
}
function stopAutoScroll() {
isScrolling = false;
clearInterval(scrollInterval);
if (startScrollBtn) {
startScrollBtn.textContent = "Inizia Auto-Scroll";
startScrollBtn.className = "primary";
}
if (statusLabel && statusLabel.textContent === "Scrolling in corso...") statusLabel.textContent = "Scrolling fermato";
}
// ==========================================
// 4. DOWNLOAD LOGIC
// ==========================================
let isDownloading = false;
function generateFilename(mediaData, username) {
let dateStr = "";
try {
const dateObj = new Date(mediaData.date);
if (!isNaN(dateObj.getTime())) {
const y = dateObj.getFullYear();
const m = String(dateObj.getMonth() + 1).padStart(2, '0');
const d = String(dateObj.getDate()).padStart(2, '0');
dateStr = `${y}-${m}-${d}`;
}
} catch (e) {}
const ext = mediaData.url.split('?')[0].split('.').pop() || (mediaData.type === 'video' ? 'mp4' : 'jpg');
const safeExt = ext.length > 5 ? (mediaData.type === 'video' ? 'mp4' : 'jpg') : ext;
const prefix = mediaData.isLocked ? "BLOCCATO_" : "";
return `${dateStr}_${username}_${prefix}${mediaData.id}.${safeExt}`;
}
async function startMassDownload() {
if (isDownloading) return;
if (collectedMedia.size === 0) {
alert("Nessun media trovato! Esegui l'Auto-Scroll per caricare i post prima di scaricare.");
return;
}
const confirmDownload = confirm(`Stai per scaricare ${collectedMedia.size} file. Continuare?`);
if (!confirmDownload) return;
isDownloading = true;
downloadBtn.textContent = "Download in corso...";
downloadBtn.className = "danger";
const username = extractUsernameFromUrl();
const mediaArray = Array.from(collectedMedia.values());
let downloaded = 0;
for (const media of mediaArray) {
const filename = generateFilename(media, username);
statusLabel.textContent = `Download: ${downloaded + 1}/${mediaArray.length}`;
await new Promise((resolve) => {
GM_download({
url: media.url,
name: filename,
onload: () => { downloaded++; resolve(); },
onerror: (err) => { console.error("Errore:", media.url, err); downloaded++; resolve(); }
});
setTimeout(resolve, 8000);
});
await new Promise(r => setTimeout(r, 400));
}
isDownloading = false;
downloadBtn.textContent = "Scarica Tutto";
downloadBtn.className = "success";
statusLabel.textContent = "Completato!";
alert(`Finito! Sono stati scaricati ${downloaded} media.`);
}
window.addEventListener('load', () => setTimeout(createUI, 2000));
})();