// ==UserScript==
// @name 抖音搜索结果检测作者是否存在
// @namespace http://tampermonkey.net/
// @version 1.8
// @description 搜索页面的高效操作
// @author You
// @match https://www.douyin.com/*/search/*
// @match https://www.douyin.com/search/*
// @match https://search.bilibili.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=douyin.com
// @grant none
// @license MIT
// ==/UserScript==
var aggrx_searchList = []; // document.querySelectorAll('#search-content-area ul[data-e2e="scroll-list"] li');
var aggrx_scrollListHeight = 0; //aggrx_searchList[0].clientHeight + 70;
// 复制文本
function aggrx_copyToClipboard(text, copy_success, copy_failed) {
navigator.clipboard.writeText(text).then(function() {
console.log('Text successfully copied to clipboard');
if (copy_success) { copy_success(text) }
}).catch(function(err) {
console.error('Unable to copy text to clipboard', err);
if (copy_failed) { copy_failed(err) }
});
}
// 添加 复制合集按钮DOM
function aggrx_addCopyCollectionButtonDom(LiElement, page_type) {
const divBOX = document.createElement('div');
divBOX.className = 'aggrx_buttonBox';
//divBOX.style.width = '100%';
divBOX.style.height = '40px';
const button1 = document.createElement('button');
// button1.className = 'aggrx_buttonBox_button'
button1.classList.add('aggrx_buttonBox_button');
button1.classList.add(`aggrx_buttonBox_button-${page_type}`);
// 设置按钮的文本
button1.textContent = '视频连接';
// 设置按钮的宽度和高度
//button1.style.width = '49%';
//button1.style.height = '40px';
//button1.style.float = 'left';
function copy_success(msg) {
aggrx_showToast('info', `已复制 ${msg}`)
}
function copy_failed(error) {
aggrx_showToast('error', `复制失败 ${error}`)
}
if (page_type === 'search-douyin') {
if (window.location.href.includes('type=video')) {
// 拦截按钮的点击事件
button1.addEventListener('click', (event) => {
// 阻止事件冒泡
event.stopPropagation();
// 获取父元素
const parentElement = event.target.closest('li');
let videoUrl = parentElement.querySelector('div.search-result-card a').href;
let authorDom = parentElement.querySelectorAll("div.search-result-card > a > div > div > div > div > span > span")[1];
if (!authorDom) {
console.warn(`没有找到作者节点 ${parentElement}`)
return;
}
let authorName = authorDom.innerText
if (!authorName) {
console.warn(`作者名称是空`)
return;
}
authorName = authorName.replace(' 🈚️', '').replace(' 🈶', '')
console.info(`检查 ${authorName} 视频URL:${videoUrl}`)
// 使用正则表达式从 href 中提取视频 ID
const regex = /\/video\/(\d+)/;
const match = videoUrl.match(regex);
if (!(match && match[1])) {
return
}
let videoId = match[1];
console.log('视频 ID:', videoId);
let collectionData = window.collectionIDObject[videoId]
// copy_button_ref.textContent = "复制";
let href = new URL(videoUrl);
href.search = '';
let hrefString = href.toString()
if (collectionData.collectionID) {
aggrx_copyToClipboard(`${hrefString},1`, copy_success, copy_failed)
} else {
aggrx_copyToClipboard(`${hrefString}`, copy_success, copy_failed)
}
});
} else if (window.location.href.includes('type=general')) {
divBOX.classList.add('aggrx-button-box-general')
button1.addEventListener('click', (event) => {
// 阻止事件冒泡
event.stopPropagation();
// 获取父元素
const parentElement = event.target.closest('div[id^="waterfall_item_"]')
let videoUrl = `https://www.douyin.com/video/${parentElement.getAttribute('id').replace('waterfall_item_', '')}`;
let authorDom = parentElement.querySelectorAll("div.search-result-card span > span")[1];
if (!authorDom) {
console.warn(`没有找到作者节点 ${parentElement}`)
return;
}
let authorName = authorDom.innerText
if (!authorName) {
console.warn(`作者名称是空`)
return;
}
authorName = authorName.replace(' 🈚️', '').replace(' 🈶', '')
console.info(`检查 ${authorName} 视频URL:${videoUrl}`)
//let videoId = match[1];
// console.log('视频 ID:', videoId);
//let collectionData = window.collectionIDObject[videoId]
// copy_button_ref.textContent = "复制";
let href = new URL(videoUrl);
href.search = '';
let hrefString = href.toString()
let hasCollection = Array.from(parentElement.querySelectorAll('div.search-result-card span')).map((item)=>item.innerText).join('').includes('合集')
if (hasCollection) {
aggrx_copyToClipboard(`${hrefString},1`, copy_success, copy_failed)
} else {
aggrx_copyToClipboard(`${hrefString}`, copy_success, copy_failed)
}
});
} else {
aggrx_showToast('error', '不支持的页面, 抖音支支持`视频`和`综合`页面')
}
} else if (page_type === 'search-bilibili') {
// 拦截按钮的点击事件
button1.addEventListener('click', (event) => {
// 阻止事件冒泡
event.stopPropagation();
const copy_button_ref = event.currentTarget;
// 获取父元素
const parentElement = event.target.closest('div.bili-video-card');
let videoUrl = parentElement.querySelector('div.bili-video-card a').href;
let authorDom = parentElement.querySelectorAll("div.bili-video-card .bili-video-card__info--author")[0];
if (!authorDom) {
console.warn(`没有找到作者节点 ${parentElement}`)
return;
}
let authorName = authorDom.innerText
if (!authorName) {
console.warn(`作者名称是空`)
return;
}
authorName = authorName.replace(' 🈚️', '').replace(' 🈶', '')
console.info(`检查 ${authorName} 视频URL:${videoUrl}`)
// copy_button_ref.textContent = "复制";
let href = new URL(videoUrl);
href.search = '';
let hrefString = href.toString()
if (0) {
aggrx_copyToClipboard(`${hrefString},1`, copy_success, copy_failed)
} else {
aggrx_copyToClipboard(`${hrefString}`, copy_success, copy_failed)
}
});
} else {
console.warn('不支持的页面类型')
}
divBOX.appendChild(button1);
// 设置 li 元素的高度
LiElement.style.height = `${aggrx_scrollListHeight}px`;
LiElement.style.marginBottom = `50px`;
// 添加按钮到当前 li 元素
LiElement.appendChild(divBOX);
// DOMUpdated()
return true
}
// 拦截请求
function aggrx_requestInterception() {
let page_type = aggrx_get_page_type(window.location.href)
if (page_type !== 'search-douyin') {
// aggrx_showToast('error', '不支持的页面, 请切换到正确的标签再使用本插件. 抖音需要切换到`视频`页面')
return;
}
window.collectionIDObject = {};
// 保存原始的 open 和 send 方法
const originalOpen = XMLHttpRequest.prototype.open;
const originalSend = XMLHttpRequest.prototype.send;
// 重写 open 方法,记录请求信息
XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
this._requestInfo = { method, url }; // 保存请求的 URL 和方法
return originalOpen.apply(this, arguments);
};
// 重写 send 方法,拦截响应数据
XMLHttpRequest.prototype.send = function(body) {
const xhr = this;
// 保存请求体数据
this._requestInfo.body = body;
// 保存原始的 onreadystatechange
const originalOnReadyStateChange = xhr.onreadystatechange;
// 重写 onreadystatechange
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
const { url } = xhr._requestInfo;
// 判断是否是特定接口
if (url.includes('/aweme/v1/web/search/item/')) {
console.log(`Request to: ${url}`);
try {
const parsedResponse = JSON.parse(xhr.responseText);
parsedResponse.data.forEach(datum => {
let aweme_info = datum.aweme_info;
window.collectionIDObject[datum.aweme_info.aweme_id] = {
videoID: aweme_info.aweme_id,
authorName: aweme_info.nickname,
authorUID: aweme_info.author.uid,
desc: aweme_info.desc,
tag: aweme_info.text_extra
};
if (datum.aweme_info.mix_info) {
let collectionData = datum.aweme_info.mix_info;
window.collectionIDObject[datum.aweme_info.aweme_id].collectionID = collectionData.mix_id;
window.collectionIDObject[datum.aweme_info.aweme_id].collectionName = collectionData.mix_name;
}
})
} catch (e) {
console.error('Error parsing response:', e);
}
}
}
// 调用原始的回调函数
if (originalOnReadyStateChange) {
originalOnReadyStateChange.apply(xhr, arguments);
}
};
return originalSend.apply(this, arguments);
};
}
// 拦截请求
aggrx_requestInterception();
function aggrx_get(url, success, failed) {
// 创建一个新的 XMLHttpRequest 实例
let xhr = new XMLHttpRequest();
// 配置请求类型和目标 URL
xhr.open('GET', url, true);
// 设置请求头
xhr.setRequestHeader('Accept', 'application/json');
xhr.setRequestHeader('Content-Type', 'application/json');
// 定义请求体数据
//var data = JSON.stringify({
// "url": `http://new_bms.yingshidq.com.cn/api/inside/author/nametoid?authorName=#{authorName}`, // 烟火中的水滴
// "type": "text"
//});
// 监听请求完成的回调
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4) {
return
}
if (xhr.status >= 200 && xhr.status < 300) {
// 请求成功时处理响应
// console.log('Response:', xhr.responseText);
try {
// 尝试将响应文本转换为 JSON
var responseJson = JSON.parse(xhr.responseText);
success(responseJson)
} catch (e) {
// 如果 JSON 解析失败,捕获并处理错误
console.error('Error parsing JSON:', e);
failed({ 'error': e });
}
} else {
failed({ 'error': 'other Error' });
}
};
// 处理网络错误
xhr.onerror = function() {
console.error('Network error or CORS issue');
failed({ 'error': 'other error' });
};
// 发送请求
xhr.send();
}
function aggrx_updateAuthorCheckResult(authorName, authorDom, page_type, response) {
console.info(`response=`, response)
try {
let data_obj = response.data || {};
let author_id = `${data_obj.has_author_id}` || 'false';
if (author_id == 'true') {
console.info(`检测作者 ${authorName} 请求成功, 🈶 数据 ${author_id}`);
authorDom.innerText = `${authorName} 🈶`;
authorDom.style.color = 'red';
} else if (author_id == 'false') {
authorDom.innerText = `${authorName} 🈚️`;
authorDom.style.color = 'green';
console.warn(`检测作者 ${authorName} 请求成功, 🈚️ 数据 `);
} else {
console.warn(`${authorName} 不支持的数据`, response);
}
//DOMUpdated()
} catch (e) {
console.error(`检测作者 ${authorName} 接口数据格式错误. ${e}`);
}
}
let aggrx_queue = [] // 发送队列
let aggrx_queue_running = false
function aggrx_queue_start() {
if (aggrx_queue_running) {
return
}
queue_send()
aggrx_queue_running = true;
function queue_send() {
let task = aggrx_queue.shift()
if (!task) {
aggrx_queue_running = false
return;
}
aggrx_queue_running = true;
let authorName = task[0]
let authorDom = task[1]
let page_type = task[2]
console.info(`${Date()} ${authorName} 出队`)
//const callbackName = `jsonpCallback_${crypto.randomUUID().replace(/-/g, '')}_${Date.now()}`;
//console.info(`${authorName} callback: ${callbackName} `)
// &callback=${callbackName}
function startNext() {
setTimeout(() => { queue_send() }, 100)
}
if (0) {
aggrx_queue_check_user_api(authorName, authorDom, page_type, () => {
startNext()
}, () => {
startNext()
})
} else {
aggrx_queue_check_user_jsonp(authorName, authorDom, page_type, () => {
startNext()
}, () => {
startNext()
})
}
}
}
function aggrx_queue_check_user_api(authorName, authorDom, page_type, success, failed) {
aggrx_get(`https://openapi.yingshidq.com.cn/api/sv/v1/author/list-out?authorName=${authorName}`, (response) => {
setTimeout(() => {
aggrx_updateAuthorCheckResult(authorName, authorDom, response)
success()
}, 100)
}, (error) => {
console.error(`检测作者 ${authorName} 请求失败`, error)
setTimeout(() => {
failed()
}, 100)
})
}
function aggrx_queue_check_user_jsonp(authorName, authorDom, page_type, success, failed) {
// 定义全局回调函数名称,确保唯一性
const callbackName = `jsonpCallback_${crypto.randomUUID().replace(/-/g, '')}_${Date.now()}`;
console.info(`${authorName} callback: ${callbackName} `)
// 创建一个全局回调函数
window[callbackName] = function(response) {
let timeout = window[`${callbackName}-timeout`]
if (timeout) {
window[`${callbackName}-timeout`] = undefined
clearTimeout(timeout);
}
// 删除全局回调函数以清理空间
delete window[callbackName];
// 删除动态创建的 script 标签
let script = window[`${callbackName}-script`]
if (script) {
window[`${callbackName}-script`] = undefined
document.body.removeChild(script);
}
// 解析响应数据并调用回调
setTimeout(() => {
aggrx_updateAuthorCheckResult(authorName, authorDom, page_type, response)
success()
}, 200)
};
// 动态创建 script 标签
const script = document.createElement('script');
script.id = callbackName; // 设置 id
script.src = `https://openapi.yingshidq.com.cn/api/sv/v1/author/list-out?authorName=${authorName}&callback=${callbackName}`;
window[`${callbackName}-script`] = script
// 处理加载失败的情况
script.onerror = function() {
// 清理回调和 script
let timeout = window[`${callbackName}-timeout`]
if (timeout) {
window[`${callbackName}-timeout`] = undefined
clearTimeout(timeout);
}
delete window[callbackName];
let script = window[`${callbackName}-script`]
if (script) {
window[`${callbackName}-script`] = undefined
document.body.removeChild(script);
}
console.error(`检测作者 ${authorName} 请求失败`);
failed()
};
// 将 script 标签添加到文档中以触发请求
document.body.appendChild(script);
window[`${callbackName}-timeout`] = setTimeout(() => {
console.error(`检测作者 ${authorName} 请求超时`);
delete window[callbackName];
let script = window[`${callbackName}-script`]
if (script) {
window[`${callbackName}-script`] = undefined
document.body.removeChild(script);
}
failed()
}, 5000);
}
// 检查作者是否入库
function aggrx_checkApiUserJSONP(authorName, authorDom, page_type) {
console.info(`${Date()} ${authorName} 入队`)
aggrx_queue.push([authorName, authorDom, page_type])
aggrx_queue_start()
return
}
/**
* 检查作者
* @param {HtmlElement} liDom
* @param {string} page_type
*/
function aggrx_checkAuthor(liDom, page_type) {
try {
let authorDom = undefined
if (page_type === 'search-douyin') {
if (window.location.href.includes('type=video')) {
authorDom = liDom.querySelectorAll("div.search-result-card > a > div > div > div > div > span > span")[1];
} else if (window.location.href.includes('type=general')) {
let parentElement = liDom
authorDom = parentElement.querySelectorAll("div.search-result-card span > span")[1];
} else {
aggrx_showToast('error', '不支持的页面, 抖音支支持`视频`和`综合`页面')
}
} else if (page_type === 'search-bilibili') {
authorDom = liDom.querySelectorAll("div.bili-video-card .bili-video-card__info--author")[0];
} else {
console.warn(`不支持的页面类型 ${page_type}`)
return;
}
if (!authorDom) {
console.warn(`没有找到作者节点 ${liDom}`)
return;
}
let authorName = authorDom.innerText
if (!authorName) {
console.warn(`作者名称是空`)
return;
}
console.info(`检查 ${authorName}`)
if (authorName.includes("🈶")) { console.info(`${authorName} 已经检查过`); return };
if (authorName.includes("🈚️")) { console.info(`${authorName} 已经检查过`); return };
aggrx_checkApiUserJSONP(authorName, authorDom, page_type);
} catch (error) {
console.error('检查作者dom 错误!!!!', error)
}
}
// 监听 列表dom
function aggrx_targetListNode(page_type, observer_el_selector) {
// 监听
const targetNode = document.querySelector(observer_el_selector);
if (!targetNode){
return false
}
// 创建一个 MutationObserver 实例
const observer = new MutationObserver(mutationsList => {
// 遍历所有的 mutations
mutationsList.forEach(mutation => {
// 只有子节点变动才执行
if (mutation.type !== 'childList') {
return
}
// 为每个 li 元素添加按钮
mutation.addedNodes.forEach((li, index) => {
// 确保每个 li 只添加一个按钮
if (li.querySelector('div.aggrx_buttonBox')) {
console.info("已经处理过了")
return
}
aggrx_addCopyCollectionButtonDom(li, page_type);
aggrx_checkAuthor(li, page_type);
});
});
});
// 配置观察器 启动观察 服务
const config = { childList: true, subtree: false };
observer.observe(targetNode, config);
return true
}
let initialized = {};
// 初始化
function aggrxInitialization(event) {
event.stopPropagation();
let page_type = aggrx_get_page_type(window.location.href)
if (!page_type) {
aggrx_showToast('error', '不支持的页面, 请切换到正确的标签再使用本插件. 抖音需要切换到`视频`页面')
return;
}
if (page_type === 'search-douyin') {
let video_page_observer_el = '#search-content-area ul[data-e2e="scroll-list"]'
if (!(initialized[video_page_observer_el] || false)) {
initialized[video_page_observer_el] = aggrx_targetListNode(page_type,video_page_observer_el);
}
let general_page_observer_el = 'div#waterFallScrollContainer'
if (!(initialized[general_page_observer_el] || false)) {
initialized[general_page_observer_el] = aggrx_targetListNode(page_type,general_page_observer_el);
}
}
// document.getElementById('aggrxInitializationButton').innerText = '初始化中';
// 初始化遍历每个 li 元素
if (page_type === 'search-douyin') {
if (window.location.href.includes('type=video')) {
aggrx_searchList = document.querySelectorAll('#search-content-area ul[data-e2e="scroll-list"] li');
} else if (window.location.href.includes('type=general')) {
aggrx_searchList = document.querySelectorAll('div#waterFallScrollContainer div[id^="waterfall_item_"]');
} else {
aggrx_showToast('error', '不支持的页面, 抖音支支持`视频`和`综合`页面')
}
aggrx_scrollListHeight = aggrx_searchList[0].clientHeight + 70;
} else if (page_type == 'search-bilibili') {
// aggrx_searchList = document.querySelectorAll('div.search-all-list > .video-list div.bili-video-card');
aggrx_searchList = document.querySelectorAll('div.video-list div.bili-video-card')
aggrx_scrollListHeight = aggrx_searchList[0].clientHeight + 70;
} else {
aggrx_showToast('error', '不支持的页面, 请切换到正确的标签再使用本插件. 抖音需要切换到`视频`页面')
}
aggrx_searchList.forEach((li, index) => {
if (li.querySelector('div.aggrx_buttonBox')) {
console.info("已经处理过了")
return
}
aggrx_addCopyCollectionButtonDom(li, page_type);
aggrx_checkAuthor(li, page_type);
});
}
(function() {
'use strict';
console.info("查询作者是否入库 启动")
const css = document.createElement("style");
css.type = "text/css";
css.innerText = `
.aggrx-search-douyin {
position: fixed;
top: 5px;
left: calc(50% + 300px);
padding: 10px;
background-color: #33333305;
color: white;
text-align: center;
z-index: 1000;
}
.aggrx-search-douyin button {
padding: 5px 10px;
font-size: 16px;
cursor: pointer;
border-radius: 8px;
background: transparent;
color: white;
border: 2px solid #cdcdcd;
}
.aggrx-search-bilibili {
position: fixed;
top: 102px;
left: calc(50% + 330px);
z-index: 1000;
}
.aggrx-toast {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background-color: #333;
color: white;
padding: 10px 20px;
border-radius: 5px;
font-size: 16px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
opacity: 0;
transition: opacity 0.5s ease;
z-index: 9999; /* 设置 z-index 为 9999 确保 toast 在最上层 */
}
.aggrx-toast.show {
opacity: 0.8;
}
.aggrx_buttonBox{
height: 40px;
/*top: 1px;*/
/*margin-left: 150px;*/
position: absolute;
background: transparent;
/*z-index:99999*/
}
.aggrx-button-box-general{
/*height: 40px;*/
/*position: absolute;*/
/*top: 10px;*/
/*right: 10px;*/
}
.aggrx_buttonBox_button{
background: transparent;
border: 2px solid #cdcdcd;
padding: 4px;
color: white;
margin-right: 4px;
cursor: pointer;
}
.aggrx_buttonBox_button-search-bilibili{
color: #00aeec !important;
}
`;
document.head.appendChild(css);
let page_type = aggrx_get_page_type(window.location.href)
if (!page_type) {
aggrx_showToast('error', '不支持的页面, 请切换到正确的标签再使用本插件. 抖音需要切换到`视频`页面')
return;
}
const pannel = document.createElement('div');
pannel.id = 'aggrx-pannel';
pannel.classList.add(`aggrx-${page_type}`);
if (page_type.includes('bilibili')) {
pannel.innerHTML = `
<button id="aggrxInitializationButton" class='vui_button vui_button--blue vui_button--lg search-button'>扫描</button>
`;
} else {
pannel.innerHTML = `
<button id="aggrxInitializationButton">扫描</button>
`;
}
document.body.appendChild(pannel);
document.getElementById("aggrxInitializationButton").addEventListener("click", aggrxInitialization);
})();
function aggrx_get_page_type(url) {
let page_type = ''
let current_url = new URL(url)
if (current_url.hostname.match(/search.bilibili.com/)) {
return 'search-bilibili'
} else if (current_url.href.match(/\.douyin\.com\/root\/search\/.*type=video.*/) || current_url.href.match(/\.douyin\.com\/root\/search\/.*type=video.*/)) {
return 'search-douyin'
} else if (current_url.href.match(/\.douyin\.com\/search\/.*/) || current_url.href.match(/\.douyin\.com\/search\/.*/)) {
return 'search-douyin'
} else {
return;
}
}
function aggrx_showToast(type, message) {
// 创建一个新的 toast 元素
const toast = document.createElement('div');
toast.classList.add('aggrx-toast');
toast.textContent = message;
if (type === 'info') {
toast.style.color = 'white'
} else {
toast.style.color = 'red'
}
// 将 toast 添加到 body
document.body.appendChild(toast);
// 显示 toast
setTimeout(() => {
toast.classList.add('show');
}, 10); // 等待一小段时间(确保元素已经插入 DOM)
// 5秒后隐藏并删除 toast
setTimeout(() => {
toast.classList.remove('show');
// 过渡动画结束后删除 toast 元素
setTimeout(() => {
toast.remove();
}, 500); // 等待过渡动画完成
}, 2000); // 5秒后消失
}