StripChat Special Models Tracker

Track and manage special models on StripChat without AI filtering.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

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

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         StripChat Special Models Tracker
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Track and manage special models on StripChat without AI filtering.
// @description  For a browser extension advanced version of this script with
// @description  thumbnail based AI filtering on models showing tits, armpits, butt, vagina, asshole (anus),
// @description  and more features, contact through email: [email protected]
// @author       You
// @match        https://stripchat.com/*
// @match        https://www.stripchat.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    console.log('StripChat Special Models Tracker v1.0');

    // Intercept History API to detect SPA navigation
    (function (history) {
        var pushState = history.pushState;
        history.pushState = function () {
            pushState.apply(history, arguments);
            window.dispatchEvent(new Event('pushstate'));
        };
        var replaceState = history.replaceState;
        history.replaceState = function () {
            replaceState.apply(history, arguments);
            window.dispatchEvent(new Event('replacestate'));
        };
    })(window.history);

    // Listen for custom events and popstate to update button state
    window.addEventListener('pushstate', updateSpecialButtonState);
    window.addEventListener('replacestate', updateSpecialButtonState);
    window.addEventListener('popstate', updateSpecialButtonState);

    // Global variables
    let specialModels = new Map();
    let isFetching = false;
    let showSpecialModels = false;
    let filteredModelsByCategory = { 'SPECIAL': [], 'COUPLE': [], 'OTHER': [] };
    let collapsedCategories = new Set();

    // Logging
    function log(msg, type = 'info') {
        const colors = { info: '#4CAF50', warning: '#ff9800', error: '#f44336', debug: '#2196F3' };
        console.log(`%c[SpecialTracker] [${type.toUpperCase()}] ${msg}`, `color: ${colors[type] || '#ccc'}`);
    }

    function loadSpecialModels() {
        const defaultSpecialModels = [
            ["/mikakinamo", { "link": "/mikakinamo", "banned": false }],
            ["/ellieerose", { "link": "/ellieerose", "banned": false }],
            ["/vanessa_walters", { "link": "/vanessa_walters", "banned": false }],
            ["/amethystfoxx", { "link": "/amethystfoxx", "banned": true }],
            ["/whitefoxxie113", { "link": "/whitefoxxie113", "banned": false }]
        ];

        try {
            const saved = localStorage.getItem('specialModels');
            if (saved) {
                const parsedData = JSON.parse(saved);
                if (Array.isArray(parsedData)) {
                    specialModels = new Map(parsedData.map(([key, value]) => [key, { link: value.link || key, banned: value.banned || false }]));
                } else {
                    specialModels = new Map(defaultSpecialModels);
                }
            } else {
                specialModels = new Map(defaultSpecialModels);
                saveSpecialModels();
            }
        } catch (err) {
            log(`Error loading special models: ${err.message}`, 'error');
            specialModels = new Map(defaultSpecialModels);
        }
    }

    function saveSpecialModels() {
        try {
            const dataToSave = Array.from(specialModels.entries());
            localStorage.setItem('specialModels', JSON.stringify(dataToSave));
        } catch (err) {
            log(`Failed to save special models: ${err.message}`, 'error');
        }
    }

    function toggleSpecialModel() {
        const modelLink = window.location.pathname.replace(/\/+$/, '').toLowerCase();
        if (specialModels.has(modelLink)) {
            specialModels.delete(modelLink);
            log(`Removed ${modelLink} from special list`, 'info');
        } else {
            specialModels.set(modelLink, { link: modelLink, banned: false });
            log(`Added ${modelLink} to special list`, 'info');
        }
        saveSpecialModels();
        updateSpecialButtonState();
    }

    function updateSpecialButtonState() {
        const btn = document.getElementById('toggle-special-btn');
        if (!btn) return;
        const modelLink = window.location.pathname.replace(/\/+$/, '').toLowerCase();
        const model = specialModels.get(modelLink);

        if (model) {
            btn.textContent = `Remove Special (${model.banned ? 'Banned' : 'OK'})`;
            btn.style.background = model.banned ? '#f44336' : '#ffd700';
            btn.style.color = model.banned ? '#fff' : '#000';
        } else {
            btn.textContent = 'Add to Special';
            btn.style.background = '#9c27b0';
            btn.style.color = '#fff';
        }
    }

    function createUI() {
        const targetDiv = document.querySelector('[data-testid="viewcam-model-sections"]');
        if (!targetDiv || document.getElementById('special-models-section')) return;

        const section = document.createElement('div');
        section.id = 'special-models-section';
        section.style.cssText = `margin: 20px 0; padding: 15px; background: rgb(80, 30, 30); border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.2);`;

        const viewCamPageMain = document.querySelector('.view-cam-page-main');
        if (viewCamPageMain) {
            viewCamPageMain.parentNode.insertBefore(section, viewCamPageMain.nextSibling);
        } else {
            targetDiv.parentNode.insertBefore(section, targetDiv);
        }

        section.innerHTML = `
            <button id="special-models-toggle" style="background: rgba(255,255,255,0.0); border: 2px solid rgba(255,255,255,0.3); color: white; padding: 10px 24px; border-radius: 8px; font-size: 16px; font-weight: bold; cursor: pointer; transition: all 0.3s ease; backdrop-filter: blur(10px); display: flex; align-items: center; gap: 10px; width: 100%; justify-content: center;">
                <span style="font-size: 18px;">⭐</span>
                <span id="toggle-text">Show Favorites Library</span>
                <span id="special-count-badge" style="background: rgba(255,255,255,0.3); padding: 4px 12px; border-radius: 20px; font-size: 14px; margin-left: auto;">0</span>
            </button>
            <div id="special-models-container" style="display: none; margin-top: 0px; padding-top: 15px;">
                <div style="display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 15px;">
                    <button id="refresh-specials-btn" style="background: #2196F3; border: none; color: white; padding: 8px 16px; border-radius: 6px; font-size: 12px; cursor: pointer;">Refresh Now</button>
                    <button id="edit-specials-btn" style="background: #ff9800; border: none; color: white; padding: 8px 16px; border-radius: 6px; font-size: 12px; cursor: pointer;">Edit Specials</button>
                    <button id="toggle-special-btn" style="background: #9c27b0; border: none; color: white; padding: 8px 16px; border-radius: 6px; font-size: 12px; cursor: pointer;">Add to Special</button>
                </div>
                <div id="special-models-grid" style="display: flex; flex-direction: column; gap: 20px; max-height: 600px; overflow-y: auto; padding-right: 10px;"></div>
            </div>
        `;

        document.getElementById('special-models-toggle').addEventListener('click', () => {
            showSpecialModels = !showSpecialModels;
            document.getElementById('special-models-container').style.display = showSpecialModels ? 'block' : 'none';
            if (showSpecialModels) fetchSpecials();
        });

        document.getElementById('refresh-specials-btn').addEventListener('click', fetchSpecials);
        document.getElementById('edit-specials-btn').addEventListener('click', showEditSpecialsUI);
        document.getElementById('toggle-special-btn').addEventListener('click', toggleSpecialModel);

        ensureStyles();
        updateSpecialButtonState();
    }

    function ensureStyles() {
        if (document.getElementById('special-tracker-styles')) return;
        const style = document.createElement('style');
        style.id = 'special-tracker-styles';
        style.textContent = `
            .category-section {
                display: flex; flex-direction: column; gap: 10px;
            }
            .category-header {
                color: #fff; font-size: 14px; font-weight: bold; padding: 10px 15px;
                background: rgba(255,255,255,0.06); border-radius: 8px; border-left: 4px solid #ffd700;
                cursor: pointer; display: flex; align-items: center; gap: 10px;
                position: sticky; top: 0; z-index: 10; backdrop-filter: blur(10px);
                user-select: none; transition: background 0.2s ease;
            }
            .category-header:hover { background: rgba(255,255,255,0.1); }
            .models-subgrid {
                display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px;
            }
            .special-model-card {
                position: relative; overflow: hidden; text-decoration: none; color: inherit;
                transition: transform 0.2s ease, box-shadow 0.2s ease;
            }
            .special-model-card:hover { transform: translateY(-4px); box-shadow: 0 8px 25px rgba(0,0,0,0.3); }
        `;
        document.head.appendChild(style);
    }

    async function checkBroadcastsStatus(username) {
        try {
            const url = `https://stripchat.com/api/front/v1/broadcasts/${username}`;
            const resp = await fetch(url, { headers: { 'Accept': 'application/json', 'Referer': 'https://stripchat.com/' } });
            if (resp.status === 200) {
                const data = await resp.json();
                if (data.item && data.item.isLive) return data.item;
            }
        } catch (e) { }
        return null;
    }

    async function fetchSpecials() {
        if (isFetching) return;
        isFetching = true;
        log('Fetching favorites status...', 'info');
        filteredModelsByCategory = { 'SPECIAL': [], 'COUPLE': [], 'OTHER': [] };

        try {
            const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
            const toff = new Date().getTimezoneOffset();
            const common = { 'Accept': '*/*', 'Content-Type': 'application/json', 'Referer': 'https://stripchat.com/favorites' };

            const dynUrl = `https://stripchat.com/api/front/v3/config/dynamic?uniq=${Math.random().toString(36).slice(2)}`;
            const dyn = await fetch(dynUrl, { credentials: 'include', headers: common });
            const dynData = await dyn.json();
            const token = dynData?.dynamic?.jwtToken || '';

            const favUrl = `https://stripchat.com/api/front/models/favorites?offset=0&sortBy=bestMatch&limit=100&uniq=${Math.random().toString(36).slice(2)}`;
            const headers = { ...common, Authorization: token };
            const fav = await fetch(favUrl, { credentials: 'include', headers });
            const favData = await fav.json();

            (favData.models || []).forEach(m => {
                const link = `/${m.username}`.toLowerCase();
                const special = specialModels.get(link);
                let thumb = m.previewUrlThumbSmall || m.previewUrl;
                if (m.snapshotTimestamp && m.id) thumb = `https://img.doppiocdn.com/thumbs/${m.snapshotTimestamp}/${m.id}`;

                const modelData = {
                    thumbnail: thumb,
                    modelName: m.username,
                    modelLink: link,
                    isOnline: (m.isLive || m.isOnline),
                    status: m.status || 'public',
                    isBanned: special ? special.banned : false
                };

                if (special) {
                    filteredModelsByCategory['SPECIAL'].push(modelData);
                } else if (m.gender === 'maleFemale') {
                    filteredModelsByCategory['COUPLE'].push(modelData);
                } else {
                    filteredModelsByCategory['OTHER'].push(modelData);
                }
            });

            // 2. Check banned specials not in favorites
            const bannedPromises = [];
            specialModels.forEach((model, link) => {
                if (model.banned && !filteredModelsByCategory['SPECIAL'].some(m => m.modelLink === link)) {
                    bannedPromises.push((async () => {
                        const live = await checkBroadcastsStatus(link.replace(/^\//, ''));
                        if (live) {
                            filteredModelsByCategory['SPECIAL'].push({
                                thumbnail: `https://img.doppiocdn.com/thumbs/${live.snapshotTimestamp}/${live.modelId}`,
                                modelName: live.username,
                                modelLink: link,
                                isOnline: true,
                                status: live.status || 'public',
                                isBanned: true
                            });
                        }
                    })());
                }
            });
            await Promise.all(bannedPromises);

            renderGrid();
        } catch (e) {
            log(`Error fetching favorites: ${e.message}`, 'error');
        } finally {
            isFetching = false;
        }
    }

    function renderGrid() {
        const grid = document.getElementById('special-models-grid');
        const badge = document.getElementById('special-count-badge');
        if (!grid) return;

        const total = Object.values(filteredModelsByCategory).reduce((acc, cat) => acc + cat.length, 0);
        badge.textContent = total;

        if (total === 0) {
            grid.innerHTML = '<div style="color:white;text-align:center;padding:20px;">No live favorites found.</div>';
            return;
        }

        const sections = [
            { id: 'SPECIAL', name: 'Special Models', color: '#ffd700' },
            { id: 'COUPLE', name: 'Couple Content', color: '#ff69b4' },
            { id: 'OTHER', name: 'Other Live Favorites', color: '#4CAF50' }
        ];

        grid.innerHTML = '';
        sections.forEach(sec => {
            const models = filteredModelsByCategory[sec.id];
            if (models.length === 0) return;

            const isCollapsed = collapsedCategories.has(sec.id);
            const container = document.createElement('div');
            container.className = 'category-section';

            const header = document.createElement('div');
            header.className = 'category-header';
            header.style.borderLeftColor = sec.color;
            header.innerHTML = `
                <span style="transition: transform 0.3s ease; transform: rotate(${isCollapsed ? '-90' : '0'}deg); color: ${sec.color}">▼</span>
                <span style="flex: 1;">${sec.name} (${models.length})</span>
                <span style="font-size: 11px; opacity: 0.6;">${isCollapsed ? 'click to expand' : 'click to collapse'}</span>
            `;

            header.onclick = () => {
                if (collapsedCategories.has(sec.id)) collapsedCategories.delete(sec.id);
                else collapsedCategories.add(sec.id);
                renderGrid();
            };

            container.appendChild(header);

            if (!isCollapsed) {
                const subgrid = document.createElement('div');
                subgrid.className = 'models-subgrid';
                subgrid.innerHTML = models.map(m => `
                    <a href="${m.modelLink}" class="special-model-card" style="${m.isBanned ? 'background: rgba(244, 67, 54, 0.15);' : ''}">
                        <div style="position: relative;">
                            <img src="${m.thumbnail}" style="width: 100%; height: 120px; object-fit: cover;">
                            ${m.status !== 'public' ? `<div style="position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;color:white;font-size:12px;font-weight:bold;">${m.status.toUpperCase()}</div>` : ''}
                            <div style="position:absolute;bottom:0;width:100%;background:rgba(0,0,0,0.5);color:white;text-align:center;font-size:11px;padding:2px 0;">${m.modelName}</div>
                            ${m.isOnline ? `<div style="position:absolute;top:6px;right:6px;background:#4CAF50;color:white;padding:2px 6px;border-radius:10px;font-size:10px;font-weight:bold;">LIVE</div>` : ''}
                            ${m.isBanned ? `<div style="position:absolute;top:0;left:0;width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:white;font-size:20px;font-weight:bold;">BANNED</div>` : ''}
                        </div>
                    </a>
                `).join('');

                subgrid.querySelectorAll('a').forEach(a => {
                    a.onclick = (e) => {
                        if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
                        e.preventDefault();
                        const link = a.getAttribute('href');
                        if (window.location.pathname !== link) {
                            history.pushState({}, '', link);
                            window.dispatchEvent(new Event('popstate'));
                        }
                    };
                });
                container.appendChild(subgrid);
            }
            grid.appendChild(container);
        });
    }

    function showEditSpecialsUI() {
        const existing = document.getElementById('specials-modal'); if (existing) existing.remove();
        const modal = document.createElement('div'); modal.id = 'specials-modal';
        modal.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);z-index:10000;display:flex;align-items:center;justify-content:center;';

        const listHTML = Array.from(specialModels.entries()).map(([link, m]) => `
            <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px; padding:10px; background:#f0f0f0; border-radius:4px; color: black;">
                <span style="flex:1;">${link}</span>
                <label style="font-size:12px; margin-right:15px;"><input type="checkbox" class="ban-toggle" data-link="${link}" ${m.banned ? 'checked' : ''}> Banned</label>
                <button class="del-special" data-link="${link}" style="background:#f44336; color:white; border:none; padding:5px 10px; border-radius:4px; cursor:pointer;">Delete</button>
            </div>
        `).join('');

        modal.innerHTML = `
            <div style="background:white; padding:20px; border-radius:8px; width:80%; max-width:600px; max-height:80%; overflow:auto; color:black;">
                <h3 style="margin-top:0;">Edit Special Models</h3>
                <div id="specials-list-container">${listHTML}</div>
                <div style="margin-top:20px; display:flex; gap:10px;">
                    <input id="new-special-link" placeholder="/username" style="flex:1; padding:8px;">
                    <button id="add-special-btn" style="background:#4CAF50; color:white; border:none; padding:8px 16px; border-radius:4px;">Add</button>
                    <button id="close-specials" style="background:#2196F3; color:white; border:none; padding:8px 16px; border-radius:4px;">Close</button>
                </div>
            </div>`;

        document.body.appendChild(modal);

        modal.querySelectorAll('.ban-toggle').forEach(cb => cb.onchange = () => {
            const m = specialModels.get(cb.dataset.link);
            if (m) { m.banned = cb.checked; saveSpecialModels(); updateSpecialButtonState(); }
        });
        modal.querySelectorAll('.del-special').forEach(btn => btn.onclick = () => {
            specialModels.delete(btn.dataset.link);
            saveSpecialModels(); updateSpecialButtonState(); showEditSpecialsUI();
        });
        modal.querySelector('#add-special-btn').onclick = () => {
            let link = modal.querySelector('#new-special-link').value.trim();
            if (!link) return;
            if (!link.startsWith('/')) link = '/' + link;
            specialModels.set(link.toLowerCase(), { link: link.toLowerCase(), banned: false });
            saveSpecialModels(); updateSpecialButtonState(); showEditSpecialsUI();
        };
        modal.querySelector('#close-specials').onclick = () => modal.remove();
        modal.onclick = (e) => { if (e.target === modal) modal.remove(); };
    }

    function init() {
        loadSpecialModels();

        const observer = new MutationObserver(() => {
            if (!document.getElementById('special-models-section')) {
                if (document.querySelector('[data-testid="viewcam-model-sections"]') || document.querySelector('.view-cam-page-main')) {
                    createUI();
                }
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });

        // Initial try
        createUI();
    }

    init();

})();