// ==UserScript== // @name YouTube Recommendation History (YouTube推荐内容回溯) // @namespace https://github.com/Kibidango086/YouTube-Recommendation-History/ // @version 1.1 // @description Save and review YouTube homepage recommendations to prevent losing interesting videos after an accidental refresh.(保存和回溯YouTube首页推荐内容,防止误点刷新后丢失想看的视频推荐)(用Claude写的) // @author Kibidang086 // @license MIT // @match https://www.youtube.com/ // @grant none // @downloadURL none // ==/UserScript== (function() { 'use strict'; // 存储推荐历史的数组 let recommendationHistory = []; let currentIndex = -1; let isRestoring = false; // 存储键名 const STORAGE_KEY = 'youtube_recommendation_history'; const INDEX_KEY = 'youtube_current_index'; // 创建前进按钮 function createForwardButton() { const button = document.createElement('button'); button.innerHTML = `往后 next`; button.id = 'youtube-forward-btn'; applyButtonStyles(button); return button; } // 创建后退按钮 function createBackwardButton() { const button = document.createElement('button'); button.innerHTML = `往前 prev`; button.id = 'youtube-backward-btn'; applyButtonStyles(button); return button; } // 应用按钮样式 function applyButtonStyles(button) { const isDark = isDarkMode(); const lightTheme = { background: '#f1f1f1', border: '#d3d3d3', color: '#0f0f0f', hoverBg: '#e5e5e5', hoverBorder: '#c6c6c6', activeBg: '#d9d9d9' }; const darkTheme = { background: '#303030', border: '#4a4a4a', color: '#ffffff', hoverBg: '#3a3a3a', hoverBorder: '#5a5a5a', activeBg: '#2a2a2a' }; const theme = isDark ? darkTheme : lightTheme; button.style.cssText = ` display: flex; align-items: center; gap: 8px; background: ${theme.background}; border: 1px solid ${theme.border}; border-radius: 18px; padding: 8px 16px; font-size: 14px; font-weight: 500; color: ${theme.color}; cursor: pointer; transition: all 0.2s ease; font-family: "Roboto", sans-serif; white-space: nowrap; margin: 0 3px; `; // 移除旧事件,避免重复绑定(可选) button.onmouseenter = () => { button.style.background = theme.hoverBg; button.style.borderColor = theme.hoverBorder; }; button.onmouseleave = () => { button.style.background = theme.background; button.style.borderColor = theme.border; }; button.onmousedown = () => { button.style.background = theme.activeBg; }; button.onmouseup = () => { button.style.background = theme.hoverBg; }; } // 从localStorage加载历史记录 function loadHistoryFromStorage() { try { const stored = localStorage.getItem(STORAGE_KEY); const storedIndex = localStorage.getItem(INDEX_KEY); if (stored) { recommendationHistory = JSON.parse(stored); console.log('从存储加载历史记录:', recommendationHistory.length, '条'); } if (storedIndex !== null) { currentIndex = parseInt(storedIndex); console.log('当前索引:', currentIndex); } } catch (error) { console.error('加载历史记录失败:', error); recommendationHistory = []; currentIndex = -1; } } // 保存历史记录到localStorage function saveHistoryToStorage() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(recommendationHistory)); localStorage.setItem(INDEX_KEY, currentIndex.toString()); } catch (error) { console.error('保存历史记录失败:', error); } } // 清理过期的历史记录 function cleanupOldHistory() { const now = Date.now(); const maxAge = 60 * 60 * 1000; // 1小时 recommendationHistory = recommendationHistory.filter(item => { return (now - item.timestamp) < maxAge; }); // 调整currentIndex if (currentIndex >= recommendationHistory.length) { currentIndex = recommendationHistory.length - 1; } saveHistoryToStorage(); } // 获取推荐数据 function getRecommendationData() { const videos = []; const videoElements = document.querySelectorAll('ytd-rich-item-renderer, ytd-video-renderer, ytd-compact-video-renderer'); videoElements.forEach(element => { try { // 标题 const titleElement = element.querySelector('#video-title, h3 a, .ytd-video-meta-block h3 a, #video-title-link'); // 链接 const linkElement = element.querySelector('a#thumbnail, a#video-title, #video-title-link, a[href*="/watch"]'); // 缩略图 const thumbnailElement = element.querySelector('img, ytd-thumbnail img, .ytd-thumbnail img'); // 频道名称 const channelElement = element.querySelector( '#channel-name a, ' + '.ytd-channel-name a, ' + 'ytd-channel-name a, ' + '#metadata a[href*="/channel"], ' + '#metadata a[href*="/@"], ' + '.ytd-video-meta-block #metadata a' ); // 视频时长 const durationElement = element.querySelector( 'ytd-thumbnail-overlay-time-status-renderer #text, ' + '.ytd-thumbnail-overlay-time-status-renderer #text, ' + 'span.ytd-thumbnail-overlay-time-status-renderer, ' + '.video-time, ' + 'ytd-thumbnail-overlay-time-status-renderer span' ); // 观看次数和上传时间(通常在metadata中) const metadataElements = element.querySelectorAll( '#metadata-line span, ' + '.ytd-video-meta-block #metadata-line span, ' + '#metadata span, ' + '.ytd-video-meta-block span:not([id]), ' + 'ytd-video-meta-block span' ); if (titleElement && linkElement) { // 提取观看次数和上传时间 let viewCount = ''; let uploadTime = ''; // 从metadata元素中提取信息 metadataElements.forEach(span => { const text = span.textContent?.trim() || ''; if (text) { // 检查是否是观看次数(包含"次观看"、"views"等) if (text.includes('次观看') || text.includes('views') || text.includes('次播放') || /^\d+[\d,]*\s*(次|views)/i.test(text)) { if (!viewCount) viewCount = text; } // 检查是否是上传时间(包含时间相关词汇) else if (text.includes('前') || text.includes('ago') || text.includes('天') || text.includes('小时') || text.includes('分钟') || text.includes('秒') || text.includes('年') || text.includes('月') || text.includes('周') || /\d+(天|小时|分钟|秒|年|月|周|hours?|days?|minutes?|seconds?|years?|months?|weeks?)/i.test(text)) { if (!uploadTime) uploadTime = text; } } }); // 如果上面的方法没有找到,尝试其他选择器 if (!viewCount || !uploadTime) { const additionalMetadata = element.querySelectorAll( 'span:not([id]):not([class*="button"]):not([class*="icon"]), ' + 'div:not([id]):not([class*="button"]):not([class*="icon"]) span' ); additionalMetadata.forEach(span => { const text = span.textContent?.trim() || ''; if (text && text.length > 0 && text.length < 50) { // 避免太长的文本 if (!viewCount && (text.includes('次观看') || text.includes('views') || /^\d+[\d,]*\s*(次|views)/i.test(text))) { viewCount = text; } if (!uploadTime && (text.includes('前') || text.includes('ago') || /\d+(天|小时|分钟|秒|年|月|周)/i.test(text))) { uploadTime = text; } } }); } const videoData = { title: titleElement.textContent?.trim() || '', url: linkElement.href || '', thumbnail: thumbnailElement?.src || thumbnailElement?.getAttribute('data-src') || '', channel: channelElement?.textContent?.trim() || '', duration: durationElement?.textContent?.trim() || '', viewCount: viewCount, uploadTime: uploadTime, element: element.outerHTML }; // 调试输出 if (videoData.title) { console.log('提取到视频数据:', { title: videoData.title.substring(0, 30) + '...', duration: videoData.duration, viewCount: videoData.viewCount, uploadTime: videoData.uploadTime, channel: videoData.channel }); } videos.push(videoData); } } catch (error) { console.log('提取视频数据时出错:', error); } }); console.log(`总共提取到 ${videos.length} 个视频`); return videos; } // 保存当前推荐内容 function saveCurrentRecommendations() { if (isRestoring) return; const recommendations = getRecommendationData(); if (recommendations.length > 0) { // 检查是否与最新的历史记录相同(避免重复保存) if (recommendationHistory.length > 0) { const latest = recommendationHistory[recommendationHistory.length - 1]; const currentUrls = recommendations.map(r => r.url).sort(); const latestUrls = latest.data.map(r => r.url).sort(); if (JSON.stringify(currentUrls) === JSON.stringify(latestUrls)) { console.log('推荐内容未变化,跳过保存'); return; } } // 如果当前不在历史末尾,删除后面的历史 if (currentIndex < recommendationHistory.length - 1) { recommendationHistory = recommendationHistory.slice(0, currentIndex + 1); } recommendationHistory.push({ timestamp: Date.now(), data: recommendations, url: window.location.href }); // 限制历史记录数量 if (recommendationHistory.length > 10) { recommendationHistory.shift(); } else { currentIndex++; } // 保存到localStorage saveHistoryToStorage(); updateButtonStates(); console.log('已保存推荐内容,当前历史数量:', recommendationHistory.length); } } // 检测当前主题模式 function isDarkMode() { // 获取页面的主容器元素 const el = document.querySelector('ytd-app') || document.body; const bgColor = window.getComputedStyle(el).backgroundColor; // 解析背景颜色为 RGB const rgb = bgColor.match(/\d+/g); if (!rgb || rgb.length < 3) return false; // 将 RGB 转为亮度值(YIQ公式,近似反映人眼感受) const [r, g, b] = rgb.map(Number); const brightness = (r * 299 + g * 587 + b * 114) / 1000; return brightness < 128; // 亮度低于128视为深色模式 } // 创建自定义视频卡片 function createVideoCard(video, isDark) { const card = document.createElement('div'); const cardBg = isDark ? '#0f0f0f' : '#ffffff'; const textColor = isDark ? '#f1f1f1' : '#0f0f0f'; const secondaryTextColor = isDark ? '#aaaaaa' : '#606060'; const hoverBg = isDark ? '#272727' : '#f8f8f8'; card.style.cssText = ` background: ${cardBg}; border-radius: 12px; overflow: hidden; cursor: pointer; transition: all 0.2s ease; margin-bottom: 16px; box-shadow: ${isDark ? '0 1px 3px rgba(255,255,255,0.1)' : '0 1px 3px rgba(0,0,0,0.1)'}; `; // 提取视频ID用于缩略图 const videoId = extractVideoId(video.url); const thumbnailUrl = videoId ? `https://img.youtube.com/vi/${videoId}/0.jpg` : video.thumbnail; // 格式化视频时长显示 const durationDisplay = video.duration || ''; // 格式化上传时间和观看次数 const uploadTimeDisplay = video.uploadTime || ''; const viewCountDisplay = video.viewCount || ''; // 构建元数据行 let metadataLine = ''; if (viewCountDisplay && uploadTimeDisplay) { metadataLine = `${viewCountDisplay} • ${uploadTimeDisplay}`; } else if (viewCountDisplay) { metadataLine = viewCountDisplay; } else if (uploadTimeDisplay) { metadataLine = uploadTimeDisplay; } else { metadataLine = '推荐视频'; } card.innerHTML = `