您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds filtering tools, private video checking, and infinite scroll to camwhores.tv
// ==UserScript== // @name CW.tv Ultimate Toolbox 2025 // @namespace http://tampermonkey.net/ // @version 1.4 // @description Adds filtering tools, private video checking, and infinite scroll to camwhores.tv // @author cakehorn // @match https://www.camwhores.tv/* // @grant GM_xmlhttpRequest // @connect camwhores.tv // ==/UserScript== (function() { 'use strict'; // Configuration const config = { scrollThreshold: 500, // Distance from bottom (in px) to trigger loading more content loadingDelay: 500, // Delay in ms to prevent excessive requests filterDelay: 300, // Delay before applying filter (for typing) checkBatchSize: 5, // Number of videos to check at once checkDelay: 800, // Delay between batch checks (ms) markPrivateVideos: true, // Add visual indicators to private videos }; // Variables let isLoading = false; let isCheckingFriends = false; let nextPageUrl = null; let filterTimer = null; let currentKeywordFilter = ''; let currentMinDuration = 0; // in seconds // Helper function to get the next page URL function getNextPageUrl() { const paginationLinks = document.querySelectorAll('.pagination a'); for (let i = 0; i < paginationLinks.length; i++) { if (paginationLinks[i].textContent.includes('Next')) { return paginationLinks[i].href; } } return null; } // Helper function to parse duration string (e.g., "5:30" to seconds) function parseDuration(durationStr) { if (!durationStr) return 0; // Handle different duration formats const parts = durationStr.trim().split(':'); let seconds = 0; if (parts.length === 3) { // hours:minutes:seconds seconds = parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseInt(parts[2]); } else if (parts.length === 2) { // minutes:seconds seconds = parseInt(parts[0]) * 60 + parseInt(parts[1]); } else if (parts.length === 1) { // seconds only seconds = parseInt(parts[0]); } return isNaN(seconds) ? 0 : seconds; } // Add tools panel with all features function addToolsPanel() { const toolsPanel = document.createElement('div'); toolsPanel.style.cssText = ` position: fixed; bottom: 20px; left: 20px; background-color: #333; color: white; padding: 15px; border-radius: 5px; z-index: 9999; display: flex; flex-direction: column; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); width: 250px; max-height: 90vh; overflow-y: auto; `; // Add title const panelTitle = document.createElement('div'); panelTitle.textContent = 'Video Tools'; panelTitle.style.cssText = ` font-weight: bold; font-size: 16px; margin-bottom: 10px; text-align: center; border-bottom: 1px solid #555; padding-bottom: 5px; `; toolsPanel.appendChild(panelTitle); // 1. Keyword filter const keywordFilterContainer = document.createElement('div'); keywordFilterContainer.style.cssText = `margin-bottom: 10px;`; const keywordLabel = document.createElement('label'); keywordLabel.textContent = 'Filter by keyword:'; keywordLabel.style.cssText = `display: block; margin-bottom: 5px;`; const keywordInput = document.createElement('input'); keywordInput.type = 'text'; keywordInput.placeholder = 'Enter keyword to filter...'; keywordInput.style.cssText = ` width: 100%; padding: 5px; border-radius: 4px; border: 1px solid #555; background-color: #222; color: white; box-sizing: border-box; `; keywordFilterContainer.appendChild(keywordLabel); keywordFilterContainer.appendChild(keywordInput); toolsPanel.appendChild(keywordFilterContainer); // 2. Minimum duration filter const durationFilterContainer = document.createElement('div'); durationFilterContainer.style.cssText = `margin-bottom: 10px;`; const durationLabel = document.createElement('label'); durationLabel.textContent = 'Min duration (mm:ss):'; durationLabel.style.cssText = `display: block; margin-bottom: 5px;`; const durationInput = document.createElement('input'); durationInput.type = 'text'; durationInput.placeholder = 'e.g., 5:00 for 5 minutes'; durationInput.style.cssText = ` width: 100%; padding: 5px; border-radius: 4px; border: 1px solid #555; background-color: #222; color: white; box-sizing: border-box; `; durationFilterContainer.appendChild(durationLabel); durationFilterContainer.appendChild(durationInput); toolsPanel.appendChild(durationFilterContainer); // Apply filters button const applyButton = document.createElement('button'); applyButton.textContent = 'Apply Filters'; applyButton.style.cssText = ` padding: 5px 10px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; margin-bottom: 15px; `; toolsPanel.appendChild(applyButton); // Separator const separator1 = document.createElement('div'); separator1.style.cssText = ` border-top: 1px solid #555; margin: 5px 0 15px 0; `; toolsPanel.appendChild(separator1); // 3. Friend Check Features const friendCheckTitle = document.createElement('div'); friendCheckTitle.textContent = 'Private Video Tools'; friendCheckTitle.style.cssText = ` font-weight: bold; margin-bottom: 10px; `; toolsPanel.appendChild(friendCheckTitle); // Check Friend button const checkFriendButton = document.createElement('button'); checkFriendButton.textContent = 'CHECK FRIEND STATUS'; checkFriendButton.style.cssText = ` padding: 8px 10px; background-color: #FF9800; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; margin-bottom: 10px; width: 100%; `; toolsPanel.appendChild(checkFriendButton); // Erase Inaccessible button const eraseButton = document.createElement('button'); eraseButton.textContent = 'ERASE INACCESSIBLE VIDEOS'; eraseButton.style.cssText = ` padding: 8px 10px; background-color: #F44336; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; margin-bottom: 15px; width: 100%; `; toolsPanel.appendChild(eraseButton); // Status display for friend check const checkStatusDisplay = document.createElement('div'); checkStatusDisplay.id = 'friend-check-status'; checkStatusDisplay.style.cssText = ` font-size: 12px; margin-bottom: 15px; padding: 5px; background-color: #222; border-radius: 3px; display: none; `; toolsPanel.appendChild(checkStatusDisplay); // Separator const separator2 = document.createElement('div'); separator2.style.cssText = ` border-top: 1px solid #555; margin: 5px 0 15px 0; `; toolsPanel.appendChild(separator2); // 4. Navigation Shortcuts const navTitle = document.createElement('div'); navTitle.textContent = 'Quick Navigation'; navTitle.style.cssText = ` font-weight: bold; margin-bottom: 10px; `; toolsPanel.appendChild(navTitle); // Profile button const profileButton = document.createElement('button'); profileButton.textContent = 'MY PROFILE'; profileButton.style.cssText = ` padding: 8px 10px; background-color: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; margin-bottom: 10px; width: 100%; `; toolsPanel.appendChild(profileButton); // Favorites button const favoritesButton = document.createElement('button'); favoritesButton.textContent = 'MY FAVORITES'; favoritesButton.style.cssText = ` padding: 8px 10px; background-color: #9C27B0; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; margin-bottom: 15px; width: 100%; `; toolsPanel.appendChild(favoritesButton); // Add the panel to the page document.body.appendChild(toolsPanel); // Event listeners for filtering keywordInput.addEventListener('input', function() { clearTimeout(filterTimer); filterTimer = setTimeout(() => { currentKeywordFilter = this.value.toLowerCase().trim(); applyFilters(); }, config.filterDelay); }); durationInput.addEventListener('input', function() { clearTimeout(filterTimer); filterTimer = setTimeout(() => { // Parse the input duration const durationStr = this.value.trim(); if (durationStr) { currentMinDuration = parseDuration(durationStr); } else { currentMinDuration = 0; } applyFilters(); }, config.filterDelay); }); applyButton.addEventListener('click', function() { currentKeywordFilter = keywordInput.value.toLowerCase().trim(); const durationStr = durationInput.value.trim(); if (durationStr) { currentMinDuration = parseDuration(durationStr); } else { currentMinDuration = 0; } applyFilters(); }); // Friend Check functionality checkFriendButton.addEventListener('click', function() { if (isCheckingFriends) return; // Get all video items const videoItems = Array.from(document.querySelectorAll('.thumb, .item')); if (videoItems.length === 0) { alert('No videos found on this page.'); return; } // Look for private video indicators const privateVideos = videoItems.filter(item => { // Look for any private video indicators return item.querySelector('.private, .lock, [class*="private"], [class*="lock"]') !== null || (item.textContent && (item.textContent.includes('Private') || item.textContent.includes('Friends only'))); }); if (privateVideos.length === 0) { alert('No private videos found on this page.'); return; } isCheckingFriends = true; checkStatusDisplay.style.display = 'block'; checkStatusDisplay.innerHTML = `Found ${privateVideos.length} private videos. Starting check...`; checkStatusDisplay.style.color = '#FFC107'; // Process videos in batches to avoid overloading checkPrivateVideos(privateVideos, 0, checkStatusDisplay); }); // Erase inaccessible videos eraseButton.addEventListener('click', function() { const inaccessibleVideos = document.querySelectorAll('.private-video-inaccessible'); if (inaccessibleVideos.length === 0) { alert('No inaccessible videos found. Run CHECK FRIEND STATUS first.'); return; } // Remove all inaccessible videos inaccessibleVideos.forEach(item => { item.style.display = 'none'; }); alert(`Removed ${inaccessibleVideos.length} inaccessible private videos from view.`); }); // Navigation button event listeners profileButton.addEventListener('click', function() { // Navigate to profile page - adjust URL as needed window.location.href = 'https://www.camwhores.tv/my/'; }); favoritesButton.addEventListener('click', function() { // Navigate to favorites page - adjust URL as needed window.location.href = 'https://www.camwhores.tv/my/favourites/videos/'; }); return toolsPanel; } // Check private videos for accessibility - Fixed function function checkPrivateVideos(videos, startIndex, statusDisplay) { if (startIndex >= videos.length) { isCheckingFriends = false; statusDisplay.innerHTML = `✅ Completed checking ${videos.length} private videos.`; statusDisplay.style.color = '#4CAF50'; // Count results const accessible = document.querySelectorAll('.private-video-accessible').length; const inaccessible = document.querySelectorAll('.private-video-inaccessible').length; statusDisplay.innerHTML += `<br>✓ Accessible: ${accessible}<br>✗ Inaccessible: ${inaccessible}`; return; } // Process a batch of videos const endIndex = Math.min(startIndex + config.checkBatchSize, videos.length); statusDisplay.innerHTML = `Checking private videos ${startIndex+1}-${endIndex} of ${videos.length}...`; // Process each video in the current batch for (let i = startIndex; i < endIndex; i++) { const videoItem = videos[i]; const link = videoItem.querySelector('a[href*="/videos/"]'); if (!link) continue; const videoUrl = link.href; // Check if this video has already been processed if (videoItem.classList.contains('private-video-checked')) { continue; } // Mark as being processed videoItem.classList.add('private-video-checked'); // Create a fetch request to check accessibility fetch(videoUrl) .then(response => response.text()) .then(html => { // FIX: More robust check for inaccessible videos // Check if the response indicates an inaccessible video first const isInaccessible = html.includes('need to be friends') || html.includes('private video') || html.includes('friends only') || html.includes('You are not friends') || // Check if there's no video player (!html.includes('video-player') && !html.includes('video_player') && !html.includes('player-holder')); // Mark the video accordingly if (!isInaccessible) { videoItem.classList.add('private-video-accessible'); // Highlight title in blue and green const titleElement = videoItem.querySelector('.title a, a.title'); if (titleElement) { titleElement.style.color = '#00C853'; titleElement.style.textShadow = '0 0 2px #2196F3'; titleElement.style.fontWeight = 'bold'; } } else { videoItem.classList.add('private-video-inaccessible'); // Add red X through title const titleElement = videoItem.querySelector('.title a, a.title'); if (titleElement) { titleElement.style.color = '#F44336'; titleElement.style.textDecoration = 'line-through'; // Add X icon const xIcon = document.createElement('span'); xIcon.textContent = '❌ '; xIcon.style.color = '#F44336'; titleElement.parentNode.insertBefore(xIcon, titleElement); } // Add semi-transparent overlay const overlay = document.createElement('div'); overlay.style.cssText = ` position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.6); display: flex; justify-content: center; align-items: center; z-index: 5; `; const notAccessible = document.createElement('div'); notAccessible.textContent = 'NOT ACCESSIBLE'; notAccessible.style.cssText = ` color: #F44336; font-weight: bold; background-color: rgba(0, 0, 0, 0.7); padding: 5px 10px; border-radius: 3px; transform: rotate(-15deg); `; overlay.appendChild(notAccessible); // Make sure the video container has position relative const imgContainer = videoItem.querySelector('.thumb-container, .img'); if (imgContainer) { imgContainer.style.position = 'relative'; imgContainer.appendChild(overlay); } } }) .catch(error => { console.error('Error checking video:', error); // Mark as error but still continue videoItem.classList.add('private-video-error'); }); } // Schedule the next batch setTimeout(() => { checkPrivateVideos(videos, endIndex, statusDisplay); }, config.checkDelay); } // Apply filters to video items function applyFilters() { const videoItems = document.querySelectorAll('.thumb, .item'); let hiddenCount = 0; videoItems.forEach(item => { let shouldShow = true; // Get the title for keyword filtering const titleElement = item.querySelector('.title a, a.title'); const title = titleElement ? titleElement.textContent.toLowerCase() : ''; // Check keyword filter if (currentKeywordFilter && !title.includes(currentKeywordFilter)) { shouldShow = false; } // Check duration filter if still visible if (shouldShow && currentMinDuration > 0) { const durationElement = item.querySelector('.duration'); if (durationElement) { const videoDuration = parseDuration(durationElement.textContent); if (videoDuration < currentMinDuration) { shouldShow = false; } } } // Apply visibility if (shouldShow) { item.style.display = ''; } else { item.style.display = 'none'; hiddenCount++; } }); // Display filter results const existingStatus = document.getElementById('filter-status'); if (existingStatus) { existingStatus.remove(); } if (currentKeywordFilter || currentMinDuration > 0) { const statusDiv = document.createElement('div'); statusDiv.id = 'filter-status'; statusDiv.style.cssText = ` position: fixed; top: 20px; right: 20px; background-color: rgba(33, 33, 33, 0.8); color: white; padding: 10px; border-radius: 5px; z-index: 9999; font-size: 14px; `; const totalVideos = videoItems.length; const visibleVideos = totalVideos - hiddenCount; statusDiv.innerHTML = ` <b>Filter active:</b> Showing ${visibleVideos} of ${totalVideos} videos<br> ${currentKeywordFilter ? `<b>Keyword:</b> "${currentKeywordFilter}"<br>` : ''} ${currentMinDuration > 0 ? `<b>Min duration:</b> ${Math.floor(currentMinDuration/60)}:${(currentMinDuration%60).toString().padStart(2, '0')}` : ''} `; document.body.appendChild(statusDiv); // Auto-hide after 5 seconds setTimeout(() => { if (statusDiv.parentNode) { statusDiv.style.opacity = '0'; statusDiv.style.transition = 'opacity 1s'; setTimeout(() => statusDiv.remove(), 1000); } }, 5000); } } // Check if the current page has videos (to apply infinite scroll) function isVideoPage() { // Check if the page contains video thumbnails or a pagination element return document.querySelectorAll('.thumb, .pagination').length > 0; } // Load more content when scrolling function loadMoreContent() { if (isLoading || !nextPageUrl) return; isLoading = true; // Create a loading indicator const loadingIndicator = document.createElement('div'); loadingIndicator.style.cssText = ` text-align: center; padding: 20px; font-size: 16px; color: #999; `; loadingIndicator.innerHTML = 'Loading more videos...'; // Find the container to append to const contentContainer = document.querySelector('.thumbs, .list-videos'); if (contentContainer) { contentContainer.appendChild(loadingIndicator); // Fetch the next page fetch(nextPageUrl) .then(response => response.text()) .then(html => { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); // Extract the video elements from the next page const newItems = doc.querySelectorAll('.thumb, .item'); // Append the new items to the current page newItems.forEach(item => { const clonedItem = item.cloneNode(true); contentContainer.appendChild(clonedItem); // Apply current filters to new items if ((currentKeywordFilter && currentKeywordFilter !== '') || currentMinDuration > 0) { let shouldShow = true; // Check keyword filter if (currentKeywordFilter && currentKeywordFilter !== '') { const titleElement = clonedItem.querySelector('.title a, a.title'); const title = titleElement ? titleElement.textContent.toLowerCase() : ''; if (!title.includes(currentKeywordFilter)) { shouldShow = false; } } // Check duration filter if (shouldShow && currentMinDuration > 0) { const durationElement = clonedItem.querySelector('.duration'); if (durationElement) { const videoDuration = parseDuration(durationElement.textContent); if (videoDuration < currentMinDuration) { shouldShow = false; } } } if (!shouldShow) { clonedItem.style.display = 'none'; } } }); // Update the next page URL nextPageUrl = getNextPageUrl(); // Remove the loading indicator loadingIndicator.remove(); // Set a small delay before allowing more loads setTimeout(() => { isLoading = false; }, config.loadingDelay); }) .catch(error => { console.error('Error loading more content:', error); loadingIndicator.innerHTML = 'Error loading more videos. <a href="' + nextPageUrl + '">Click here to try again</a>.'; setTimeout(() => { isLoading = false; }, config.loadingDelay); }); } } // Scroll event listener for infinite scroll function setupInfiniteScroll() { window.addEventListener('scroll', function() { const scrollHeight = Math.max( document.body.scrollHeight, document.documentElement.scrollHeight ); const scrollTop = window.scrollY; const clientHeight = document.documentElement.clientHeight; // Check if user has scrolled close to the bottom if (scrollHeight - scrollTop - clientHeight < config.scrollThreshold) { loadMoreContent(); } }); } // Initialize the script function init() { // Add CSS for private video styling const styleElement = document.createElement('style'); styleElement.textContent = ` .private-video-accessible .title a { color: #00C853 !important; text-shadow: 0 0 2px #2196F3; font-weight: bold; } .private-video-inaccessible .title a { color: #F44336 !important; text-decoration: line-through !important; } .private-video-inaccessible { opacity: 0.8; } `; document.head.appendChild(styleElement); // Add the tools panel addToolsPanel(); // Setup infinite scroll only on video pages if (isVideoPage()) { nextPageUrl = getNextPageUrl(); setupInfiniteScroll(); } } // Run the initialization when the page is fully loaded window.addEventListener('load', init); })();