// ==UserScript== // @name YouTube Analytics // @namespace http://tampermonkey.net/ // @version 1.0.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 // ==/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); // 通过播放列表ID前缀判断类型 const type = playlistId.startsWith('UUSH') ? 'shorts' : 'normal'; console.log(type, statsData); return calculateMetrics(statsData, type); } 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 calculateMetrics(statsData, type) { const validItems = statsData.filter(item => item.statistics?.viewCount); if (validItems.length === 0) return null; const totalViews = validItems.reduce((sum, item) => sum + parseInt(item.statistics.viewCount), 0); const avgViews = totalViews / validItems.length; const cpm = GM_getValue("cpm", 20); return { type: type, // 使用传入的类型参数 avgViewsK: avgViews / 1000, earnings: (avgViews / 1000) * cpm, sampleSize: validItems.length }; } function showResults({ normal, shorts }) { const container = document.createElement('div'); container.id = 'yt-analytics'; // 保持原有样式 container.style.cssText = ` position: absolute; top: 250px; right: 60px; background: #fff; padding: 10px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.2); z-index: 2019; max-width: 300px; font-family: Roboto, sans-serif; font-size: 12px; `; // 创建标题 const title = document.createElement('h3'); title.textContent = '播放量分析'; container.appendChild(title); // 创建内容容器 const contentWrapper = document.createElement('div'); // 长视频数据 if (normal) { const normalSection = createSection( '长视频', normal.sampleSize, normal.avgViewsK, normal.earnings, 'orange' ); contentWrapper.appendChild(normalSection); } // 添加Shorts数据 if (shorts) { const shortsSection = createSection( 'Shorts', shorts.sampleSize, shorts.avgViewsK, shorts.earnings, 'orange' ); contentWrapper.appendChild(shortsSection); } // 无数据处理 if (!normal && !shorts) { const errorMsg = document.createElement('div'); errorMsg.style.color = 'red'; errorMsg.textContent = '没有有效数据'; contentWrapper.appendChild(errorMsg); } container.appendChild(contentWrapper); // 清理旧内容并插入新元素 document.querySelectorAll('#yt-analytics').forEach(el => el.remove()); document.body.appendChild(container); // 辅助函数:创建数据区块 function createSection(titleText, sampleSize, avgViews, earnings, color) { const section = document.createElement('div'); section.style.margin = '12px 0'; section.style.paddingBottom = '12px'; section.style.borderBottom = '1px solid #eee'; const title = document.createElement('h4'); title.textContent = `${titleText} (${sampleSize}个)`; section.appendChild(title); const views = document.createElement('div'); views.textContent = `平均千次播放量: ${avgViews.toFixed(1)}k`; section.appendChild(views); const earningsElem = document.createElement('div'); earningsElem.style.color = color; earningsElem.style.marginTop = '6px'; earningsElem.textContent = `预估佣金: $${earnings.toFixed(2)}`; section.appendChild(earningsElem); return section; } } 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; // 离开目标页面时清理元素 if (!currentHref.match(/https:\/\/www\.youtube\.com\/@.*/)) { document.querySelectorAll('#yt-analytics').forEach(el => el.remove()); } } }, 1000); window.addEventListener('load', () => setTimeout(main, 2000)); })();