OnlyFans User-Lists Link Collector

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);
})();