☰

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와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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