OnlyFans User-Lists Link Collector

Auto-scrolls and collects OnlyFans profile links. Features smart auto-stop, dynamic filename auto-download, and navigation auto-clear.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         OnlyFans User-Lists Link Collector
// @namespace    https://onlyfans.com/
// @version      1.1.9
// @description  Auto-scrolls and collects OnlyFans profile links. Features smart auto-stop, dynamic filename auto-download, and navigation auto-clear.
// @author       Gemini 3 Pro (previously ChatGPT 5.2 Thinking)
// @icon         https://static2.onlyfans.com/static/prod/f/202512181451-75a62e2193/icons/favicon-32x32.png
// @match        https://onlyfans.com/my/collections/user-lists*
// @grant        GM_addStyle
// @grant        GM_setClipboard
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';
    var TAB_W = 18;
    var TAB_H = 44;
    var TAB_TEXT_PAD = TAB_W + 9; // 18 + 9
    var PANEL_W = 340;
    var USERS_GRID_SELECTORS = [
        'div.b-grid-users',
        'div.b-grid-users-list',
        'div.b-grid'
    ];
    var SCROLL_STEP_PX = 900;
    var SCROLL_INTERVAL_MS = 900;
    var NO_PROGRESS_LIMIT = 4;
    var BOTTOM_NOLOADER_LIMIT = 3;
    var LOADER_VISIBLE_MIN_TOP_RATIO = 0.55;
    var PANEL_TOP_OFFSET_Y = -6;
    var TITLE_MARGIN_BOTTOM = 6;
    var ACTIONS_MARGIN_BOTTOM = 8;

    var collected = new Set();
    var scrolling = false;
    var scrollTimer = null;
    var scrollEl = null;
    var hideTimer = null;
    var isShown = false;
    var pinnedUntil = 0;
    var noProgressTicks = 0;
    var bottomNoLoaderTicks = 0;

    // NEW: Track current path to detect navigation
    var lastPath = window.location.pathname;

    function nowMs() { return Date.now(); }
    function pinBriefly(ms) {
        pinnedUntil = nowMs() + (ms || 1500);
        showPanel(true);
    }
    function shouldBlockHide() {
        return nowMs() <= pinnedUntil;
    }
    function normalizeUrl(u) {
        if (!u) return '';
        return u.split('?')[0].split('#')[0].replace(/\/+$/, '');
    }
    function toAbsUrl(href) {
        try { return new URL(href, window.location.origin).href; }
        catch (e) { return ''; }
    }
    function isValidProfileLink(url) {
        if (!url || !url.startsWith('https://onlyfans.com/')) return false;
        if (
            url === 'https://onlyfans.com' ||
            url === 'https://onlyfans.com/' ||
            url.startsWith('https://onlyfans.com/help') ||
            url.startsWith('https://onlyfans.com/my/') ||
            url.startsWith('https://onlyfans.com/posts/')
        ) {
            return false;
        }
        var path = url.replace('https://onlyfans.com/', '');
        if (!path) return false;
        if (path.indexOf('/') !== -1) return false;
        return true;
    }
    function getGrid() {
        for (var i = 0; i < USERS_GRID_SELECTORS.length; i++) {
            var el = document.querySelector(USERS_GRID_SELECTORS[i]);
            if (el) return el;
        }
        return null;
    }
    function findScrollableForGrid(gridEl) {
        var el = gridEl;
        while (el && el !== document.body) {
            var cs = window.getComputedStyle(el);
            var oy = cs.overflowY;
            var canScroll = (oy === 'auto' || oy === 'scroll') && (el.scrollHeight > el.clientHeight + 5);
            if (canScroll) return el;
            el = el.parentElement;
        }
        return document.scrollingElement || document.documentElement || document.body;
    }
    function getGridLinks(gridEl) {
        var out = [];
        if (!gridEl) return out;
        var anchors = gridEl.querySelectorAll('a[href]');
        for (var i = 0; i < anchors.length; i++) {
            var raw = anchors[i].getAttribute('href') || '';
            if (!raw) continue;
            if (raw.indexOf('javascript:') === 0) continue;
            var abs = normalizeUrl(toAbsUrl(raw));
            if (abs) out.push(abs);
        }
        return out;
    }
    function updateOutput() {
        var list = Array.from(collected).sort();
        output.value = list.join('\n');
        counter.textContent = list.length + ' links';
        if (!scrolling && status.textContent === 'Scrolling...') status.textContent = 'Idle';
    }
    function collectLinks() {
        var grid = getGrid();
        if (!grid) {
            status.textContent = 'No grid';
            return;
        }
        var links = getGridLinks(grid);
        for (var i = 0; i < links.length; i++) {
            if (isValidProfileLink(links[i])) collected.add(links[i]);
        }
        updateOutput();
    }
    function isVisible(el) {
        if (!el) return false;
        var r = el.getBoundingClientRect();
        if (r.width <= 0 || r.height <= 0) return false;
        if (r.bottom <= 0) return false;
        if (r.top >= window.innerHeight) return false;
        return true;
    }
    function hasVisibleLoader(gridEl, scrollElLocal) {
        var root = gridEl ? (gridEl.parentElement || gridEl) : (scrollElLocal || document.body);
        if (!root) return false;
        var selectors = [
            'svg.g-icon',
            '.g-icon',
            '.loader',
            '.loading',
            '[class*="spinner"]',
            '[class*="Loader"]',
            '[class*="loader"]'
        ];
        for (var s = 0; s < selectors.length; s++) {
            var nodes = root.querySelectorAll(selectors[s]);
            for (var i = 0; i < nodes.length; i++) {
                var el = nodes[i];
                if (!isVisible(el)) continue;
                var r = el.getBoundingClientRect();
                if (r.top < window.innerHeight * LOADER_VISIBLE_MIN_TOP_RATIO) continue;
                if (el.closest && el.closest('#of-link-panel')) continue;
                if (el.closest && el.closest('#of-hover-tab')) continue;
                return true;
            }
        }
        return false;
    }
    function atBottom(scrollElLocal) {
        if (!scrollElLocal) return false;
        return (scrollElLocal.scrollTop + scrollElLocal.clientHeight >= scrollElLocal.scrollHeight - 4);
    }
    function evaluateAutoStop(prevCount, prevScrollTop) {
        var grid = getGrid();
        var newCount = collected.size;
        var newScrollTop = scrollEl ? scrollEl.scrollTop : prevScrollTop;
        var progressed = (newCount > prevCount) || (newScrollTop !== prevScrollTop);
        if (!progressed) noProgressTicks += 1;
        else noProgressTicks = 0;
        var bottom = atBottom(scrollEl);
        var loader = hasVisibleLoader(grid, scrollEl);
        if (bottom && !loader && newCount === prevCount) bottomNoLoaderTicks += 1;
        else bottomNoLoaderTicks = 0;
        if (noProgressTicks >= 4 || bottomNoLoaderTicks >= 3) {
            stopScroll('Auto-stopped (end)', true);
        }
    }
    function startScroll() {
        if (scrolling) return;
        var grid = getGrid();
        if (!grid) {
            status.textContent = 'No grid';
            return;
        }
        scrollEl = findScrollableForGrid(grid);
        scrolling = true;
        status.textContent = 'Scrolling...';
        noProgressTicks = 0;
        bottomNoLoaderTicks = 0;
        collectLinks();
        scrollTimer = setInterval(function () {
            var prevCount = collected.size;
            var prevScrollTop = scrollEl ? scrollEl.scrollTop : 0;
            try {
                if (scrollEl) scrollEl.scrollTop = scrollEl.scrollTop + SCROLL_STEP_PX;
                else window.scrollBy(0, SCROLL_STEP_PX);
            } catch (e) {
                window.scrollBy(0, SCROLL_STEP_PX);
            }
            setTimeout(function () {
                collectLinks();
                evaluateAutoStop(prevCount, prevScrollTop);
            }, 180);
        }, SCROLL_INTERVAL_MS);
    }
    function stopScroll(reason, autoDownload) {
        scrolling = false;
        if (scrollTimer) clearInterval(scrollTimer);
        scrollTimer = null;
        status.textContent = reason || 'Stopped';

        if (autoDownload) {
            downloadTxt();
        }
    }
    function clearAll() {
        collected.clear();
        updateOutput();
        status.textContent = 'Cleared';
        setTimeout(function () {
            status.textContent = scrolling ? 'Scrolling...' : 'Idle';
        }, 900);
    }
    function copyAll() {
        GM_setClipboard(output.value || '');
        status.textContent = 'Copied';
        setTimeout(function () {
            status.textContent = scrolling ? 'Scrolling...' : 'Idle';
        }, 900);
    }

    function getCleanTitle() {
        var t = document.title || 'user-list';
        return t.replace(/\s*[—–-]\s*OnlyFans$/i, '').trim();
    }

    function getFormattedDate() {
        var d = new Date();
        var year = d.getFullYear();
        var month = String(d.getMonth() + 1).padStart(2, '0');
        var day = String(d.getDate()).padStart(2, '0');
        return year + '-' + month + '-' + day;
    }

    function downloadTxt() {
        var blob = new Blob([output.value || ''], { type: 'text/plain' });
        var a = document.createElement('a');
        a.href = URL.createObjectURL(blob);
        a.download = 'onlyfans-list_' + getCleanTitle() + '_' + getFormattedDate() + '.txt';
        a.click();
        URL.revokeObjectURL(a.href);
        status.textContent = 'Downloaded';
        setTimeout(function () {
            status.textContent = scrolling ? 'Scrolling...' : 'Idle';
        }, 900);
    }

    function setPanelTopToTab() {
        var tabRect = tab.getBoundingClientRect();
        var headerHeight = headerWrap.getBoundingClientRect().height;
        var top = Math.round(tabRect.top - headerHeight + PANEL_TOP_OFFSET_Y);
        panel.style.top = top + 'px';
    }
    function setArrow(open) {
        arrowPath.setAttribute('d', open ? 'M8 2 L4 6 L8 10 Z' : 'M4 2 L8 6 L4 10 Z');
    }
    function showPanel(force) {
        if (hideTimer) {
            clearTimeout(hideTimer);
            hideTimer = null;
        }
        setPanelTopToTab();
        if (isShown && !force) return;
        isShown = true;
        panel.classList.add('tm-panel-show');
        setArrow(true);
    }
    function scheduleHide() {
        if (shouldBlockHide()) return;
        if (hideTimer) clearTimeout(hideTimer);
        hideTimer = setTimeout(function () {
            if (shouldBlockHide()) return;
            isShown = false;
            panel.classList.remove('tm-panel-show');
            setArrow(false);
        }, 140);
    }
    GM_addStyle(`
        #of-hover-tab {
            position: fixed;
            left: 0;
            top: 50%;
            width: ${TAB_W}px;
            height: ${TAB_H}px;
            margin-top: -${Math.floor(TAB_H / 2)}px;
            z-index: 100000;
            cursor: pointer;
            opacity: 0.7;
            background-color: #bdc5c8;
            border: 1px solid #abb0b3;
            border-left: none;
            border-radius: 0 5px 5px 0;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        #of-hover-tab:hover { opacity: 1; }
        #of-hover-tab svg { display: block; }
        #of-link-panel {
            position: fixed;
            left: 0;
            top: 50%;
            width: ${PANEL_W}px;
            z-index: 99999;
            transform: translateX(-100%);
            transition: transform 140ms linear, opacity 140ms linear;
            opacity: 0.92;
            background: rgba(0,0,0,0.75);
            color: #fff;
            padding: 10px 10px 10px ${TAB_TEXT_PAD}px;
            border-radius: 0 8px 8px 0;
            font-size: 12px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.35);
        }
        #of-link-panel.tm-panel-show { transform: translateX(0); opacity: 1; }
        #of-header { margin-bottom: ${TITLE_MARGIN_BOTTOM}px; }
        #of-title { font-weight: 700; margin: 0 0 4px 0; }
        #of-row {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 8px;
            margin: 0;
        }
        #of-status { opacity: 0.9; font-size: 11px; }
        #of-actions { margin-bottom: ${ACTIONS_MARGIN_BOTTOM}px; }
        #of-actions-row1 {
            display: flex;
            flex-wrap: wrap;
            gap: 6px;
            margin-bottom: 6px;
        }
        #of-actions-row2 {
            display: flex;
            flex-wrap: wrap;
            gap: 6px;
        }
        #of-link-panel button {
            padding: 4px 10px;
            font-size: 12px;
            cursor: pointer;
            background: rgba(255,255,255,0.08);
            color: #fff;
            border: 1px solid rgba(255,255,255,0.25);
            border-radius: 999px;
            line-height: 1.2;
            user-select: none;
        }
        #of-link-panel button:hover { background: rgba(255,255,255,0.18); }
        #of-link-panel button:active { background: rgba(255,255,255,0.28); }
        #of-start { border: 1px solid #4CAF50 !important; box-shadow: 0 0 3px #4CAF50; }
        #of-stop  { border: 1px solid #F44336 !important; box-shadow: 0 0 3px #F44336; }
        #of-clear { border: 1px solid #FFEB3B !important; box-shadow: 0 0 3px #FFEB3B; color: #fff; }
        #of-link-panel textarea {
            width: 100%;
            height: 180px;
            margin-top: 6px;
            font-size: 11px;
            resize: vertical;
        }
    `);
    // Tab
    var tab = document.createElement('div');
    tab.id = 'of-hover-tab';
    tab.innerHTML =
        '<svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true">' +
        '<path id="of-arrow-path" d="M4 2 L8 6 L4 10 Z" fill="#2b2f33"></path>' +
        '</svg>';
    document.body.appendChild(tab);
    var arrowPath = document.getElementById('of-arrow-path');
    // Panel
    var panel = document.createElement('div');
    panel.id = 'of-link-panel';
    panel.innerHTML =
        '<div id="of-header">' +
            '<div id="of-title">OnlyFans Link Collector</div>' +
            '<div id="of-row">' +
                '<div id="of-counter">0 links</div>' +
                '<div id="of-status">Idle</div>' +
            '</div>' +
        '</div>' +
        '<div id="of-actions">' +
            '<div id="of-actions-row1">' +
                '<button id="of-start" type="button">Start scroll</button>' +
                '<button id="of-stop" type="button">Stop</button>' +
                '<button id="of-copy" type="button">Copy</button>' +
                '<button id="of-clear" type="button">Clear</button>' +
            '</div>' +
            '<div id="of-actions-row2">' +
                '<button id="of-txt" type="button">Download .txt</button>' +
            '</div>' +
        '</div>' +
        '<textarea id="of-output" readonly></textarea>';
    document.body.appendChild(panel);
    var headerWrap = document.getElementById('of-header');
    var output = document.getElementById('of-output');
    var counter = document.getElementById('of-counter');
    var status = document.getElementById('of-status');
    document.getElementById('of-start').addEventListener('click', function (e) {
        e.preventDefault();
        pinBriefly(2500);
        startScroll();
    });
    document.getElementById('of-stop').addEventListener('click', function (e) {
        e.preventDefault();
        pinBriefly(2500);
        stopScroll('Stopped');
    });
    document.getElementById('of-copy').addEventListener('click', function (e) {
        e.preventDefault();
        pinBriefly(2500);
        copyAll();
    });
    document.getElementById('of-txt').addEventListener('click', function (e) {
        e.preventDefault();
        pinBriefly(2500);
        downloadTxt();
    });
    document.getElementById('of-clear').addEventListener('click', function (e) {
        e.preventDefault();
        pinBriefly(2500);
        clearAll();
    });
    window.addEventListener('resize', function () {
        if (isShown) setPanelTopToTab();
    }, { passive: true });
    tab.addEventListener('mouseenter', function () { showPanel(false); });
    tab.addEventListener('mouseleave', scheduleHide);
    panel.addEventListener('mouseenter', function () { showPanel(true); });
    panel.addEventListener('mouseleave', scheduleHide);
    setArrow(false);
    // Boot: grid may appear later
    var tries = 0;
    var bootTimer = setInterval(function () {
        tries += 1;
        collectLinks();
        if (getGrid() || tries >= 30) clearInterval(bootTimer);
    }, 500);

    // NEW: Check navigation periodically to auto-clear
    setInterval(function() {
        if (window.location.pathname !== lastPath) {
            lastPath = window.location.pathname;
            clearAll();
            status.textContent = 'List changed (cleared)';
            if (scrolling) stopScroll('Navigated', false);
        }
    }, 1000);
})();