TikTok 小助手
// ==UserScript==
// @name TikTok 小助手
// @namespace http://tampermonkey.net/
// @version 5.36
// @description 获取 TikTok 数据并显示精简用户界面!
// @connect *
// @author belugua
// @match *://*/*
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_getResourceText
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant GM_setClipboard
// @connect tiktok.com
// @icon https://iili.io/dy5xjOg.jpg
// @require https://code.jquery.com/jquery-3.6.0.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/toastify-js/1.12.0/toastify.min.js
// @resource TOASTIFY_CSS https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css
// ==/UserScript==
(function() {
'use strict';
console.log('Script loaded successfully');
// 加载 Toastify.js 的 CSS
GM_addStyle(GM_getResourceText('TOASTIFY_CSS'));
// 默认设置:页面加载时是否自动显示数据面板
let autoShowDataPanel = GM_getValue('autoShowDataPanel', false);
console.log('Initial autoShowDataPanel:', autoShowDataPanel);
// 注册菜单命令,切换是否自动显示数据面板
GM_registerMenuCommand('切换自动弹出数据面板', () => {
autoShowDataPanel = !autoShowDataPanel;
GM_setValue('autoShowDataPanel', autoShowDataPanel);
alert(`自动弹出数据面板已${autoShowDataPanel ? '启用' : '禁用'}`);
console.log('Toggled autoShowDataPanel to:', autoShowDataPanel);
});
$(document).ready(function() {
console.log('Document ready');
if (autoShowDataPanel) {
initializeInterface();
}
// 监听快捷键 Alt+N 来手动打开或关闭界面
$(document).on('keydown', function(e) {
if (e.altKey && e.key === 'g') {
console.log('Alt+G pressed');
toggleInterface();
}
});
});
function initializeInterface() {
console.log('Initializing interface');
if ($('#tiktok-data-ui').length) {
console.log('Interface already initialized');
return;
}
const container = $(
`<div id="tiktok-data-ui" style="
position:fixed;
top:20px;
right:-350px;
background: rgba(255, 255, 255, 0.7);
color:black;
border:1px solid #ccc;
padding:15px;
z-index:10000;
width:300px;
border-radius:10px;
backdrop-filter: blur(10px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
">
<div id="drag-handle" style="display: flex; justify-content: space-between; align-items: center; cursor: move;">
<h3 style="color:black; margin:0; text-align:center; width:100%;">TikTok 数据面板</h3>
</div>
<div id="input-area" style="margin-top:10px; width: 100%;">
<input type="text" id="tiktok-url-input" placeholder="输入 TikTok 视频或用户名" style="
width: 100%;
padding:5px;
margin: 0;
border: none;
border-radius:5px;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.8);
transition: all 0.3s ease;
outline: none;
color: black;
box-shadow: 0px 8px 15px rgba(72, 72, 72, 0.1);
" />
</div>
<div id="data-output" style="
margin-top:15px;
font-size:14px;
text-align: left;
width: 100%;
">暂无数据</div>
</div>`
);
$('body').append(container);
console.log('Appended interface container');
$('#tiktok-data-ui').animate({ right: '20px' }, 400);
// 使界面可拖动
dragElement(document.getElementById('tiktok-data-ui'), document.getElementById('drag-handle'));
$('#tiktok-url-input').on('keydown', function(e) {
if (e.key === 'Enter') {
let url = $(this).val().trim();
console.log('Entered URL:', url);
if (url) {
if (!url.startsWith('https://www.tiktok.com/')) {
if (!url.startsWith('@')) {
url = '@' + url;
}
url = `https://www.tiktok.com/${url}`;
console.log('Formatted URL:', url);
}
fetchDataFromUrl(url);
} else {
showNotification('请输入有效的 TikTok 视频或用户链接');
}
}
});
}
function toggleInterface() {
console.log('Toggling interface');
const container = $('#tiktok-data-ui');
if (container.length) {
container.remove();
console.log('Removed interface');
} else {
initializeInterface();
}
}
function fetchDataFromUrl(url) {
console.log('Fetching data from URL:', url);
$('#data-output').text('正在获取数据...');
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: {
'User-Agent': navigator.userAgent,
'Accept': 'text/html'
},
onload: function(response) {
console.log('Data fetched, status:', response.status);
if (response.status === 200) {
const responseText = response.responseText;
const scriptMatch = responseText.match(/<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__" type="application\/json">([\s\S]*?)<\/script>/);
if (scriptMatch) {
try {
const jsonData = JSON.parse(scriptMatch[1]);
console.log('JSON data parsed successfully');
const data = extractDataFromJson(jsonData);
const userUrlMatch = url.match(/https:\/\/www\.tiktok\.com\/(\@[\w\.]+)/);
if (userUrlMatch) {
const userUrl = `https://www.tiktok.com/${userUrlMatch[1]}`;
console.log('Fetching user data from URL:', userUrl);
fetchUserData(userUrl, data);
} else {
updateDataContainerContent(data);
}
} catch (e) {
console.error('解析 JSON 时出错:', e);
showNotification('解析数据时出错');
$('#data-output').text('暂无数据');
}
} else {
console.warn('未找到指定的 <script> 标签。');
showNotification('未找到数据脚本标签');
$('#data-output').text('暂无数据');
}
} else {
console.error('请求失败,状态码:', response.status);
showNotification('请求失败,请检查 URL 是否正确');
$('#data-output').text('暂无数据');
}
},
onerror: function(error) {
console.error('请求出错:', error);
showNotification('请求出错,请检查网络连接');
$('#data-output').text('暂无数据');
}
});
}
function fetchUserData(url, videoData) {
console.log('Fetching user data from URL:', url);
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: {
'User-Agent': navigator.userAgent,
'Accept': 'text/html'
},
onload: async function(response) { // 将 onload 函数设为异步
console.log('User data fetched, status:', response.status);
if (response.status === 200) {
const responseText = response.responseText;
const scriptMatch = responseText.match(/<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__" type="application\/json">([\s\S]*?)<\/script>/);
if (scriptMatch) {
try {
const jsonData = JSON.parse(scriptMatch[1]);
console.log('User JSON data parsed successfully');
const userInfo = jsonData.__DEFAULT_SCOPE__["webapp.user-detail"].userInfo;
const userStats = userInfo.stats;
const userAvatar = userInfo.user.avatarLarger;
// 获取头像的 Blob URL
let profilepicBlobUrl = '';
try {
profilepicBlobUrl = await getProfilePicBlobUrl(userAvatar);
} catch (error) {
console.error('获取头像失败:', error);
// 可以在这里设置一个默认的头像 URL
profilepicBlobUrl = '默认头像的 URL';
}
const userData = {
followerCount: userStats.followerCount || 0,
uniqueId: userInfo.user.uniqueId || '未知',
avatar: profilepicBlobUrl || userAvatar || ''
};
console.log('User data extracted:', userData);
updateDataContainerContent({ ...videoData, ...userData });
} catch (e) {
console.error('解析用户数据 JSON 时出错:', e);
showNotification('解析用户数据时出错');
updateDataContainerContent(videoData);
}
} else {
console.warn('未找到指定的 <script> 标签。');
showNotification('未找到用户数据脚本标签');
updateDataContainerContent(videoData);
}
} else {
console.error('请求失败,状态码:', response.status);
showNotification('请求用户数据失败,请检查 URL 是否正确');
updateDataContainerContent(videoData);
}
},
onerror: function(error) {
console.error('请求用户数据出错:', error);
showNotification('请求用户数据出错,请检查网络连接');
updateDataContainerContent(videoData);
}
});
}
function getProfilePicBlobUrl(profilepicUrl) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: profilepicUrl,
responseType: 'blob',
onload: function(response) {
if (response.status === 200) {
const blob = response.response;
const blobUrl = URL.createObjectURL(blob);
resolve(blobUrl);
} else {
reject(new Error('获取头像失败,状态码: ' + response.status));
}
},
onerror: function(error) {
reject(error);
}
});
});
}
function extractDataFromJson(jsonData) {
try {
console.log('Extracting data from JSON');
const itemStruct = jsonData.__DEFAULT_SCOPE__["webapp.video-detail"].itemInfo.itemStruct;
const stats = itemStruct.stats;
const extractedData = {
createTime: itemStruct.createTime || 0,
diggCount: stats.diggCount || 0,
playCount: stats.playCount || 0,
commentCount: stats.commentCount || 0,
shareCount: stats.shareCount || 0,
collectCount: stats.collectCount || 0
};
console.log('Extracted data:', extractedData);
return extractedData;
} catch (e) {
console.error('解析数据时出错:', e);
return {};
}
}
function updateDataContainerContent(data) {
console.log('Updating data container content');
if (Object.keys(data).length === 0) {
$('#data-output').text('暂无数据');
return;
}
const {
createTime,
diggCount,
playCount,
commentCount,
shareCount,
collectCount,
followerCount,
uniqueId,
avatar // 现在 avatar 是 Blob URL
} = data;
const formattedTime = createTime ? new Date(createTime * 1000).toLocaleString() : '未知';
let dataHtml = `
<h1 style="text-align: center; color: black; font-size:15px;">---帖子信息---</h1>
<p style="color: black;">发布时间: ${formattedTime} <button class="copy-btn" data-copy="${formattedTime}" style="background:none; border:none; cursor:pointer;">📋</button></p>
<p style="color: black;">点赞数: ${diggCount} <button class="copy-btn" data-copy="${diggCount}" style="background:none; border:none; cursor:pointer;">📋</button></p>
<p style="color: black;">播放数: ${playCount} <button class="copy-btn" data-copy="${playCount}" style="background:none; border:none; cursor:pointer;">📋</button></p>
<p style="color: black;">评论数: ${commentCount} <button class="copy-btn" data-copy="${commentCount}" style="background:none; border:none; cursor:pointer;">📋</button></p>
<p style="color: black;">分享数: ${shareCount} <button class="copy-btn" data-copy="${shareCount}" style="background:none; border:none; cursor:pointer;">📋</button></p>
<p style="color: black;">收藏数: ${collectCount} <button class="copy-btn" data-copy="${collectCount}" style="background:none; border:none; cursor:pointer;">📋</button></p>
`;
if (uniqueId) {
dataHtml += `
<h1 style="text-align: center; color: black; font-size:15px;">---用户信息---</h1>
<p style="color: black;">账户名: ${uniqueId} <button class="copy-btn" data-copy="${uniqueId}" style="background:none; border:none; cursor:pointer;">📋</button></p>
<p style="color: black;">粉丝数: ${followerCount} <button class="copy-btn" data-copy="${followerCount}" style="background:none; border:none; cursor:pointer;">📋</button></p>
<img src="${avatar}" alt="用户头像" style="max-width:100%; border-radius:10px; margin-top:10px; box-shadow: 0px 9px 20px rgba(72, 72, 72, 0.3);" />
`;
}
$('#data-output').html(dataHtml);
console.log('Data container updated');
// 为复制按钮添加事件监听器
$('.copy-btn').on('click', function() {
const copyValue = $(this).data('copy');
console.log('Copying value to clipboard:', copyValue);
try {
GM_setClipboard(copyValue);
showNotification('已复制到剪贴板');
} catch (err) {
console.error('复制失败:', err);
showNotification('复制到剪贴板失败,请手动复制');
}
});
}
function showNotification(message) {
console.log('Showing notification:', message);
Toastify({
text: message,
duration: 3000,
close: true,
gravity: 'top',
position: 'center',
style: {
background: 'linear-gradient(to right, rgb(0, 176, 155), rgb(150, 201, 61))',
color: '#FFFFFF',
borderRadius: '2px',
},
stopOnFocus: true,
}).showToast();
}
// 使元素可拖动的函数,只针对特定的 handle
function dragElement(elmnt, handle) {
console.log('Making element draggable');
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
handle.onmousedown = dragMouseDown;
function dragMouseDown(e) {
console.log('Drag start');
e = e || window.event;
e.preventDefault();
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
console.log('Dragging element to:', elmnt.style.top, elmnt.style.left);
}
function closeDragElement() {
console.log('Drag end');
document.onmouseup = null;
document.onmousemove = null;
}
}
})();