您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Helper for protocol_hook.lua - Enhanced drag-to-action system for media links with MPV integration. Supports multiple protocols (mpv://, streamlink, yt-dlp) and customizable actions.
当前为
// ==UserScript== // @name Handlers Helper (Improved) // @namespace Violentmonkey Scripts // @version 4.8.7 // @description Helper for protocol_hook.lua - Enhanced drag-to-action system for media links with MPV integration. Supports multiple protocols (mpv://, streamlink, yt-dlp) and customizable actions. // @author hongmd (improved) // @license MIT // @homepageURL https://github.com/hongmd/userscript-improved // @supportURL https://github.com/hongmd/userscript-improved/issues // @include *://*/* // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @run-at document-start // @noframes // ==/UserScript== 'use strict'; // === CONSTANTS AND DEFAULTS === const GUIDE = 'Value: pipe ytdl stream mpv iptv'; /* ACTION EXPLANATIONS: * 📺 pipe (UP) → mpv://mpvy/ → Pipe video to MPV with yt-dlp processing * 📥 ytdl (DOWN) → mpv://ytdl/ → Download video using yt-dlp * 🌊 stream (LEFT) → mpv://stream/ → Stream video using streamlink * ▶️ mpv (RIGHT) → mpv://play/ → Direct play in MPV player * 📋 iptv → mpv://list/ → Add to IPTV playlist */ const LIVE_WINDOW_WIDTH = 400; const LIVE_WINDOW_HEIGHT = 640; const DRAG_THRESHOLD = 50; const RIGHT_CLICK_DELAY = 200; // Default values const DEFAULTS = { UP: 'pipe', DOWN: 'ytdl', LEFT: 'stream', RIGHT: 'mpv', hlsdomain: 'cdn.animevui.com', livechat: false, total_direction: 4, down_confirm: true, // New setting to confirm DOWN action debug: false // Debug logging toggle }; // Direction enum const DirectionEnum = Object.freeze({ CENTER: 5, RIGHT: 6, LEFT: 4, UP: 2, DOWN: 8, UP_LEFT: 1, UP_RIGHT: 3, DOWN_LEFT: 7, DOWN_RIGHT: 9 }); // === GLOBAL STATE === let settings = { UP: GM_getValue('UP', DEFAULTS.UP), DOWN: GM_getValue('DOWN', DEFAULTS.DOWN), LEFT: GM_getValue('LEFT', DEFAULTS.LEFT), RIGHT: GM_getValue('RIGHT', DEFAULTS.RIGHT), hlsdomain: GM_getValue('hlsdomain', DEFAULTS.hlsdomain), livechat: GM_getValue('livechat', DEFAULTS.livechat), total_direction: GM_getValue('total_direction', DEFAULTS.total_direction), down_confirm: GM_getValue('down_confirm', DEFAULTS.down_confirm), debug: GM_getValue('debug', DEFAULTS.debug) }; let hlsdomainArray = settings.hlsdomain.split(',').filter(d => d.trim()); let collectedUrls = new Map(); // Use Map instead of object for better performance let attachedElements = new WeakSet(); // Use WeakSet to prevent memory leaks // Add CSS class for collected links styling GM_addStyle(` .hh-collected-link { box-sizing: border-box !important; border: solid yellow 4px !important; } `); debugLog('Handlers Helper loaded with settings:', settings); // === UTILITY FUNCTIONS === function safePrompt(message, defaultValue) { try { const result = window.prompt(message, defaultValue); return result === null ? null : result.trim(); } catch (error) { debugError('Prompt error:', error); return null; } } function updateSetting(key, value) { settings[key] = value; GM_setValue(key, value); debugLog(`Updated ${key} to:`, value); } function reloadPage() { try { window.location.reload(); } catch (error) { debugError('Reload failed:', error); } } // Optimized logging function - only logs when debug is enabled function debugLog(...args) { if (settings.debug) { console.log(...args); } } function debugWarn(...args) { if (settings.debug) { console.warn(...args); } } function debugError(...args) { if (settings.debug) { console.error(...args); } } function getParentByTagName(element, tagName) { if (!element || typeof tagName !== 'string') return null; tagName = tagName.toLowerCase(); let current = element; while (current && current.nodeType === Node.ELEMENT_NODE) { if (current.tagName && current.tagName.toLowerCase() === tagName) { return current; } current = current.parentNode; } return null; } function encodeUrl(url) { try { // Validate URL before encoding new URL(url); return btoa(url).replace(/[/+=]/g, match => match === '/' ? '_' : match === '+' ? '-' : '' ); } catch (error) { debugError('Invalid URL provided to encodeUrl:', url, error); return ''; } } // Process DOM mutations for drag handler optimization function processMutations(mutations) { let needsUpdate = false; mutations.forEach(function (mutation) { if (mutation.type === 'childList') { mutation.addedNodes.forEach(function (node) { if (node.nodeType === Node.ELEMENT_NODE) { const links = node.querySelectorAll ? node.querySelectorAll('a[href]') : []; if (links.length > 0) needsUpdate = true; if (node.tagName === 'A' && node.href && !node.draggable) { needsUpdate = true; } } }); } }); if (needsUpdate) { makeLinksDraggable(); } } // YouTube menu command helper function addYouTubeMenuCommand(label, url, persistent) { GM_registerMenuCommand(label, function () { if (persistent) { if (url.includes('m.youtube.com')) { GM_setValue('hh_mobile', true); } else if (url.includes('www.youtube.com')) { GM_setValue('hh_mobile', false); } } else { GM_deleteValue('hh_mobile'); } try { location.replace(url); } catch (error) { debugError('Failed to navigate:', error); } }); } // === HELP AND TESTING FUNCTIONS === function showActionHelp() { const helpText = `🎮 DRAG DIRECTIONS & ACTIONS: 📺 UP (↑): ${settings.UP} → Pipes video to MPV with yt-dlp processing → Good for: YouTube, complex streams 📥 DOWN (↓): ${settings.DOWN} ${settings.down_confirm ? '(Confirm: ON)' : '(Confirm: OFF)'} → Downloads video using yt-dlp → Good for: Saving videos locally 🌊 LEFT (←): ${settings.LEFT} → Streams video using streamlink → Good for: Twitch, live streams ▶️ RIGHT (→): ${settings.RIGHT} → Direct play in MPV player → Good for: Direct video files 📋 UP-LEFT (↖): list → Adds to IPTV/playlist → Good for: Building playlists 🎯 USAGE: 1. Hover over a video link 2. Drag in desired direction 3. Release to trigger action 🔧 Settings: ${settings.total_direction} directions, HLS: ${hlsdomainArray.length} domains 🐛 TROUBLESHOOTING: - Check browser console (F12) for debug logs - Make sure links are draggable (script auto-enables) - Try dragging further than ${DRAG_THRESHOLD}px - Look for "🚀 Drag started" and "⬆️ Executing UP action" logs`; alert(helpText); } function testDirections() { debugLog('🧪 Testing direction detection:'); const tests = [ { name: 'UP', start: [100, 100], end: [100, 50] }, { name: 'DOWN', start: [100, 100], end: [100, 150] }, { name: 'LEFT', start: [100, 100], end: [50, 100] }, { name: 'RIGHT', start: [100, 100], end: [150, 100] }, { name: 'UP-LEFT', start: [100, 100], end: [50, 50] }, { name: 'NO MOVEMENT', start: [100, 100], end: [105, 105] } ]; tests.forEach(test => { const direction = getDirection(test.start[0], test.start[1], test.end[0], test.end[1]); debugLog(`${test.name}: (${test.start[0]},${test.start[1]}) -> (${test.end[0]},${test.end[1]}) = Direction ${direction}`); }); } // === MENU COMMANDS SETUP === function setupMenuCommands() { // Help command first GM_registerMenuCommand('❓ Show Action Help', showActionHelp); GM_registerMenuCommand('🧪 Test Directions', testDirections); GM_registerMenuCommand(`📺 UP: ${settings.UP}`, function () { const value = safePrompt(GUIDE + '\n\n↑ UP Action (pipe = stream to MPV with yt-dlp)', settings.UP); if (value) { updateSetting('UP', value); reloadPage(); } }); GM_registerMenuCommand(`📥 DOWN: ${settings.DOWN}`, function () { const value = safePrompt(GUIDE + '\n\n↓ DOWN Action (ytdl = download with yt-dlp)', settings.DOWN); if (value) { updateSetting('DOWN', value); reloadPage(); } }); GM_registerMenuCommand(`🌊 LEFT: ${settings.LEFT}`, function () { const value = safePrompt(GUIDE + '\n\n← LEFT Action (stream = use streamlink)', settings.LEFT); if (value) { updateSetting('LEFT', value); reloadPage(); } }); GM_registerMenuCommand(`▶️ RIGHT: ${settings.RIGHT}`, function () { const value = safePrompt(GUIDE + '\n\n→ RIGHT Action (mpv = direct play)', settings.RIGHT); if (value) { updateSetting('RIGHT', value); reloadPage(); } }); GM_registerMenuCommand('HLS Domains', function () { const value = safePrompt('Example: 1.com,2.com,3.com,4.com', settings.hlsdomain); if (value !== null) { updateSetting('hlsdomain', value); hlsdomainArray = value.split(',').filter(d => d.trim()); } }); GM_registerMenuCommand(`Live Chat: ${settings.livechat}`, function () { updateSetting('livechat', !settings.livechat); reloadPage(); }); GM_registerMenuCommand(`Directions: ${settings.total_direction}`, function () { const newValue = settings.total_direction === 4 ? 8 : 4; updateSetting('total_direction', newValue); reloadPage(); }); GM_registerMenuCommand(`DOWN Confirm: ${settings.down_confirm ? 'ON' : 'OFF'}`, function () { updateSetting('down_confirm', !settings.down_confirm); reloadPage(); }); GM_registerMenuCommand(`🐛 Debug Mode: ${settings.debug ? 'ON' : 'OFF'}`, function () { updateSetting('debug', !settings.debug); reloadPage(); }); } // === LIVE CHAT FUNCTIONS === function openPopout(chatUrl) { try { const features = [ 'fullscreen=no', 'toolbar=no', 'titlebar=no', 'menubar=no', 'location=no', `width=${LIVE_WINDOW_WIDTH}`, `height=${LIVE_WINDOW_HEIGHT}` ].join(','); window.open(chatUrl, '', features); } catch (error) { debugError('Failed to open popout:', error); } } function openLiveChat(url) { try { const urlObj = new URL(url); const href = urlObj.href; if (href.includes('www.youtube.com/watch') || href.includes('m.youtube.com/watch')) { const videoId = urlObj.searchParams.get('v'); if (videoId) { openPopout(`https://www.youtube.com/live_chat?is_popout=1&v=${videoId}`); } } else if (href.match(/https:\/\/.*?\.twitch\.tv\//)) { openPopout(`https://www.twitch.tv/popout${urlObj.pathname}/chat?popout=`); } else if (href.match(/https:\/\/.*?\.nimo\.tv\//)) { try { const selector = `a[href="${urlObj.pathname}"] .nimo-player.n-as-full`; const element = document.querySelector(selector); if (element && element.id) { const streamId = element.id.replace('home-hot-', ''); openPopout(`https://www.nimo.tv/popout/chat/${streamId}`); } } catch (error) { debugError('Nimo.tv chat extraction failed:', error); } } } catch (error) { debugError('Live chat opener failed:', error); } } // === ACTION EXECUTION === function executeAction(targetUrl, actionType) { debugLog('Executing action:', actionType, 'for URL:', targetUrl); // Check if this is a DOWN action and confirmation is enabled if (actionType === settings.DOWN && settings.down_confirm) { const confirmed = confirm(`Confirm DOWN action (${actionType})?\n\nURL: ${targetUrl}\n\nClick OK to proceed or Cancel to abort.`); if (!confirmed) { debugLog('DOWN action cancelled by user'); return; } } let finalUrl = ''; let app = 'play'; let isHls = false; // Check HLS domains for (const domain of hlsdomainArray) { if (domain && (targetUrl.includes(domain) || document.domain.includes(domain))) { if (actionType === 'stream') { targetUrl = targetUrl.replace(/^https?:/, 'hls:'); } isHls = true; break; } } // Handle different URL types if (targetUrl.startsWith('http') || targetUrl.startsWith('hls:')) { finalUrl = targetUrl; } else if (targetUrl.startsWith('mpv://')) { try { location.href = targetUrl; } catch (error) { debugError('Failed to navigate to mpv URL:', error); } return; } else { finalUrl = location.href; } // Process collected URLs let urlString = ''; if (collectedUrls.size > 0) { const urls = Array.from(collectedUrls.keys()); urlString = urls.join(' '); // Reset visual indicators collectedUrls.forEach((element) => { try { element.classList.remove('hh-collected-link'); } catch (error) { debugError('Failed to reset element class:', error); } }); collectedUrls.clear(); debugLog('Processed collected URLs:', urlString); } else { urlString = finalUrl; } // Determine app type and protocol action switch (actionType) { case 'pipe': app = 'mpvy'; // Pipe video stream to MPV with yt-dlp preprocessing break; case 'iptv': app = 'list'; // Add to IPTV playlist break; case 'stream': app = 'stream'; // Stream using streamlink break; case 'mpv': case 'vid': app = 'play'; // Direct play in MPV break; default: app = actionType; // Pass through custom actions } // Build final URL const encodedUrl = encodeUrl(urlString); const encodedReferer = encodeUrl(location.href); let protocolUrl = `mpv://${app}/${encodedUrl}/?referer=${encodedReferer}`; if (isHls) { protocolUrl += '&hls=1'; } debugLog('Action details:', { actionType, app, finalUrl, urlString, isHls, protocolUrl }); // Open live chat if needed if (actionType === 'stream' && settings.livechat) { openLiveChat(finalUrl); } debugLog('Final protocol URL:', protocolUrl); try { location.href = protocolUrl; } catch (error) { debugError('Failed to navigate to protocol URL:', error); } } // === DIRECTION CALCULATION === function getDirection(startX, startY, endX, endY) { const deltaX = endX - startX; const deltaY = endY - startY; debugLog('Direction calculation:', { start: [startX, startY], end: [endX, endY], delta: [deltaX, deltaY], threshold: DRAG_THRESHOLD }); // Check for center (no movement) if (Math.abs(deltaX) < DRAG_THRESHOLD && Math.abs(deltaY) < DRAG_THRESHOLD) { debugLog('Direction: CENTER (no movement)'); return DirectionEnum.CENTER; } let direction; if (settings.total_direction === 4) { // 4-direction mode if (Math.abs(deltaX) > Math.abs(deltaY)) { direction = deltaX > 0 ? DirectionEnum.RIGHT : DirectionEnum.LEFT; } else { direction = deltaY > 0 ? DirectionEnum.DOWN : DirectionEnum.UP; } debugLog('4-direction mode, result:', direction, direction === DirectionEnum.UP ? '(UP)' : direction === DirectionEnum.DOWN ? '(DOWN)' : direction === DirectionEnum.LEFT ? '(LEFT)' : '(RIGHT)'); } else { // 8-direction mode if (deltaX === 0) { direction = deltaY > 0 ? DirectionEnum.DOWN : DirectionEnum.UP; debugLog('8-direction mode, vertical movement, result:', direction); } else { const slope = deltaY / deltaX; const absSlope = Math.abs(slope); if (absSlope < 0.4142) { // ~22.5 degrees direction = deltaX > 0 ? DirectionEnum.RIGHT : DirectionEnum.LEFT; } else if (absSlope > 2.4142) { // ~67.5 degrees direction = deltaY > 0 ? DirectionEnum.DOWN : DirectionEnum.UP; } else { // Diagonal directions if (deltaX > 0) { direction = deltaY > 0 ? DirectionEnum.DOWN_RIGHT : DirectionEnum.UP_RIGHT; } else { direction = deltaY > 0 ? DirectionEnum.DOWN_LEFT : DirectionEnum.UP_LEFT; } } debugLog('8-direction mode, slope:', slope, 'result:', direction); } } return direction; } // === DRAG HANDLING === function attachDragHandler(element) { if (!element || attachedElements.has(element)) return; attachedElements.add(element); // Make sure elements are draggable - optimized version if (element === document) { // Use a single observer with throttling let observerTimeout; const observer = new MutationObserver(function (mutations) { // Throttle observer updates to prevent excessive processing if (observerTimeout) return; observerTimeout = setTimeout(() => { observerTimeout = null; processMutations(mutations); }, 100); // 100ms throttle }); observer.observe(document, { childList: true, subtree: true }); // Initial setup makeLinksDraggable(); // Use event delegation for drag events let dragState = null; document.addEventListener('dragstart', function (event) { const link = getDraggableLink(event.target); if (!link) return; debugLog('🚀 Drag started on element:', event.target.tagName, link.href || event.target.src); dragState = { startX: event.clientX, startY: event.clientY, target: link }; // Prevent default drag behavior for non-draggable elements if (!event.target.draggable) { event.preventDefault(); return; } }, { passive: true }); document.addEventListener('dragend', function (event) { if (!dragState) return; debugLog('🏁 Drag ended'); const endX = event.clientX; const endY = event.clientY; const direction = getDirection(dragState.startX, dragState.startY, endX, endY); debugLog(`🎯 Final drag direction: ${direction} (${dragState.startX},${dragState.startY} -> ${endX},${endY})`); debugLog('Current settings:', settings); const targetHref = dragState.target.href || dragState.target.src; if (!targetHref) { debugWarn('❌ No href or src found on target element'); dragState = null; return; } debugLog('🔗 Target URL:', targetHref); // Execute action based on direction switch (direction) { case DirectionEnum.RIGHT: debugLog('➡️ Executing RIGHT action:', settings.RIGHT); executeAction(targetHref, settings.RIGHT); break; case DirectionEnum.LEFT: debugLog('⬅️ Executing LEFT action:', settings.LEFT); executeAction(targetHref, settings.LEFT); break; case DirectionEnum.UP: debugLog('⬆️ Executing UP action:', settings.UP); executeAction(targetHref, settings.UP); break; case DirectionEnum.DOWN: debugLog('⬇️ Executing DOWN action:', settings.DOWN); executeAction(targetHref, settings.DOWN); break; case DirectionEnum.UP_LEFT: debugLog('↖️ Executing UP_LEFT action: list'); executeAction(targetHref, 'list'); break; default: debugLog('❓ Direction not mapped to action:', direction); } dragState = null; }, { passive: true }); } } // Helper function to find draggable link from event target function getDraggableLink(element) { if (!element) return null; let current = element; while (current && current !== document) { if (current.tagName === 'A' && current.href) { return current; } current = current.parentElement; } return null; } // Optimized function to make links draggable function makeLinksDraggable() { // Use a single query and cache the result const links = document.querySelectorAll('a[href]:not([draggable="true"])'); links.forEach(link => { link.draggable = true; debugLog('Made link draggable:', link.href); }); } // === RIGHT-CLICK COLLECTION === function setupRightClickCollection() { let mouseIsDown = false; let isHeld = false; document.addEventListener('mousedown', function (event) { const link = getParentByTagName(event.target, 'A'); if (!link) return; mouseIsDown = true; // Cleanup listeners const handleMouseUp = function () { mouseIsDown = false; document.removeEventListener('mouseup', handleMouseUp); }; const handleContextMenu = function (contextEvent) { if (isHeld) { contextEvent.preventDefault(); isHeld = false; } document.removeEventListener('contextmenu', handleContextMenu); }; document.addEventListener('mouseup', handleMouseUp, { once: true }); document.addEventListener('contextmenu', handleContextMenu, { once: true }); // Handle right-click if (event.button === 2) { setTimeout(function () { if (mouseIsDown) { toggleUrlCollection(link, event.target); mouseIsDown = false; isHeld = true; } }, RIGHT_CLICK_DELAY); } }); } function toggleUrlCollection(link, target) { if (!link.href) return; if (collectedUrls.has(link.href)) { // Remove from collection const element = collectedUrls.get(link.href); try { element.classList.remove('hh-collected-link'); } catch (error) { debugError('Failed to remove collected link class:', error); } collectedUrls.delete(link.href); debugLog('Removed URL from collection:', link.href); } else { // Add to collection try { target.classList.add('hh-collected-link'); } catch (error) { debugError('Failed to add collected link class:', error); } collectedUrls.set(link.href, target); debugLog('Added URL from collection:', link.href); } debugLog('Current collection size:', collectedUrls.size); } // === SHADOW DOM AND SPECIAL FEATURES === function handleShadowRoots() { document.addEventListener('mouseover', function (event) { if (event.target.shadowRoot && !attachedElements.has(event.target)) { attachDragHandler(event.target.shadowRoot); } }); } function setupYouTubeFeatures() { const domain = document.domain; if (domain !== 'www.youtube.com' && domain !== 'm.youtube.com') return; const firstChar = (location.host || '').charAt(0); if (firstChar === 'w') { addYouTubeMenuCommand( "Switch to YouTube Mobile (Persistent)", "https://m.youtube.com/?persist_app=1&app=m", true ); addYouTubeMenuCommand( "Switch to YouTube Mobile (Temporary)", "https://m.youtube.com/?persist_app=0&app=m", false ); } else if (firstChar === 'm') { addYouTubeMenuCommand( "Switch to YouTube Desktop (Persistent)", "https://www.youtube.com/?persist_app=1&app=desktop", true ); addYouTubeMenuCommand( "Switch to YouTube Desktop (Temporary)", "https://www.youtube.com/?persist_app=0&app=desktop", false ); // Mobile layout improvements try { GM_addStyle(` ytm-rich-item-renderer { width: 33% !important; margin: 1px !important; padding: 0px !important; } `); } catch (error) { debugError('Failed to apply YouTube mobile styles:', error); } } } // === CLEANUP ON PAGE UNLOAD === function cleanup() { // Clear collections to free memory collectedUrls.clear(); // Remove any visual indicators document.querySelectorAll('.hh-collected-link').forEach(el => { el.classList.remove('hh-collected-link'); }); debugLog('Handlers Helper cleanup completed'); } // Add cleanup on page unload window.addEventListener('beforeunload', cleanup); // === INITIALIZATION === function initialize() { try { setupMenuCommands(); attachDragHandler(document); // Use standard drag handler setupRightClickCollection(); handleShadowRoots(); setupYouTubeFeatures(); debugLog('Handlers Helper (Improved) initialized successfully'); debugLog('Settings validated and loaded'); } catch (error) { debugError('Initialization failed:', error); } } // Start the script if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initialize); } else { initialize(); }
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址