// ==UserScript== // @name YouTube 综合悬浮播放器 // @namespace https://lele1894.tk/ // @version 1.0 // @description 在YouTube视频和Shorts缩略图上添加悬浮播放按钮,支持在lele1894.tk网站播放 // @author lele1894 // @match https://www.youtube.com/* // @grant none // @license MIT // @homepageURL https://lele1894.tk/ // @supportURL https://github.com/lele1894/YouTube-Comprehensive-Floating-Player/issues // @downloadURL none // ==/UserScript== (function() { 'use strict'; // 可配置的基础URL变量 const baseUrl = 'https://lele1894.tk/1.html'; // 创建悬浮播放器样式 const style = document.createElement('style'); style.textContent = ` .float-player-container { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 9999; background: rgba(0, 0, 0, 0.9); padding: 10px; border-radius: 8px; display: none; box-shadow: 0 0 10px rgba(0,0,0,0.5); } .player-container { position: relative; width: 100%; height: 100%; } .player-container iframe { width: 100%; height: 100%; display: block; border: none; } .custom-play-btn { position: absolute; top: 8px; left: 8px; background: rgba(0, 0, 0, 0.7); color: white; border: none; width: 24px; height: 24px; border-radius: 4px; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; z-index: 100; opacity: 0; transition: opacity 0.2s; pointer-events: auto; } /* 悬浮显示按钮 */ .thumbnail-container:hover .custom-play-btn, #thumbnail-container:hover .custom-play-btn, .shorts-container:hover .custom-play-btn { opacity: 1; } .thumbnail-btn { position: absolute; top: 8px; left: 40px; background: rgba(0, 0, 0, 0.7); color: white; border: none; width: 24px; height: 24px; border-radius: 4px; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; z-index: 100; opacity: 0; transition: opacity 0.2s; pointer-events: auto; } .new-window-btn { position: absolute; top: 8px; left: 72px; background: rgba(0, 0, 0, 0.7); color: white; border: none; width: 24px; height: 24px; border-radius: 4px; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; z-index: 100; opacity: 0; transition: opacity 0.2s; pointer-events: auto; } /* 悬浮显示按钮 */ .thumbnail-container:hover .thumbnail-btn, #thumbnail-container:hover .thumbnail-btn, .shorts-container:hover .new-window-btn { opacity: 1; } .thumbnail-container:hover .new-window-btn, #thumbnail-container:hover .new-window-btn { opacity: 1; } /* 订阅页面特殊布局修复 */ ytd-browse[page-subtype="subscriptions"] .custom-play-btn, ytd-browse[page-subtype="subscriptions"] .thumbnail-btn, ytd-browse[page-subtype="subscriptions"] .new-window-btn { position: absolute; top: 4px; z-index: 101; } ytd-browse[page-subtype="subscriptions"] .thumbnail-btn { left: 32px; } ytd-browse[page-subtype="subscriptions"] .new-window-btn { left: 64px; } /* 防止按钮影响布局 */ .custom-play-btn, .thumbnail-btn, .new-window-btn { box-sizing: border-box; transform: none; margin: 0; padding: 0; } /* 确保缩略图容器相对定位 */ .thumbnail-container, .shorts-container { position: relative !important; } /* 针对不同页面的特殊处理 */ ytd-rich-item-renderer .thumbnail-container, ytd-video-renderer .thumbnail-container, ytd-compact-video-renderer .thumbnail-container { position: relative !important; overflow: visible !important; } .thumbnail-preview { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0, 0, 0, 0.9); padding: 20px; border-radius: 8px; z-index: 10000; display: none; width: 90vw; max-height: 90vh; overflow-y: auto; } .preview-header { color: white; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid rgba(255,255,255,0.2); text-align: center; } .preview-title { font-size: 16px; margin-bottom: 8px; } .preview-desc { font-size: 13px; color: #aaa; line-height: 1.4; } .thumbnail-list { display: flex; justify-content: center; gap: 20px; flex-wrap: nowrap; padding: 0 20px; } .thumbnail-item { position: relative; cursor: pointer; background: rgba(255, 255, 255, 0.1); border-radius: 8px; overflow: hidden; flex: 1; max-width: 400px; transition: transform 0.2s; } .thumbnail-item:hover { transform: scale(1.02); } .thumbnail-item img { width: 100%; height: auto; display: none; } .resolution { position: absolute; bottom: 10px; left: 10px; background: rgba(0, 0, 0, 0.7); color: white; padding: 4px 8px; border-radius: 4px; font-size: 13px; } .close-preview { position: absolute; top: 15px; right: 15px; color: white; cursor: pointer; font-size: 24px; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; background: rgba(255,255,255,0.1); border-radius: 50%; transition: background 0.2s; } .close-preview:hover { background: rgba(255,255,255,0.2); } .player-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 9998; display: none; } `; document.head.appendChild(style); // 创建悬浮播放器容器 const playerContainer = document.createElement('div'); playerContainer.className = 'float-player-container'; document.body.appendChild(playerContainer); const overlay = document.createElement('div'); overlay.className = 'player-overlay'; document.body.appendChild(overlay); // 添加封面图预览容器 const previewContainer = document.createElement('div'); previewContainer.className = 'thumbnail-preview'; document.body.appendChild(previewContainer); // 获取视频封面图URL列表 const getThumbnailUrls = (videoId) => { return [ { url: `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`, resolution: 'HD (1280x720)' }, { url: `https://img.youtube.com/vi/${videoId}/sddefault.jpg`, resolution: 'SD (640x480)' }, { url: `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`, resolution: 'HQ (480x360)' }, { url: `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`, resolution: 'MQ (320x180)' } ]; }; // 下载图片函数 const downloadImage = async (url, filename) => { try { const response = await fetch(url); const blob = await response.blob(); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = filename; link.click(); URL.revokeObjectURL(link.href); } catch (error) { console.error('下载失败:', error); } }; // 监听页面变化,为视频加播放按钮 const observer = new MutationObserver((mutations) => { // 检测当前页面类型 const isSubscriptionPage = window.location.href.includes('/feed/subscriptions'); const isHomePage = window.location.pathname === '/' || window.location.href.includes('youtube.com/'); // 如果是订阅页面,使用更谨慎的方式 if (isSubscriptionPage) { console.log('检测到订阅页面,使用特殊处理模式...'); } else { console.log('DOM变化检测到,开始查找视频元素...'); } // 处理普通YouTube视频 processRegularVideos(isSubscriptionPage); // 处理YouTube Shorts processShortsVideos(); }); // 处理普通YouTube视频 function processRegularVideos(isSubscriptionPage) { // 扩展选择器以支持新版YouTube的各种视频元素 let videoSelectors; if (isSubscriptionPage) { // 订阅页面的特殊选择器,更加精确 videoSelectors = [ 'ytd-rich-item-renderer a#video-title-link', 'ytd-rich-item-renderer h3 a[href*="/watch"]', 'ytd-rich-item-renderer .ytd-rich-grid-media a[href*="/watch"]', '#video-title-link' ]; } else { // 其他页面的全面选择器 videoSelectors = [ '#video-title', 'ytd-rich-item-renderer a#video-title-link', 'ytd-video-renderer a#video-title-link', 'ytd-grid-video-renderer a#video-title-link', 'ytd-compact-video-renderer a#video-title-link', 'ytd-rich-item-renderer h3 a[href*="/watch"]', 'ytd-video-renderer h3 a[href*="/watch"]', 'ytd-rich-item-renderer .ytd-rich-grid-media a[href*="/watch"]', 'a[href*="/watch?v="]:not([data-processed])', '#video-title-link', '.ytd-rich-grid-media #video-title', '.ytd-rich-grid-media #video-title-link' ]; } let videos = []; videoSelectors.forEach(selector => { const elements = document.querySelectorAll(selector); elements.forEach(el => { if (!videos.includes(el) && el.href && el.href.includes('/watch')) { videos.push(el); } }); }); videos.forEach((video, index) => { if (!video.dataset.hasCustomButton) { video.dataset.hasCustomButton = 'true'; // 扩展缩略图容器查找逻辑以支持新版YouTube let thumbnailContainer = findThumbnailContainer(video, isSubscriptionPage); // 检查缩略图容器是否具有aria-hidden属性,如果有则需要找到合适的父容器 let buttonContainer = thumbnailContainer; if (thumbnailContainer && thumbnailContainer.getAttribute('aria-hidden') === 'true') { // 如果缩略图容器有aria-hidden=true,则查找合适的父容器 let parent = thumbnailContainer.parentElement; while (parent && parent !== document.body) { if (parent.getAttribute('aria-hidden') !== 'true') { buttonContainer = parent; break; } parent = parent.parentElement; } } if (buttonContainer) { // 确保按钮容器有正确的样式 if (!buttonContainer.classList.contains('thumbnail-container')) { buttonContainer.classList.add('thumbnail-container'); } // 设置 ID 以兼容原有CSS if (!buttonContainer.id) { buttonContainer.id = 'thumbnail-container'; } buttonContainer.style.position = 'relative'; buttonContainer.style.overflow = 'visible'; // 检查是否已经有按钮了 if (buttonContainer.querySelector('.custom-play-btn, .thumbnail-btn, .new-window-btn')) { return; } // 添加播放按钮 addPlayButton(buttonContainer, video, isSubscriptionPage); // 添加封面图按钮 addThumbnailButton(buttonContainer, video, isSubscriptionPage); // 添加新窗口按钮 addNewWindowButton(buttonContainer, video, isSubscriptionPage); } } }); } // 处理YouTube Shorts function processShortsVideos() { // 查找所有Shorts缩略图元素 const shortsThumbnails = document.querySelectorAll(` a[href^="/shorts/"]:not([data-popup-added]) `); shortsThumbnails.forEach(thumbnail => { // 确保缩略图包含图片元素,避免选择到标题链接 const hasImage = thumbnail.querySelector('img'); // 同时检查是否在特定的视频容器内 const isInVideoContainer = thumbnail.closest('ytd-rich-item-renderer, ytd-video-renderer, ytd-grid-video-renderer, ytd-compact-video-renderer, ytd-reel-video-renderer'); // 如果有图片或者在视频容器内,则添加按钮 if (!thumbnail.querySelector('.custom-play-btn') && (hasImage || isInVideoContainer)) { thumbnail.setAttribute('data-popup-added', 'true'); // 添加容器用于正确定位按钮 const container = document.createElement('div'); container.className = 'shorts-container'; container.style.position = 'relative'; // 将缩略图包裹在容器中 thumbnail.parentNode.insertBefore(container, thumbnail); container.appendChild(thumbnail); // 添加播放按钮 addShortsPlayButton(container, thumbnail); // 添加新窗口按钮 addShortsNewWindowButton(container, thumbnail); } }); } // 查找缩略图容器 function findThumbnailContainer(video, isSubscriptionPage) { let thumbnailContainer = null; if (isSubscriptionPage) { // 订阅页面的特殊处理 const richItem = video.closest('ytd-rich-item-renderer'); if (richItem) { // 直接查找 ytd-thumbnail 元素 thumbnailContainer = richItem.querySelector('ytd-thumbnail'); // 如果没找到,尝试查找 a 标签包含的缩略图 if (!thumbnailContainer) { const thumbnailLink = richItem.querySelector('a[href*="/watch"] img'); if (thumbnailLink) { thumbnailContainer = thumbnailLink.closest('a'); } } } } else { // 其他页面的原有处理逻辑 // 方法 1: 传统方式 - 查找父级容器 const videoRenderer = video.closest('ytd-rich-item-renderer, ytd-compact-video-renderer, ytd-video-renderer, ytd-grid-video-renderer'); if (videoRenderer) { const thumbnailSelectors = [ '#thumbnail', 'ytd-thumbnail', '.ytd-thumbnail', 'ytd-thumbnail a', '.ytd-thumbnail a', '[id="thumbnail"]', 'a[href*="/watch"] img' ]; for (const selector of thumbnailSelectors) { thumbnailContainer = videoRenderer.querySelector(selector); if (thumbnailContainer) { if (thumbnailContainer.tagName === 'IMG') { thumbnailContainer = thumbnailContainer.closest('a') || thumbnailContainer.parentElement; } break; } } } // 方法 2: 直接查找父级元素 if (!thumbnailContainer) { let parent = video.parentElement; let attempts = 0; while (parent && !thumbnailContainer && attempts < 10) { const selectors = ['#thumbnail', 'ytd-thumbnail', '.ytd-thumbnail', 'a[href*="/watch"] img']; for (const selector of selectors) { const found = parent.querySelector(selector); if (found) { thumbnailContainer = found.tagName === 'IMG' ? found.closest('a') || found.parentElement : found; break; } } if (!thumbnailContainer) { parent = parent.parentElement; attempts++; if (parent && (parent === document.body || parent.tagName === 'HTML')) break; } } } // 方法 3: 通过兄弟元素查找 if (!thumbnailContainer) { const container = video.closest('div, ytd-rich-item-renderer, ytd-video-renderer'); if (container) { const selectors = ['#thumbnail', 'ytd-thumbnail', '.ytd-thumbnail', 'img[src*="ytimg.com"]']; for (const selector of selectors) { const found = container.querySelector(selector); if (found) { thumbnailContainer = found.tagName === 'IMG' ? found.closest('a') || found.parentElement : found; break; } } } } // 方法 4: 如果还是找不到,尝试查找任何包含缩略图的元素 if (!thumbnailContainer) { const richItem = video.closest('ytd-rich-item-renderer'); if (richItem) { const imgs = richItem.querySelectorAll('img'); for (const img of imgs) { if (img.src && (img.src.includes('ytimg.com') || img.src.includes('youtube.com'))) { thumbnailContainer = img.closest('a') || img.parentElement; break; } } } } } return thumbnailContainer; } // 添加播放按钮(普通视频) function addPlayButton(container, video, isSubscriptionPage) { const playButton = document.createElement('button'); playButton.className = 'custom-play-btn'; playButton.textContent = '弹'; // 防止布局破坏的样式设置 playButton.style.cssText = ` position: absolute !important; top: ${isSubscriptionPage ? '4px' : '8px'} !important; left: 8px !important; background: rgba(0, 0, 0, 0.7) !important; color: white !important; border: none !important; width: 24px !important; height: 24px !important; border-radius: 4px !important; cursor: pointer !important; font-size: 14px !important; display: flex !important; align-items: center !important; justify-content: center !important; z-index: 101 !important; opacity: 0 !important; transition: opacity 0.2s !important; pointer-events: auto !important; margin: 0 !important; padding: 0 !important; box-sizing: border-box !important; transform: none !important; `; // 添加悬浮显示事件 container.addEventListener('mouseenter', () => { playButton.style.opacity = '1'; }); container.addEventListener('mouseleave', () => { playButton.style.opacity = '0'; }); playButton.onclick = (e) => { e.preventDefault(); e.stopPropagation(); let videoId = extractVideoId(video); // 在当前页面打开播放器页面并传递视频ID参数 if (videoId) { // 构造播放器URL并传递视频ID参数 const playerUrl = baseUrl + '?v=' + videoId; // 清空播放器容器 playerContainer.innerHTML = ''; // 创建iframe元素 const iframe = document.createElement('iframe'); iframe.src = playerUrl; iframe.width = '100%'; iframe.height = '100%'; iframe.frameBorder = '0'; iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen'; iframe.allowFullscreen = true; iframe.loading = 'eager'; // 添加加载事件 iframe.onload = function() { console.log('播放器页面加载成功,URL: ' + playerUrl); }; iframe.onerror = function() { console.error('播放器页面加载失败,URL: ' + playerUrl); // 显示错误信息 playerContainer.innerHTML = `
无法加载播放器页面
URL: ${playerUrl}
请检查网络连接或稍后重试
无法加载播放器页面
URL: ${playerUrl}
请检查网络连接或稍后重试