☰

OnlyFans Batch Downloader

Scarica automaticamente tutti i media (foto e video) di un utente OnlyFans tramite auto-scroll e API scraping. Qualità massima garantita.

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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));
})();