// ==UserScript== // @name YouTube Analytics // @namespace http://tampermonkey.net/ // @version 1.1.4 // @description 获取7天前视频和Shorts的播放量统计并显示图表 // @author NIL // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @match https://www.youtube.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com // @require https://cdn.jsdelivr.net/npm/chart.js // ==/UserScript== (function () { 'use strict'; const API_BASE = 'https://www.googleapis.com/youtube/v3'; const DEFAULT_PARAMS = { part: 'snippet', maxResults: 20, order: 'date' }; // 注册菜单命令 GM_registerMenuCommand("设置 API KEY", () => { const currentApiKey = GM_getValue("apiKey", ""); const userInput = prompt("请输入 YouTube Data API 密钥", currentApiKey); if (userInput !== null) GM_setValue("apiKey", userInput); }); GM_registerMenuCommand("设置 CPM", () => { const currentCPM = GM_getValue("cpm", 20); const userInput = prompt("请输入CPM", currentCPM); if (userInput !== null) GM_setValue("cpm", Number(userInput)); }); let isMainRunning = false; async function main() { if (isMainRunning) return; isMainRunning = true; try { // 清理旧元素 document.querySelectorAll('#yt-analytics').forEach(el => el.remove()); const apiKey = GM_getValue("apiKey"); if (!apiKey) return alert('请先设置API密钥'); const channelId = await getChannelId(); if (!channelId) throw new Error('获取频道ID失败'); const results = await Promise.all([ processPlaylist(`UULF${channelId.slice(2)}`, apiKey), processPlaylist(`UUSH${channelId.slice(2)}`, apiKey) ]); showResults({ normal: results[0], shorts: results[1] }); } catch (error) { alert(`错误: ${error.message}`); console.error('运行错误:', error); } finally { isMainRunning = false; } } async function processPlaylist(playlistId, apiKey) { try { const rawItems = await fetchPlaylistItems(playlistId, apiKey); const filteredItems = filterOldVideos(rawItems, 7); console.log(filteredItems); if (filteredItems.length === 0) return null; const statsData = await fetchVideoStats(filteredItems, apiKey); const mergedData = mergeVideoData(filteredItems, statsData); // 通过播放列表ID前缀判断类型 const type = playlistId.startsWith('UUSH') ? 'shorts' : 'normal'; console.log(type, statsData); return { ...calculateMetrics(mergedData), type, dataPoints: mergedData }; } catch (error) { console.error(`处理播放列表 ${playlistId} 失败:`, error); return null; } } function fetchPlaylistItems(playlistId, apiKey) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: `${API_BASE}/playlistItems?${new URLSearchParams({ ...DEFAULT_PARAMS, playlistId, key: apiKey })}`, onload: res => { const data = JSON.parse(res.responseText); data.items ? resolve(data.items) : reject('无效的播放列表'); }, onerror: err => reject('播放列表请求失败') }); }); } function filterOldVideos(items, days) { const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - days); return items.filter(item => { const pubDate = new Date(item.snippet.publishedAt); return pubDate < cutoff; }).slice(0, 10); } function fetchVideoStats(items, apiKey) { const videoIds = items.map(item => item.snippet.resourceId.videoId); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: `${API_BASE}/videos?${new URLSearchParams({ part: 'statistics', id: videoIds.join(','), key: apiKey })}`, onload: res => { const data = JSON.parse(res.responseText); resolve(data.items || []); }, onerror: err => reject('统计请求失败') }); }); } function mergeVideoData(playlistItems, statsItems) { return playlistItems.map(item => { const stats = statsItems.find(s => s.id === item.snippet.resourceId.videoId); return { views: stats ? parseInt(stats.statistics.viewCount) : 0, date: new Date(item.snippet.publishedAt) }; }).sort((a, b) => a.date - b.date); // 按日期排序 } function calculateMetrics(dataPoints) { const validData = dataPoints.filter(d => d.views > 0); if (validData.length === 0) return null; const totalViews = validData.reduce((sum, d) => sum + d.views, 0); const avgViews = totalViews / validData.length; const cpm = GM_getValue("cpm", 20); return { avgViewsK: avgViews / 1000, earnings: (avgViews / 1000) * cpm, sampleSize: validData.length }; } function createErrorMessage(container) { const errorMsg = document.createElement('div'); errorMsg.style.color = '#ff4444'; errorMsg.style.padding = '10px'; errorMsg.style.textAlign = 'center'; errorMsg.textContent = '⚠️ 没有找到有效数据!'; container.appendChild(errorMsg); } function showResults({ normal, shorts }) { const container = document.createElement('div'); const isDarkTheme = document.documentElement.hasAttribute("dark") && document.documentElement.hasAttribute("darker-dark-theme"); let backgroundColor = `rgba(255, 255, 255, 0.8)`; let fontColor = `rgb(113, 113, 113)`; //检测是否为暗色模式 if (isDarkTheme === true) { backgroundColor = `rgba(71, 71, 71, 0.8)`; fontColor = `rgb(214, 206, 206)`; } container.id = 'yt-analytics'; container.style.cssText = ` position: absolute; top: 100px; right: 60px; background: ${backgroundColor}; backdrop-filter: blur(10px); padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.2); z-index: 2019; font-family: Roboto, sans-serif; font-size: 1.5rem; color: ${fontColor}; `; const contentWrapper = document.createElement('div'); contentWrapper.id = 'content-container' contentWrapper.style.display = 'flex'; contentWrapper.style.justifyContent = 'space-between'; // 显示统计数据 if (normal) createStatsSection('长视频', normal, contentWrapper); if (shorts) createStatsSection('Shorts', shorts, contentWrapper); if (!normal && !shorts) createErrorMessage(contentWrapper); // 添加图表容器 const chartsContainer = document.createElement('div'); chartsContainer.style.marginTop = '20px'; chartsContainer.style.display = 'flex'; if (normal) createChartContainer('normal', '长视频播放趋势', chartsContainer); if (shorts) createChartContainer('shorts', 'Shorts播放趋势', chartsContainer); container.appendChild(contentWrapper); container.appendChild(chartsContainer) document.body.appendChild(container); // 延迟渲染图表确保容器已存在 setTimeout(() => { if (normal) renderChart('chart-normal', normal.dataPoints, '#FF6384', fontColor); if (shorts) renderChart('chart-shorts', shorts.dataPoints, '#36A2EB', fontColor); }, 100); } function createStatsSection(titleText, data, container) { const section = document.createElement('div'); section.style.marginBottom = '20px'; section.style.paddingBottom = '15px'; const title = document.createElement('h3'); title.textContent = `${titleText} (${data.sampleSize}个)`; title.style.margin = '0 0 10px 0'; section.appendChild(title); const views = document.createElement('div'); views.textContent = `平均千次播放量: ${data.avgViewsK.toFixed(1)}k`; views.style.fontSize = '1.2rem' section.appendChild(views); const earnings = document.createElement('div'); earnings.style.color = '#FFA500'; earnings.style.marginTop = '8px'; earnings.textContent = `预估佣金: $${data.earnings.toFixed(2)}`; section.appendChild(earnings); container.appendChild(section); } function createChartContainer(type, title, container) { const chartSection = document.createElement('div'); chartSection.style.marginBottom = '25px'; const chartTitle = document.createElement('h4'); chartTitle.textContent = title; chartTitle.style.margin = '0 0 10px 0'; chartTitle.style.fontSize = '13px'; chartSection.appendChild(chartTitle); const canvas = document.createElement('canvas'); canvas.id = `chart-${type}`; canvas.style.maxHeight = '200px'; chartSection.appendChild(canvas); container.appendChild(chartSection); } function renderChart(canvasId, dataPoints, color, fontColor) { const ctx = document.getElementById(canvasId).getContext('2d'); new Chart(ctx, { type: 'line', data: { labels: dataPoints.map(d => formatDate(d.date)), datasets: [{ label: '播放量', data: dataPoints.map(d => d.views), borderColor: color, tension: 0.4, fill: false, pointRadius: 3, pointHitRadius: 10, pointHoverRadius: 6 }] }, options: { responsive: true, plugins: { legend: { display: false }, tooltip: { mode: 'nearest', intersect: false } }, interaction: { mode: 'nearest', intersect: false }, scales: { x: { grid: { display: false, color: fontColor, }, ticks: { color: fontColor, } }, y: { beginAtZero: true, grid: { color: fontColor, display: true, }, border: { display: false }, ticks: { color: fontColor, callback: value => value >= 1000 ? `${(value / 1000).toFixed(0)}k` : value } } } } }); } //格式化日期 function formatDate(date) { return new Date(date).toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' }).replace(/\//g, '-'); } function getChannelId() { return new Promise((resolve) => { const checkExist = setInterval(() => { const metaTag = document.querySelector('meta[itemprop="identifier"]'); if (metaTag) { clearInterval(checkExist); resolve(metaTag.getAttribute('content')); } }, 500); setTimeout(() => { clearInterval(checkExist); resolve(null); }, 10000); }); } let lastHref = window.location.href; setInterval(() => { const currentHref = window.location.href; if (currentHref !== lastHref) { lastHref = currentHref; document.querySelectorAll('#yt-analytics').forEach(el => el.remove()); } }, 1000); window.addEventListener('load', () => setTimeout(main, 2000)); })();