Multi-archive search tool with modern dashboard design + Import/Export
// ==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();
})();