Cam ARNA

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

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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();

})();