// ==UserScript== // @name Circle FTP Playlist Maker (Ultimate) // @namespace http://tampermonkey.net/ // @version 7.0 // @description Collects links on Circle FTP and creates a XPF playlist for VLC // @author LazyDevUserX // @match http://15.1.1.50/* // @match http://new.circleftp.net/* // @downloadURL https://raw.githubusercontent.com/LazyDevUserX/CircleFTP-Playlist-Maker-Ultimate/main/userscript/circle-ftp-playlist-maker-ultimate.user.js // @updateURL https://raw.githubusercontent.com/LazyDevUserX/CircleFTP-Playlist-Maker-Ultimate/main/userscript/circle-ftp-playlist-maker-ultimate.user.js // @grant none // ==/UserScript== (function () { 'use strict'; // Configuration const config = { excludedDomains: ['new.circleftp.net', 'hd.circleftp.net'], allowedExtensions: ['.mp4', '.mkv', '.avi', '.mov', '.wmv', '.flv', '.webm'], minFileSize: 1024 * 1024, // 1MB minimum file size maxLinks: 1000, // Safety limit maxTitleLength: 200, // Maximum title length for filenames contentPaths: ['/content/'], // Paths where button should be visible seasonSelectors: { 1: '/html/body/div[1]/main/div[2]/section/div/div[1]', 2: '/html/body/div[1]/main/div[2]/section/div/div[2]', 3: '/html/body/div[1]/main/div[2]/section/div/div[3]', 4: '/html/body/div[1]/main/div[2]/section/div/div[4]', 5: '/html/body/div[1]/main/div[2]/section/div/div[5]', 6: '/html/body/div[1]/main/div[2]/section/div/div[6]', 7: '/html/body/div[1]/main/div[2]/section/div/div[7]', 8: '/html/body/div[1]/main/div[2]/section/div/div[8]' } }; // Check if current page is a content page function isContentPage() { return config.contentPaths.some(path => window.location.pathname.includes(path)); } // Detect available seasons function detectAvailableSeasons() { const availableSeasons = []; // Check each season selector for (const [season, selector] of Object.entries(config.seasonSelectors)) { try { const element = document.evaluate(selector, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; if (element) { availableSeasons.push(parseInt(season)); } } catch (e) { console.error(`Error checking season ${season}:`, e); } } // If no seasons found with XPath, try alternative method if (availableSeasons.length === 0) { console.log("No seasons found with XPath, trying alternative method"); // Try to find season buttons by text content const seasonButtons = document.querySelectorAll('button'); seasonButtons.forEach(button => { const text = button.textContent.trim(); const seasonMatch = text.match(/Season\s+(\d+)/i); if (seasonMatch) { const seasonNum = parseInt(seasonMatch[1]); if (!availableSeasons.includes(seasonNum)) { availableSeasons.push(seasonNum); } } }); } // Sort seasons numerically availableSeasons.sort((a, b) => a - b); return availableSeasons; } // Create the modern UI function createModernUI() { // Remove existing UI if any const existingUI = document.getElementById('playlistModernUI'); if (existingUI) { existingUI.remove(); } // Create main container const uiContainer = document.createElement('div'); uiContainer.id = 'playlistModernUI'; uiContainer.innerHTML = `
Select Season
All Seasons
`; document.body.appendChild(uiContainer); // Add styles const style = document.createElement('style'); style.textContent = ` /* Modern UI Styles */ .fab-container { position: fixed; bottom: 30px; right: 30px; z-index: 10000; } .fab-button { position: relative; width: 64px; height: 64px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; cursor: pointer; box-shadow: 0 8px 24px rgba(102, 126, 234, 0.3); overflow: visible; transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); display: flex; align-items: center; justify-content: center; } .fab-button:hover { transform: scale(1.1); box-shadow: 0 12px 30px rgba(102, 126, 234, 0.5); } .fab-button:active { transform: scale(0.95); } .fab-button.processing { animation: pulse 2s infinite; } @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.7); } 70% { box-shadow: 0 0 0 15px rgba(102, 126, 234, 0); } 100% { box-shadow: 0 0 0 0 rgba(102, 126, 234, 0); } } .fab-icon { position: relative; z-index: 2; width: 28px; height: 28px; color: white; transition: transform 0.3s ease; } .fab-button:hover .fab-icon { transform: translateY(-3px); } .fab-pulse { position: absolute; top: 0; left: 0; width: 100%; height: 100%; border-radius: 50%; background: inherit; z-index: 1; opacity: 0; transform: scale(1); } .fab-button.right-clicked .fab-pulse { animation: ripple-effect 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275); } @keyframes ripple-effect { 0% { transform: scale(1); opacity: 0.7; } 100% { transform: scale(2.5); opacity: 0; } } .season-menu { position: absolute; bottom: 80px; right: 0; background: rgba(30, 30, 40, 0.95); backdrop-filter: blur(10px); border-radius: 16px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); padding: 16px; min-width: 200px; opacity: 0; transform: translateY(20px) scale(0.9); transform-origin: bottom right; transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); pointer-events: none; z-index: 10001; border: 1px solid rgba(255, 255, 255, 0.1); } .season-menu.show { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; } .season-menu-header { color: rgba(255, 255, 255, 0.7); font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); } .season-menu-items { display: flex; flex-direction: column; gap: 8px; } .season-item { display: flex; align-items: center; padding: 12px; border-radius: 12px; cursor: pointer; transition: all 0.2s ease; position: relative; overflow: hidden; } .season-item::before { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); opacity: 0; transition: opacity 0.2s ease; z-index: 0; } .season-item:hover::before { opacity: 0.1; } .season-icon { width: 20px; height: 20px; color: rgba(255, 255, 255, 0.8); margin-right: 12px; position: relative; z-index: 1; transition: transform 0.2s ease; } .season-item:hover .season-icon { transform: translateX(3px); } .season-text { color: white; font-size: 14px; font-weight: 500; position: relative; z-index: 1; } /* Notification Styles */ .notification-container { position: fixed; top: 20px; right: 20px; z-index: 10002; display: flex; flex-direction: column; gap: 12px; } .notification { background: rgba(30, 30, 40, 0.95); backdrop-filter: blur(10px); border-radius: 12px; padding: 16px 20px; color: white; font-size: 14px; font-weight: 500; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); border: 1px solid rgba(255, 255, 255, 0.1); transform: translateX(400px); opacity: 0; transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); max-width: 320px; position: relative; overflow: hidden; } .notification.show { transform: translateX(0); opacity: 1; } .notification::before { content: ''; position: absolute; top: 0; left: 0; width: 4px; height: 100%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } .notification.success::before { background: linear-gradient(135deg, #4CAF50, #45a049); } .notification.warning::before { background: linear-gradient(135deg, #FF9800, #F57C00); } .notification.error::before { background: linear-gradient(135deg, #F44336, #D32F2F); } .notification-close { position: absolute; top: 12px; right: 12px; width: 16px; height: 16px; cursor: pointer; opacity: 0.6; transition: opacity 0.2s ease; } .notification-close:hover { opacity: 1; } .notification-close::before, .notification-close::after { content: ''; position: absolute; top: 50%; left: 0; width: 100%; height: 2px; background: white; } .notification-close::before { transform: rotate(45deg); } .notification-close::after { transform: rotate(-45deg); } `; document.head.appendChild(style); // Hide UI if not on content page if (!isContentPage()) { uiContainer.style.display = 'none'; } // Set up event listeners setupEventListeners(); } // Set up event listeners function setupEventListeners() { const button = document.getElementById('playlistBtn'); const seasonMenu = document.getElementById('seasonMenu'); // Left click - download all button.addEventListener('click', function(e) { createRipple(e, this); generatePlaylist('all'); }); // Right click - show season menu button.addEventListener('contextmenu', function(e) { e.preventDefault(); // Add right-click animation this.classList.add('right-clicked'); setTimeout(() => { this.classList.remove('right-clicked'); }, 800); // Detect available seasons and update menu const availableSeasons = detectAvailableSeasons(); updateSeasonMenu(availableSeasons); // Toggle menu seasonMenu.classList.toggle('show'); }); // Close menu when clicking outside document.addEventListener('click', function(e) { if (!button.contains(e.target) && !seasonMenu.contains(e.target)) { seasonMenu.classList.remove('show'); } }); // Handle season selection document.getElementById('seasonMenuItems').addEventListener('click', function(e) { const seasonItem = e.target.closest('.season-item'); if (seasonItem) { const season = seasonItem.getAttribute('data-season'); seasonMenu.classList.remove('show'); generatePlaylist(season); } }); } // Create ripple effect function createRipple(event, button) { const ripple = document.createElement('span'); const rect = button.getBoundingClientRect(); const size = Math.max(rect.width, rect.height); const x = event.clientX - rect.left - size / 2; const y = event.clientY - rect.top - size / 2; ripple.style.width = ripple.style.height = size + 'px'; ripple.style.left = x + 'px'; ripple.style.top = y + 'px'; ripple.style.position = 'absolute'; ripple.style.borderRadius = '50%'; ripple.style.background = 'rgba(255, 255, 255, 0.6)'; ripple.style.transform = 'scale(0)'; ripple.style.animation = 'ripple-animation 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275)'; ripple.style.pointerEvents = 'none'; ripple.style.zIndex = '3'; button.appendChild(ripple); setTimeout(() => { ripple.remove(); }, 800); } // Update season menu with available seasons function updateSeasonMenu(availableSeasons) { const seasonMenuItems = document.getElementById('seasonMenuItems'); // Clear existing items except "All Seasons" const allSeasonsItem = seasonMenuItems.querySelector('[data-season="all"]'); seasonMenuItems.innerHTML = ''; seasonMenuItems.appendChild(allSeasonsItem); // Add season items availableSeasons.forEach(season => { const item = document.createElement('div'); item.className = 'season-item'; item.setAttribute('data-season', season); item.innerHTML = `
Season ${season}
`; seasonMenuItems.appendChild(item); }); } // Show notification function showNotification(message, type = 'info') { const container = document.getElementById('notificationContainer'); const notification = document.createElement('div'); notification.className = `notification ${type}`; notification.innerHTML = `
${message}
`; container.appendChild(notification); // Trigger animation setTimeout(() => { notification.classList.add('show'); }, 10); // Set up close button const closeButton = notification.querySelector('.notification-close'); closeButton.addEventListener('click', () => { notification.classList.remove('show'); setTimeout(() => { notification.remove(); }, 400); }); // Auto dismiss setTimeout(() => { notification.classList.remove('show'); setTimeout(() => { notification.remove(); }, 400); }, 4000); } // Enhanced title extraction with fallbacks function getTitle() { // Try multiple selectors for reliability const selectors = [ "/html/body/div/main/div[1]/div", "h1", ".title", ".page-title", "head title" ]; for (const selector of selectors) { let element; if (selector.startsWith('/')) { element = document.evaluate(selector, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; } else { element = document.querySelector(selector); } if (element && element.textContent.trim()) { // Clean the title but don't truncate it yet return element.textContent.trim() .replace(/[\\/:*?"<>|]/g, '') // Remove invalid filename characters .replace(/\s+/g, ' ') // Replace multiple spaces with single space .trim(); // Remove leading/trailing spaces } } return "CircleFTP_Playlist"; } // Sanitize filename to ensure it's valid and not too long function sanitizeFilename(filename) { // First, remove any special characters that might cause issues let sanitized = filename.replace(/[\\/:*?"<>|]/g, ''); // If the filename is too long, truncate it if (sanitized.length > config.maxTitleLength) { // Try to break at a space to avoid cutting words in half const truncated = sanitized.substring(0, config.maxTitleLength); const lastSpace = truncated.lastIndexOf(' '); if (lastSpace > config.maxTitleLength * 0.8) { // If we found a space near the end, break there sanitized = truncated.substring(0, lastSpace); } else { // Otherwise, just truncate at the max length sanitized = truncated; } } return sanitized.trim(); } // Optimized link collection with filtering function collectLinks(season = 'all') { const links = new Set(); let contentArea; if (season === 'all') { contentArea = document.querySelector('main, .content, #content') || document.body; } else { // Get the specific season container using XPath const seasonXPath = config.seasonSelectors[season]; if (seasonXPath) { const seasonElement = document.evaluate(seasonXPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; if (seasonElement) { contentArea = seasonElement; } else { showNotification(`Season ${season} container not found`, 'warning'); return []; } } else { showNotification(`No selector defined for Season ${season}`, 'warning'); return []; } } const linkElements = contentArea.querySelectorAll('a[href]'); for (const link of linkElements) { if (links.size >= config.maxLinks) break; const href = link.getAttribute('href'); if (!href) continue; // Skip excluded domains if (config.excludedDomains.some(domain => href.includes(domain))) continue; // Skip non-media files const hasExtension = config.allowedExtensions.some(ext => href.toLowerCase().includes(ext)); if (!hasExtension) continue; // Convert relative URLs to absolute const absoluteUrl = new URL(href, window.location.href).href; // Skip if already added if (links.has(absoluteUrl)) continue; links.add(absoluteUrl); } return Array.from(links); } // Generate playlist with progress feedback async function generatePlaylist(season = 'all') { const button = document.getElementById('playlistBtn'); button.classList.add('processing'); try { const links = collectLinks(season); if (links.length === 0) { showNotification('No valid media links found', 'warning'); return; } const title = getTitle(); const seasonText = season === 'all' ? 'All Seasons' : `Season ${season}`; const xspfContent = createXSPF(links, `${title} - ${seasonText}`); downloadPlaylist(xspfContent, `${title} - ${seasonText}`); showNotification(`Playlist created with ${links.length} items from ${seasonText}`, 'success'); } catch (error) { console.error('Playlist generation failed:', error); showNotification('Failed to create playlist', 'error'); } finally { setTimeout(() => { button.classList.remove('processing'); }, 1000); } } // Create XSPF content function createXSPF(links, title) { let xspfContent = ` ${escapeXml(title)} `; links.forEach((link, index) => { xspfContent += ` ${escapeXml(link)} ${index} `; }); xspfContent += ` `; links.forEach((link, index) => { xspfContent += ` `; }); xspfContent += ` `; return xspfContent; } // Download playlist file function downloadPlaylist(content, title) { // Sanitize the title for use as a filename const filename = sanitizeFilename(title); const blob = new Blob([content], { type: 'application/xspf+xml' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${filename}.xspf`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } // XML escape utility function escapeXml(unsafe) { return unsafe.replace(/[<>&'"]/g, c => ({ '<': '<', '>': '>', '&': '&', '\'': ''', '"': '"' }[c])); } // Initialize createModernUI(); // Handle navigation changes (for single-page applications) let lastUrl = location.href; new MutationObserver(() => { const url = location.href; if (url !== lastUrl) { lastUrl = url; // Check if we need to show/hide the UI based on new URL const uiContainer = document.getElementById('playlistModernUI'); if (uiContainer) { if (isContentPage()) { uiContainer.style.display = ''; } else { uiContainer.style.display = 'none'; } } } }).observe(document, { subtree: true, childList: true }); })();