Cam ARNA

Multi-archive search tool with modern dashboard design + Import/Export

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         Cam ARNA
// @namespace    http://tampermonkey.net/
// @version      2.2.1
// @description  Multi-archive search tool with modern dashboard design + Import/Export
// @author       user006-ui
// @license      MIT
// @match        https://*.stripchat.com/*
// @match        https://*.chaturbate.com/*
// @match        https://chaturbate.com/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_openInTab
// @connect      archivebate.com
// @connect      showcamrips.com
// @connect      camshowrecordings.com
// @connect      camwh.com
// @connect      topcamvideos.com
// @connect      lovecamporn.com
// @connect      camwhores.tv
// @connect      bestcam.tv
// @connect      xhomealone.com
// @connect      stream-leak.com
// @connect      mfcamhub.com
// @connect      camshowrecord.net
// @connect      camwhoresbay.com
// @connect      camsave1.com
// @connect      onscreens.me
// @connect      livecamrips.to
// @connect      cumcams.cc
// @connect      allmy.cam
// @connect      livecamsrip.com
// @connect      stripchat.com
// @connect      chaturbate.com
// @connect      camgirlfinder.net
// @connect      nrtool.to
// @connect      camsrip.com
// @connect      camcaps.tv
// @connect      curbate.tv
// @connect      rec-tube.com
// @connect      camshaip.com
// @connect      camsclips.net
// @connect      motherless.com
// @connect      webpussi.com
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration & Utils ---
    const Config = {
        colors: {
            bg: '#0f172a',
            surface: '#1e293b',
            border: '#334155',
            primary: '#6366f1',
            accent: '#818cf8',
            text: '#f8fafc',
            textMuted: '#94a3b8',
            success: '#10b981',
            error: '#ef4444'
        }
    };

    const Utils = {
        escapeRegex: (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
        isValidUsername: (str) => /^[a-zA-Z0-9_\-]{3,50}$/.test(str),

        openSafe: (url) => {
            if (typeof GM_openInTab === 'function') {
                GM_openInTab(url, { active: true, insert: true, setParent: true });
            } else {
                window.open(url, '_blank', 'noopener,noreferrer');
            }
        },

        crypto: {
            secret: 'CAM_ARNA_SALT_v3_MODERN',
            encrypt: (text) => {
                if (!text) return '';
                try { return CryptoJS.AES.encrypt(text, Utils.crypto.secret).toString(); }
                catch (e) { return ''; }
            },
            decrypt: (ciphertext) => {
                if (!ciphertext) return '';
                try {
                    const bytes = CryptoJS.AES.decrypt(ciphertext, Utils.crypto.secret);
                    return bytes.toString(CryptoJS.enc.Utf8);
                } catch (e) { return ''; }
            }
        }
    };

    const Storage = {
        get: (key, defaultValue) => GM_getValue(key, defaultValue),
        set: (key, value) => GM_setValue(key, value)
    };

    // --- Sites Configuration ---
    const archiveSites = [
        { name: 'Archivebate', url: 'https://archivebate.com/profile/{username}', domain: 'archivebate.com' },
        { name: 'Showcamrips', url: 'https://showcamrips.com/model/en/{username}', domain: 'showcamrips.com' },
        { name: 'CamRecordings', url: 'https://www.camshowrecordings.com/model/{username}', domain: 'camshowrecordings.com' },
        { name: 'CamWH', url: 'https://camwh.com/tags/{username}/', domain: 'camwh.com' },
        { name: 'TopCam', url: 'https://www.topcamvideos.com/showall/?search={username}', domain: 'topcamvideos.com' },
        { name: 'LoveCam', url: 'https://lovecamporn.com/showall/?search={username}', domain: 'lovecamporn.com' },
        { name: 'Camwhores.tv', url: 'https://www.camwhores.tv/search/{username}/', domain: 'camwhores.tv' },
        { name: 'Bestcam', url: 'https://bestcam.tv/model/{username}', domain: 'bestcam.tv' },
        { name: 'XHome', url: 'https://xhomealone.com/tags/{username}/', domain: 'xhomealone.com' },
        { name: 'StreamLeak', url: 'https://stream-leak.com/models/{username}/', domain: 'stream-leak.com' },
        { name: 'MFCamHub', url: 'https://mfcamhub.com/models/{username}/', domain: 'mfcamhub.com' },
        { name: 'CamRecord', url: 'https://camshowrecord.net/video/list?page=1&model={username}', domain: 'camshowrecord.net' },
        { name: 'CW Bay', url: 'https://www.camwhoresbay.com/search/{username}/', domain: 'camwhoresbay.com' },
        { name: 'CamSave', url: 'https://www.camsave1.com/?search={username}&women=true', domain: 'camsave1.com' },
        { name: 'OnScreens', url: 'https://www.onscreens.me/m/{username}', domain: 'onscreens.me' },
        { name: 'LiveCamRips', url: 'https://livecamrips.to/search/{username}/1', domain: 'livecamrips.to' },
        { name: 'CumCams', url: 'https://cumcams.cc/performer/{username}', domain: 'cumcams.cc' },
        { name: 'AllMyCam', url: 'https://allmy.cam/search/{username}/', domain: 'allmy.cam' },
        { name: 'LCRip', url: 'https://www.livecamsrip.com/{username}/profile', domain: 'livecamsrip.com' },
        { name: 'CamsRip', url: 'https://camsrip.com/{username}/profile', domain: 'camsrip.com' },
        { name: 'CamCaps', url: 'https://camcaps.tv/search/videos/{username}', domain: 'camcaps.tv' },
        { name: 'Curbate', url: 'https://curbate.tv/search?q={username}', domain: 'curbate.tv' },
        { name: 'RecTube', url: 'https://rec-tube.com/search/{username}/', domain: 'rec-tube.com' },
        { name: 'CamShaip', url: 'https://camshaip.com/search/{username}/', domain: 'camshaip.com' },
        { name: 'CamsClips', url: 'https://www.camsclips.net/search/{username}/', domain: 'camsclips.net' },
        { name: 'Motherless', url: 'https://motherless.com/term/{username}', domain: 'motherless.com' },
        { name: 'WebPussi', url: 'https://www.webpussi.com/search/{username}/', domain: 'webpussi.com' }
    ];

    function getFaviconUrl(domain) {
        return `https://www.google.com/s2/favicons?domain=${domain}&sz=32`;
    }

    // --- Page Checker Logic ---
    const PageChecker = {
        checkPage: function(url) {
            return new Promise((resolve) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: url,
                    timeout: 10000,
                    onload: (res) => resolve(this.analyze(res, url)),
                    onerror: () => resolve(false),
                    ontimeout: () => resolve(false)
                });
            });
        },

        analyze: function(response, url) {
            if (response.status === 404 || response.status >= 500) return false;
            const text = response.responseText;
            if (!text) return false;
            const lowerText = text.toLowerCase();
            const titleMatch = lowerText.match(/<title[^>]*>(.*?)<\/title>/i);
            const title = titleMatch ? titleMatch[1] : '';

            if (url.includes('livecamrips.to')) {
                const noResultPatterns = ['no records found', 'no models found', 'no results', '0 models found'];
                if (noResultPatterns.some(p => lowerText.includes(p))) return false;
                if (!text.includes('class="video') && !text.includes('model-card')) return false;
            }

            if (url.includes('cumcams.cc')) {
                const notFoundPatterns = [/<h1[^>]*>\s*404\s*<\/h1>/i, /performer\s+not\s+found/i];
                if (notFoundPatterns.some(p => p.test(text))) return false;
                if (!text.includes('profile-info') && !text.includes('class="performer')) return false;
            }

            if (url.includes('allmy.cam') && !text.includes('class="video-card"')) return false;
            if (url.includes('showcamrips') && text.includes("data:image/png;base64")) return false;
            if (url.includes('camshowrecordings.com') && !text.includes('class="h1modelpage"')) return false;
            if (url.includes('camwhores.tv') && /no\s+videos?\s+found|0\s+videos/i.test(lowerText)) return false;

            if (['not found', '404', 'error'].some(term => title.includes(term))) return false;
            const genericNotFound = [/no\s+videos?\s+found/i, /no\s+results?\s+found/i, /does\s+not\s+exist/i, /0\s+results?/i];
            if (genericNotFound.some(p => p.test(lowerText))) return false;

            return true;
        }
    };

    // --- Styling ---
    function injectStyles() {
        GM_addStyle(`
            :root {
                --ca-bg: ${Config.colors.bg};
                --ca-surf: ${Config.colors.surface};
                --ca-border: ${Config.colors.border};
                --ca-prim: ${Config.colors.primary};
                --ca-acc: ${Config.colors.accent};
                --ca-text: ${Config.colors.text};
                --ca-muted: ${Config.colors.textMuted};
                --ca-success: ${Config.colors.success};
                --ca-error: ${Config.colors.error};
            }
            .ca-fab { position: fixed; bottom: 20px; right: 20px; width: 50px; height: 50px; border-radius: 50%; background: var(--ca-prim); color: white; border: none; cursor: pointer; z-index: 9999; font-size: 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
            .ca-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); display: flex; align-items: center; justify-content: center; z-index: 10000; font-family: sans-serif; }
            .ca-panel { background: var(--ca-bg); width: 450px; max-height: 80vh; border-radius: 12px; border: 1px solid var(--ca-border); display: flex; flex-direction: column; overflow: hidden; color: var(--ca-text); }
            .ca-head { padding: 16px; border-bottom: 1px solid var(--ca-border); display: flex; justify-content: space-between; align-items: center; }
            .ca-brand { font-weight: bold; font-size: 18px; display: flex; align-items: center; gap: 8px; }
            .ca-tabs { display: flex; background: var(--ca-surf); padding: 4px; gap: 4px; }
            .ca-tab { flex: 1; padding: 8px; border: none; background: none; color: var(--ca-muted); cursor: pointer; border-radius: 6px; }
            .ca-tab.active { background: var(--ca-prim); color: white; }
            .ca-body { padding: 16px; overflow-y: auto; }
            .ca-search-box { position: relative; margin-bottom: 16px; }
            .ca-input { width: 100%; padding: 10px 10px 10px 35px; background: var(--ca-surf); border: 1px solid var(--ca-border); border-radius: 8px; color: white; box-sizing: border-box; }
            .ca-icon { position: absolute; left: 10px; top: 10px; width: 18px; color: var(--ca-muted); }
            .ca-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
            .ca-item { background: var(--ca-surf); padding: 10px; border-radius: 8px; border: 1px solid var(--ca-border); display: flex; align-items: center; gap: 10px; cursor: pointer; transition: 0.2s; position: relative; }
            .ca-item:hover { border-color: var(--ca-prim); }
            .ca-item img { width: 16px; height: 16px; }
            .ca-item-name { font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
            .ca-status { width: 8px; height: 8px; border-radius: 50%; margin-left: auto; }
            .checking .ca-status { background: orange; animation: pulse 1s infinite; }
            .found { border-color: var(--ca-success) !important; }
            .found .ca-status { background: var(--ca-success); }
            .not-found { opacity: 0.5; grayscale: 1; }
            .not-found .ca-status { background: var(--ca-error); }
            .ca-toast { position: fixed; bottom: 80px; right: 20px; background: var(--ca-prim); padding: 8px 16px; border-radius: 4px; color: white; z-index: 10001; }
            .ca-close { background: none; border: none; color: var(--ca-muted); cursor: pointer; font-size: 18px; }
            .ca-btn-small { padding: 4px 8px; background: var(--ca-surf); border: 1px solid var(--ca-border); border-radius: 4px; color: var(--ca-text); cursor: pointer; font-size: 12px; }
            .ca-btn-small:hover { background: var(--ca-prim); border-color: var(--ca-prim); }
            @keyframes pulse { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } }
        `);
    }

    // --- Main UI Logic ---
    const UI = {
        isOpen: false,

        toggle: function() {
            if (this.isOpen) {
                document.querySelector('.ca-overlay')?.remove();
                this.isOpen = false;
            } else {
                this.render();
                this.isOpen = true;
            }
        },

        render: function() {
            const overlay = document.createElement('div');
            overlay.className = 'ca-overlay';
            overlay.onclick = (e) => { if(e.target === overlay) UI.toggle(); };

            const panel = document.createElement('div');
            panel.className = 'ca-panel';
            panel.innerHTML = `
                <div class="ca-head">
                    <div class="ca-brand"><span>⚡</span> ARNA <span style="font-size:10px; color:var(--ca-muted); border:1px solid var(--ca-border); padding:1px 4px; border-radius:4px;">2.2</span></div>
                    <button class="ca-close">✕</button>
                </div>
                <div class="ca-tabs">
                    <button class="ca-tab active" data-view="search">Search</button>
                    <button class="ca-tab" data-view="saved">Saved</button>
                    <button class="ca-tab" data-view="tools">Tools</button>
                </div>
                <div class="ca-body">
                    <div class="ca-search-box">
                        <svg class="ca-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21l-4.35-4.35m1.35-5.65a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
                        <input type="text" class="ca-input" id="ca-user" placeholder="Search username..." autocomplete="off">
                    </div>

                    <div id="view-search">
                        <div style="font-size: 12px; color: var(--ca-muted); margin-bottom: 8px;">Archives & Recorders</div>
                        <div class="ca-grid" id="ca-archives"></div>
                        <div class="ca-item" id="ca-btn-save" style="justify-content:center; border-style:dashed; opacity:0.8; margin-top: 12px;">
                            <span class="ca-item-name">+ Save to Favorites</span>
                        </div>
                    </div>

                    <div id="view-saved" style="display:none">
                        <div style="display:flex; gap:8px; margin-bottom:12px;">
                             <button id="ca-btn-import" class="ca-btn-small" style="flex:1;">⬇️ Import JSON</button>
                             <button id="ca-btn-export" class="ca-btn-small" style="flex:1;">⬆️ Export JSON</button>
                             <input type="file" id="ca-file-input" style="display:none" accept=".json">
                        </div>
                        <div id="ca-saved-list"></div>
                    </div>

                    <div id="view-tools" style="display:none">
                        <div class="ca-grid">
                            <div class="ca-item tool-btn" data-url="https://www.cbhours.com/user/{u}.html">
                                <span class="ca-item-name">📅 Schedule</span>
                            </div>
                            <div class="ca-item tool-btn" data-url="https://statbate.com/search/1/{u}">
                                <span class="ca-item-name">📊 Statistics</span>
                            </div>
                            <div class="ca-item tool-btn" data-url="https://camgirlfinder.net/models/sc/{u}">
                                <span class="ca-item-name">🔍 Finder</span>
                            </div>
                            <div class="ca-item tool-btn" data-url="https://nrtool.to/nrtool/search?site=&s={u}">
                                <span class="ca-item-name">🖼️ Images</span>
                            </div>
                        </div>
                    </div>
                </div>
            `;

            overlay.appendChild(panel);
            document.body.appendChild(overlay);

            // Populate Archives
            const grid = panel.querySelector('#ca-archives');
            archiveSites.forEach(site => {
                const el = document.createElement('div');
                el.className = 'ca-item archive-item';
                el.dataset.url = site.url;
                el.innerHTML = `
                    <img src="${getFaviconUrl(site.domain)}" loading="lazy">
                    <span class="ca-item-name">${site.name}</span>
                    <div class="ca-status"></div>
                `;
                el.onclick = () => {
                    const u = document.getElementById('ca-user').value.trim();
                    if (u) Utils.openSafe(site.url.replace('{username}', u));
                };
                grid.appendChild(el);
            });

            // Event Listeners
            panel.querySelector('.ca-close').onclick = () => UI.toggle();

            const tabs = panel.querySelectorAll('.ca-tab');
            tabs.forEach(t => t.onclick = () => {
                tabs.forEach(x => x.classList.remove('active'));
                t.classList.add('active');
                panel.querySelectorAll('[id^="view-"]').forEach(v => v.style.display = 'none');
                panel.querySelector(`#view-${t.dataset.view}`).style.display = 'block';
                if(t.dataset.view === 'saved') UI.loadSaved();
            });

            const input = panel.querySelector('#ca-user');
            let debounce;
            input.oninput = () => {
                clearTimeout(debounce);
                debounce = setTimeout(() => UI.checkAll(input.value.trim()), 600);
            };

            panel.querySelector('#ca-btn-save').onclick = () => {
                const u = input.value.trim();
                if (u && Utils.isValidUsername(u)) {
                    let s = Storage.get('ca_saved', []);
                    if (!s.includes(u)) {
                        s.push(u);
                        Storage.set('ca_saved', s);
                        UI.showToast(`Saved ${u}`);
                    }
                }
            };

            // Import / Export Logic
            const fileInput = panel.querySelector('#ca-file-input');
            panel.querySelector('#ca-btn-export').onclick = () => {
                const saved = Storage.get('ca_saved', []);
                if(saved.length === 0) return UI.showToast("Nothing to export");
                const blob = new Blob([JSON.stringify(saved, null, 2)], {type: "application/json"});
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = `arna_backup_${new Date().toISOString().slice(0,10)}.json`;
                a.click();
                URL.revokeObjectURL(url);
                UI.showToast("Export successful");
            };

            panel.querySelector('#ca-btn-import').onclick = () => fileInput.click();
            fileInput.onchange = (e) => {
                const file = e.target.files[0];
                if(!file) return;
                const reader = new FileReader();
                reader.onload = (ev) => {
                    try {
                        const list = JSON.parse(ev.target.result);
                        if(Array.isArray(list)) {
                            // Merge unique
                            let current = Storage.get('ca_saved', []);
                            const newItems = list.filter(x => !current.includes(x) && Utils.isValidUsername(x));
                            current = [...current, ...newItems];
                            Storage.set('ca_saved', current);
                            UI.showToast(`Imported ${newItems.length} profiles`);
                            UI.loadSaved();
                        } else {
                            UI.showToast("Invalid JSON format");
                        }
                    } catch(err) {
                        UI.showToast("Error reading file");
                    }
                };
                reader.readAsText(file);
                fileInput.value = ''; // Reset
            };

            // Tools
            panel.querySelectorAll('.tool-btn').forEach(btn => {
                btn.onclick = () => {
                    const u = input.value.trim();
                    if(u) Utils.openSafe(btn.dataset.url.replace('{u}', u));
                };
            });

            this.detectUser(input);
        },

        checkAll: async function(username) {
            if (!username || !Utils.isValidUsername(username)) return;

            const items = document.querySelectorAll('.archive-item');
            items.forEach(el => {
                el.classList.remove('found', 'not-found');
                el.classList.add('checking');
            });

            items.forEach(async (el) => {
                const url = el.dataset.url.replace('{username}', username);
                const exists = await PageChecker.checkPage(url);

                if (document.contains(el)) {
                    el.classList.remove('checking');
                    if (exists) el.classList.add('found');
                    else el.classList.add('not-found');
                }
            });
        },

        loadSaved: function() {
            const list = Storage.get('ca_saved', []);
            const container = document.getElementById('ca-saved-list');
            container.innerHTML = '';

            if (list.length === 0) {
                container.innerHTML = '<div style="text-align:center; padding:20px; color:var(--ca-muted)">No saved profiles</div>';
                return;
            }

            list.forEach(u => {
                const div = document.createElement('div');
                div.className = 'ca-item';
                div.style.justifyContent = 'space-between';
                div.innerHTML = `
                    <span style="font-weight:600">${u}</span>
                    <button class="ca-delete" style="border:none;background:none;cursor:pointer;">🗑️</button>
                `;
                div.onclick = (e) => {
                    if (!e.target.classList.contains('ca-delete')) {
                        document.getElementById('ca-user').value = u;
                        document.querySelector('[data-view="search"]').click();
                        UI.checkAll(u);
                    }
                };
                div.querySelector('.ca-delete').onclick = (e) => {
                    e.stopPropagation();
                    const newList = Storage.get('ca_saved', []).filter(x => x !== u);
                    Storage.set('ca_saved', newList);
                    UI.loadSaved();
                };
                container.appendChild(div);
            });
        },

        detectUser: function(input) {
            try {
                const path = window.location.pathname.split('/').filter(Boolean);
                let user = '';
                if (window.location.host.includes('chaturbate')) user = path[0];
                else user = path[path.length - 1];

                if (user && Utils.isValidUsername(user) && !['auth','tags'].includes(user)) {
                    input.value = user;
                    UI.checkAll(user);
                }
            } catch(e) {}
        },

        showToast: function(msg) {
            const t = document.createElement('div');
            t.className = 'ca-toast';
            t.innerText = msg;
            document.body.appendChild(t);
            setTimeout(() => t.remove(), 3000);
        }
    };

    function init() {
        injectStyles();
        const fab = document.createElement('button');
        fab.className = 'ca-fab';
        fab.innerHTML = '⚡';
        fab.onclick = () => UI.toggle();
        document.body.appendChild(fab);
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();

})();