// ==UserScript==
// @name B站直播工具
// @namespace https://github.com/EY2318/
// @version 0.1
// @description B站直播辅助工具, 支持登录、开始直播、结束直播和更新直播信息
// @author LynLuc
// @match https://*.bilibili.com/*
// @icon https://www.bilibili.com/favicon.ico
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @connect api.bilibili.com
// @connect api.live.bilibili.com
// @connect passport.bilibili.com
// @license MIT
// ==/UserScript==
(function () {
"use strict";
// 基础配置
const config = {
version: [0, 3, 0],
userAgent:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0",
// LiveHime客户端的应用信息
APP_KEY: "aae92bc66f3edfab",
APP_SECRET: "af125a0d5279fd576c1b4418a3e8276d",
LIVEHIME_BUILD: "9240",
LIVEHIME_VERSION: "7.16.0.9240",
// 签名配置
START_LIVE_AUTH_CSRF: true, // 开播API是否在签名中包含csrf
STOP_LIVE_AUTH_CSRF: true, // 停播API是否在签名中包含csrf
};
// 存储数据
const data = {
userId: -1,
roomId: -1,
areaId: -1,
parentAreaId: -1,
title: "",
liveStatus: -1,
roomData: {},
rtmpAddr: "",
rtmpCode: "",
cookies: {},
csrf: "",
areaList: {},
};
// B站直播API签名相关函数
// MD5哈希函数 - 使用纯JavaScript实现
function md5(str) {
function rotateLeft(lValue, iShiftBits) {
return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits));
}
function addUnsigned(lX, lY) {
const lX8 = lX & 0x80000000;
const lY8 = lY & 0x80000000;
const lX4 = lX & 0x40000000;
const lY4 = lY & 0x40000000;
const lResult = (lX & 0x3fffffff) + (lY & 0x3fffffff);
if (lX4 & lY4) return lResult ^ 0x80000000 ^ lX8 ^ lY8;
if (lX4 | lY4) {
if (lResult & 0x40000000) return lResult ^ 0xc0000000 ^ lX8 ^ lY8;
else return lResult ^ 0x40000000 ^ lX8 ^ lY8;
} else {
return lResult ^ lX8 ^ lY8;
}
}
function F(x, y, z) {
return (x & y) | (~x & z);
}
function G(x, y, z) {
return (x & z) | (y & ~z);
}
function H(x, y, z) {
return x ^ y ^ z;
}
function I(x, y, z) {
return y ^ (x | ~z);
}
function FF(a, b, c, d, x, s, ac) {
a = addUnsigned(a, addUnsigned(addUnsigned(F(b, c, d), x), ac));
return addUnsigned(rotateLeft(a, s), b);
}
function GG(a, b, c, d, x, s, ac) {
a = addUnsigned(a, addUnsigned(addUnsigned(G(b, c, d), x), ac));
return addUnsigned(rotateLeft(a, s), b);
}
function HH(a, b, c, d, x, s, ac) {
a = addUnsigned(a, addUnsigned(addUnsigned(H(b, c, d), x), ac));
return addUnsigned(rotateLeft(a, s), b);
}
function II(a, b, c, d, x, s, ac) {
a = addUnsigned(a, addUnsigned(addUnsigned(I(b, c, d), x), ac));
return addUnsigned(rotateLeft(a, s), b);
}
function convertToWordArray(str) {
let lWordCount;
const lMessageLength = str.length;
const lNumberOfWords_temp1 = lMessageLength + 8;
const lNumberOfWords_temp2 =
(lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64;
const lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16;
const lWordArray = Array(lNumberOfWords - 1);
let lBytePosition = 0;
let lByteCount = 0;
while (lByteCount < lMessageLength) {
lWordCount = (lByteCount - (lByteCount % 4)) / 4;
lBytePosition = (lByteCount % 4) * 8;
lWordArray[lWordCount] =
lWordArray[lWordCount] |
(str.charCodeAt(lByteCount) << lBytePosition);
lByteCount++;
}
lWordCount = (lByteCount - (lByteCount % 4)) / 4;
lBytePosition = (lByteCount % 4) * 8;
lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition);
lWordArray[lNumberOfWords - 2] = lMessageLength << 3;
lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29;
return lWordArray;
}
function wordToHex(lValue) {
let wordToHexValue = "",
wordToHexValue_temp = "",
lByte,
lCount;
for (lCount = 0; lCount <= 3; lCount++) {
lByte = (lValue >>> (lCount * 8)) & 255;
wordToHexValue_temp = "0" + lByte.toString(16);
wordToHexValue =
wordToHexValue +
wordToHexValue_temp.substr(wordToHexValue_temp.length - 2, 2);
}
return wordToHexValue;
}
let x = [];
let k, AA, BB, CC, DD, a, b, c, d;
const S11 = 7,
S12 = 12,
S13 = 17,
S14 = 22;
const S21 = 5,
S22 = 9,
S23 = 14,
S24 = 20;
const S31 = 4,
S32 = 11,
S33 = 16,
S34 = 23;
const S41 = 6,
S42 = 10,
S43 = 15,
S44 = 21;
// Steps 1 and 2: Append padding bits and length
x = convertToWordArray(str);
// Step 3: Initialize MD buffer
a = 0x67452301;
b = 0xefcdab89;
c = 0x98badcfe;
d = 0x10325476;
// Step 4: Process message in 16-word blocks
for (k = 0; k < x.length; k += 16) {
AA = a;
BB = b;
CC = c;
DD = d;
a = FF(a, b, c, d, x[k + 0], S11, 0xd76aa478);
d = FF(d, a, b, c, x[k + 1], S12, 0xe8c7b756);
c = FF(c, d, a, b, x[k + 2], S13, 0x242070db);
b = FF(b, c, d, a, x[k + 3], S14, 0xc1bdceee);
a = FF(a, b, c, d, x[k + 4], S11, 0xf57c0faf);
d = FF(d, a, b, c, x[k + 5], S12, 0x4787c62a);
c = FF(c, d, a, b, x[k + 6], S13, 0xa8304613);
b = FF(b, c, d, a, x[k + 7], S14, 0xfd469501);
a = FF(a, b, c, d, x[k + 8], S11, 0x698098d8);
d = FF(d, a, b, c, x[k + 9], S12, 0x8b44f7af);
c = FF(c, d, a, b, x[k + 10], S13, 0xffff5bb1);
b = FF(b, c, d, a, x[k + 11], S14, 0x895cd7be);
a = FF(a, b, c, d, x[k + 12], S11, 0x6b901122);
d = FF(d, a, b, c, x[k + 13], S12, 0xfd987193);
c = FF(c, d, a, b, x[k + 14], S13, 0xa679438e);
b = FF(b, c, d, a, x[k + 15], S14, 0x49b40821);
a = GG(a, b, c, d, x[k + 1], S21, 0xf61e2562);
d = GG(d, a, b, c, x[k + 6], S22, 0xc040b340);
c = GG(c, d, a, b, x[k + 11], S23, 0x265e5a51);
b = GG(b, c, d, a, x[k + 0], S24, 0xe9b6c7aa);
a = GG(a, b, c, d, x[k + 5], S21, 0xd62f105d);
d = GG(d, a, b, c, x[k + 10], S22, 0x2441453);
c = GG(c, d, a, b, x[k + 15], S23, 0xd8a1e681);
b = GG(b, c, d, a, x[k + 4], S24, 0xe7d3fbc8);
a = GG(a, b, c, d, x[k + 9], S21, 0x21e1cde6);
d = GG(d, a, b, c, x[k + 14], S22, 0xc33707d6);
c = GG(c, d, a, b, x[k + 3], S23, 0xf4d50d87);
b = GG(b, c, d, a, x[k + 8], S24, 0x455a14ed);
a = GG(a, b, c, d, x[k + 13], S21, 0xa9e3e905);
d = GG(d, a, b, c, x[k + 2], S22, 0xfcefa3f8);
c = GG(c, d, a, b, x[k + 7], S23, 0x676f02d9);
b = GG(b, c, d, a, x[k + 12], S24, 0x8d2a4c8a);
a = HH(a, b, c, d, x[k + 5], S31, 0xfffa3942);
d = HH(d, a, b, c, x[k + 8], S32, 0x8771f681);
c = HH(c, d, a, b, x[k + 11], S33, 0x6d9d6122);
b = HH(b, c, d, a, x[k + 14], S34, 0xfde5380c);
a = HH(a, b, c, d, x[k + 1], S31, 0xa4beea44);
d = HH(d, a, b, c, x[k + 4], S32, 0x4bdecfa9);
c = HH(c, d, a, b, x[k + 7], S33, 0xf6bb4b60);
b = HH(b, c, d, a, x[k + 10], S34, 0xbebfbc70);
a = HH(a, b, c, d, x[k + 13], S31, 0x289b7ec6);
d = HH(d, a, b, c, x[k + 0], S32, 0xeaa127fa);
c = HH(c, d, a, b, x[k + 3], S33, 0xd4ef3085);
b = HH(b, c, d, a, x[k + 6], S34, 0x4881d05);
a = HH(a, b, c, d, x[k + 9], S31, 0xd9d4d039);
d = HH(d, a, b, c, x[k + 12], S32, 0xe6db99e5);
c = HH(c, d, a, b, x[k + 15], S33, 0x1fa27cf8);
b = HH(b, c, d, a, x[k + 2], S34, 0xc4ac5665);
a = II(a, b, c, d, x[k + 0], S41, 0xf4292244);
d = II(d, a, b, c, x[k + 7], S42, 0x432aff97);
c = II(c, d, a, b, x[k + 14], S43, 0xab9423a7);
b = II(b, c, d, a, x[k + 5], S44, 0xfc93a039);
a = II(a, b, c, d, x[k + 12], S41, 0x655b59c3);
d = II(d, a, b, c, x[k + 3], S42, 0x8f0ccc92);
c = II(c, d, a, b, x[k + 10], S43, 0xffeff47d);
b = II(b, c, d, a, x[k + 1], S44, 0x85845dd1);
a = II(a, b, c, d, x[k + 8], S41, 0x6fa87e4f);
d = II(d, a, b, c, x[k + 15], S42, 0xfe2ce6e0);
c = II(c, d, a, b, x[k + 6], S43, 0xa3014314);
b = II(b, c, d, a, x[k + 13], S44, 0x4e0811a1);
a = II(a, b, c, d, x[k + 4], S41, 0xf7537e82);
d = II(d, a, b, c, x[k + 11], S42, 0xbd3af235);
c = II(c, d, a, b, x[k + 2], S43, 0x2ad7d2bb);
b = II(b, c, d, a, x[k + 9], S44, 0xeb86d391);
a = addUnsigned(a, AA);
b = addUnsigned(b, BB);
c = addUnsigned(c, CC);
d = addUnsigned(d, DD);
}
const result = wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d);
return result.toLowerCase();
}
// 对象按键名排序
function orderPayload(obj) {
return Object.keys(obj)
.sort()
.reduce((result, key) => {
result[key] = obj[key];
return result;
}, {});
}
// URL编码查询字符串
function encodeParams(params) {
return Object.keys(params)
.map((key) => `${key}=${encodeURIComponent(params[key])}`)
.join("&");
}
// 获取基础请求参数
function basePayload() {
return {
access_key: "",
build: config.LIVEHIME_BUILD,
platform: "pc_link",
ts: Math.floor(Date.now() / 1000).toString(),
version: config.LIVEHIME_VERSION,
};
}
// 生成B站API签名
function livehimeSign(payload) {
const signed = { ...basePayload() };
signed.appkey = config.APP_KEY;
Object.assign(signed, payload);
const orderedParams = orderPayload(signed);
const queryString = encodeParams(orderedParams);
const signStr = queryString + config.APP_SECRET;
const sign = md5(signStr);
orderedParams.sign = sign;
return orderedParams;
}
// 工具函数
const utils = {
// 拼音处理工具
pinyin: {
// 获取汉字拼音首字母
getInitials: function (str) {
if (!str) return "";
const pinyinMap = this.getPinyinMap();
let result = "";
for (let i = 0; i < str.length; i++) {
const char = str[i];
// 如果是汉字,查找对应拼音首字母
if (/[\u4e00-\u9fa5]/.test(char)) {
const initial = this.getCharInitial(char, pinyinMap);
if (initial) result += initial;
} else {
// 非汉字原样保留
result += char;
}
}
return result.toLowerCase();
},
// 获取完整拼音
getFullPinyin: function (str) {
if (!str) return "";
const pinyinMap = this.getPinyinMap();
let result = "";
for (let i = 0; i < str.length; i++) {
const char = str[i];
// 如果是汉字,查找对应拼音
if (/[\u4e00-\u9fa5]/.test(char)) {
const pinyin = this.getCharPinyin(char, pinyinMap);
if (pinyin) result += pinyin;
} else {
// 非汉字原样保留
result += char;
}
}
return result.toLowerCase();
},
// 获取单个汉字的拼音首字母
getCharInitial: function (char, pinyinMap) {
const pinyin = this.getCharPinyin(char, pinyinMap);
return pinyin ? pinyin.charAt(0) : "";
},
// 获取单个汉字的完整拼音
getCharPinyin: function (char, pinyinMap) {
// 查找该汉字的拼音
return pinyinMap[char] || "";
},
// 简化版拼音映射表 (仅包含常用汉字)
getPinyinMap: function () {
// 这里是简化版的拼音映射表,只包含一些常见汉字
// 实际使用时可以扩展更多
return {
一: "yi",
二: "er",
三: "san",
四: "si",
五: "wu",
六: "liu",
七: "qi",
八: "ba",
九: "jiu",
十: "shi",
百: "bai",
千: "qian",
万: "wan",
亿: "yi",
星: "xing",
穹: "qiong",
铁: "tie",
道: "dao",
电: "dian",
竞: "jing",
技: "ji",
王: "wang",
者: "zhe",
荣: "rong",
耀: "yao",
英: "ying",
雄: "xiong",
联: "lian",
盟: "meng",
和: "he",
平: "ping",
精: "jing",
英: "ying",
网: "wang",
游: "you",
戏: "xi",
原: "yuan",
神: "shen",
崩: "beng",
坏: "huai",
蛋: "dan",
娱: "yu",
乐: "le",
动: "dong",
漫: "man",
鬼: "gui",
畜: "chu",
科: "ke",
技: "ji",
手: "shou",
游: "you",
单: "dan",
机: "ji",
绝: "jue",
地: "di",
求: "qiu",
生: "sheng",
虎: "hu",
牙: "ya",
直: "zhi",
播: "bo",
购: "gou",
物: "wu",
美: "mei",
食: "shi",
户: "hu",
外: "wai",
风: "feng",
音: "yin",
乐: "yue",
舞: "wu",
蹈: "dao",
日: "ri",
常: "chang",
学: "xue",
习: "xi",
才: "cai",
艺: "yi",
展: "zhan",
示: "shi",
房: "fang",
产: "chan",
数: "shu",
码: "ma",
摄: "she",
影: "ying",
翻: "fan",
唱: "chang",
聊: "liao",
天: "tian",
大: "da",
厅: "ting",
交: "jiao",
友: "you",
热: "re",
点: "dian",
快: "kuai",
手: "shou",
主: "zhu",
机: "ji",
游: "you",
戏: "xi",
怪: "guai",
物: "wu",
语: "yu",
音: "yin",
文: "wen",
化: "hua",
语: "yu",
言: "yan",
国: "guo",
创: "chuang",
意: "yi",
时: "shi",
尚: "shang",
知: "zhi",
识: "shi",
军: "jun",
事: "shi",
资: "zi",
讯: "xun",
教: "jiao",
育: "yu",
健: "jian",
康: "kang",
相: "xiang",
声: "sheng",
传: "chuan",
媒: "mei",
体: "ti",
育: "yu",
赛: "sai",
事: "shi",
金: "jin",
融: "rong",
社: "she",
会: "hui",
民: "min",
生: "sheng",
高: "gao",
校: "xiao",
情: "qing",
感: "gan",
校: "xiao",
园: "yuan",
二: "er",
次: "ci",
元: "yuan",
舞: "wu",
蹈: "dao",
虚: "xu",
拟: "ni",
演: "yan",
出: "chu",
音: "yin",
乐: "yue",
综: "zong",
合: "he",
歌: "ge",
舞: "wu",
才: "cai",
艺: "yi",
搞: "gao",
笑: "xiao",
脱: "tuo",
口: "kou",
秀: "xiu",
户: "hu",
外: "wai",
美: "mei",
食: "shi",
萌: "meng",
宅: "zhai",
家: "jia",
日: "ri",
常: "chang",
情: "qing",
感: "gan",
旅: "lv",
游: "you",
路: "lu",
亚: "ya",
服: "fu",
装: "zhuang",
时: "shi",
尚: "shang",
聊: "liao",
天: "tian",
线: "xian",
游: "you",
戏: "xi",
电: "dian",
竞: "jing",
体: "ti",
育: "yu",
手: "shou",
机: "ji",
大: "da",
杂: "za",
烩: "hui",
};
},
},
// 发送GET请求
get: function (url, params = {}, headers = {}) {
return new Promise((resolve, reject) => {
const queryString = Object.keys(params)
.map((key) => `${key}=${encodeURIComponent(params[key])}`)
.join("&");
const fullUrl = queryString ? `${url}?${queryString}` : url;
GM_xmlhttpRequest({
method: "GET",
url: fullUrl,
headers: {
"User-Agent": config.userAgent,
...headers,
},
onload: function (response) {
if (response.status >= 200 && response.status < 300) {
resolve(response);
} else {
reject({
status: response.status,
statusText: response.statusText,
});
}
},
onerror: function (error) {
reject(error);
},
});
});
},
// 发送POST请求
post: function (url, data = {}, headers = {}) {
return new Promise((resolve, reject) => {
let formData = "";
for (const key in data) {
formData += `${key}=${encodeURIComponent(data[key])}&`;
}
formData = formData.slice(0, -1); // 移除最后的&
GM_xmlhttpRequest({
method: "POST",
url: url,
headers: {
"User-Agent": config.userAgent,
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
...headers,
},
data: formData,
onload: function (response) {
if (response.status >= 200 && response.status < 300) {
resolve(response);
} else {
reject({
status: response.status,
statusText: response.statusText,
});
}
},
onerror: function (error) {
reject(error);
},
});
});
},
// 从响应中解析JSON
parseJSON: function (response) {
try {
return JSON.parse(response.responseText);
} catch (e) {
console.error("解析JSON失败:", e);
return null;
}
},
// 保存数据到GM_setValue
saveData: function () {
GM_setValue(
"biliLiveData",
JSON.stringify({
userId: data.userId,
roomId: data.roomId,
areaId: data.areaId,
parentAreaId: data.parentAreaId,
title: data.title,
liveStatus: data.liveStatus,
roomData: data.roomData,
rtmpAddr: data.rtmpAddr,
rtmpCode: data.rtmpCode,
csrf: data.csrf,
cookies: data.cookies,
})
);
},
// 从GM_getValue加载数据
loadData: function () {
const savedData = GM_getValue("biliLiveData");
if (savedData) {
try {
const parsedData = JSON.parse(savedData);
Object.assign(data, parsedData);
return true;
} catch (e) {
console.error("加载数据失败:", e);
return false;
}
}
return false;
},
// 日志函数
log: function (message, type = "info") {
const styles = {
info: "color: #2196F3",
success: "color: #4CAF50",
warning: "color: #FF9800",
error: "color: #F44336",
};
console.log("%c[B站直播工具] " + message, styles[type]);
},
// 加载QR Code库
loadQRCodeLib: function () {
return new Promise((resolve) => {
// 如果已经加载了QRCode库, 直接返回
if (window.QRCode) {
resolve(window.QRCode);
return;
}
// 尝试多个CDN源,如果都失败则使用内联版本
const loadFromCDN = () => {
const script = document.createElement("script");
script.src =
"https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js";
script.onerror = loadInlineQRCode;
script.onload = () => resolve(window.qrcode || window.QRCode);
document.head.appendChild(script);
};
// 加载内联版本的QRCode库(简化版本)
const loadInlineQRCode = () => {
utils.log("CDN加载失败,使用内联QRCode库", "warning");
// 简单的QR码生成函数
window.QRCode = function (container, options) {
this.makeCode = function (text) {
// 创建一个显示文本的元素作为替代
const el =
typeof container === "string"
? document.getElementById(container)
: container;
el.innerHTML = `<div style="padding:10px;border:2px solid #000;text-align:center;">
<div>扫码登录</div>
<div style="margin:8px 0;">请打开B站APP</div>
<div style="font-size:12px;color:#999">无法加载二维码库,请手动访问登录链接</div>
<div style="word-break:break-all;font-size:10px;margin-top:8px;">${text}</div>
</div>`;
};
};
resolve(window.QRCode);
};
// 开始尝试加载
loadFromCDN();
});
},
};
// 登录相关功能
const auth = {
// 检查用户登录状态
checkLoginStatus: async function (showVisualIndicator = true) {
try {
// 显示加载提示
if (showVisualIndicator) {
ui.showMessage("正在检查B站登录状态...", "info");
}
const response = await utils.get(
"https://api.bilibili.com/x/web-interface/nav/stat"
);
const result = utils.parseJSON(response);
if (result && result.code === 0) {
utils.log("用户已登录", "success");
if (showVisualIndicator) {
ui.showMessage("登录状态检查完成:已登录B站", "success");
}
return true;
} else {
utils.log("用户未登录或登录已过期", "warning");
if (showVisualIndicator) {
ui.showMessage("请先登录B站再使用本功能", "warning");
}
return false;
}
} catch (error) {
utils.log("检查登录状态失败", "error");
console.error(error);
if (showVisualIndicator) {
ui.showMessage("登录状态检查失败,请检查网络连接", "error");
}
return false;
}
},
// 通过cookie获取用户信息
getUserInfoFromCookies: async function () {
try {
// 从cookie获取csrf
const biliJct = document.cookie
.split("; ")
.find((row) => row.startsWith("bili_jct="));
if (biliJct) {
data.csrf = biliJct.split("=")[1];
}
// 从cookie获取用户ID
const dedeUserID = document.cookie
.split("; ")
.find((row) => row.startsWith("DedeUserID="));
if (dedeUserID) {
data.userId = dedeUserID.split("=")[1];
}
// 获取直播间ID
if (data.userId && data.userId !== -1) {
// 创建签名参数
const params = livehimeSign({ uid: data.userId });
const response = await utils.get(
"https://api.live.bilibili.com/room/v2/Room/room_id_by_uid",
params
);
const result = utils.parseJSON(response);
if (result && result.code === 0 && result.data) {
data.roomId = result.data.room_id;
utils.log("获取直播间信息成功", "success");
utils.saveData();
return true;
}
}
utils.log("获取用户信息失败", "error");
return false;
} catch (error) {
utils.log("获取用户信息异常", "error");
console.error(error);
return false;
}
},
// 生成二维码
generateQRCode: async function () {
try {
const response = await utils.get(
"https://passport.bilibili.com/x/passport-login/web/qrcode/generate"
);
const result = utils.parseJSON(response);
if (result && result.code === 0 && result.data) {
return {
url: result.data.url,
qrcodeKey: result.data.qrcode_key,
};
}
utils.log("生成二维码失败", "error");
return null;
} catch (error) {
utils.log("生成二维码异常", "error");
console.error(error);
return null;
}
},
// 检查二维码扫描状态
checkQRCodeStatus: async function (qrcodeKey) {
try {
const response = await utils.get(
"https://passport.bilibili.com/x/passport-login/web/qrcode/poll",
{ qrcode_key: qrcodeKey }
);
const result = utils.parseJSON(response);
if (result && result.data) {
return {
code: result.data.code,
message: result.data.message,
refreshToken: result.data.refresh_token,
timestamp: result.data.timestamp,
status: result.code === 0,
};
}
return { status: false, code: -1, message: "获取二维码状态失败" };
} catch (error) {
utils.log("检查二维码状态异常", "error");
console.error(error);
return { status: false, code: -1, message: "检查二维码状态异常" };
}
},
// 二维码登录流程
startQRLogin: async function () {
// 创建二维码容器
const qrContainer = document.createElement("div");
qrContainer.className = "bili-live-qr-container protected";
qrContainer.innerHTML = `
<div class="qr-header">B站直播工具登录</div>
<div class="qr-content">
<div class="qr-loading">正在加载二维码...</div>
<div class="qr-img"></div>
<div class="qr-status">请使用哔哩哔哩APP扫描二维码登录</div>
</div>
<div class="qr-footer">
<button class="qr-refresh">刷新二维码</button>
<button class="qr-close">关闭</button>
</div>
`;
// 添加样式
const style = document.createElement("style");
style.textContent = `
.bili-live-qr-container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 10000;
text-align: center;
font-family: Arial, sans-serif;
}
.qr-header {
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
color: #23ade5;
}
.qr-content {
padding: 10px;
}
.qr-img {
width: 200px;
height: 200px;
margin: 0 auto 15px;
display: none;
}
.qr-status {
font-size: 14px;
color: #666;
margin-top: 10px;
}
.qr-loading {
font-size: 14px;
color: #666;
margin: 80px 0;
}
.qr-footer {
margin-top: 15px;
}
.qr-close, .qr-refresh {
padding: 5px 20px;
background: #23ade5;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin: 0 5px;
}
.qr-close:hover, .qr-refresh:hover {
background: #1e9cd7;
}
`;
document.head.appendChild(style);
document.body.appendChild(qrContainer);
// 关闭按钮事件
const closeBtn = qrContainer.querySelector(".qr-close");
closeBtn.addEventListener("click", () => {
document.body.removeChild(qrContainer);
});
// 刷新按钮事件
const refreshBtn = qrContainer.querySelector(".qr-refresh");
refreshBtn.addEventListener("click", async () => {
// 重置状态
const statusText = qrContainer.querySelector(".qr-status");
statusText.textContent = "请使用哔哩哔哩APP扫描二维码登录";
statusText.style.color = "";
// 显示加载中状态
const qrImg = qrContainer.querySelector(".qr-img");
qrImg.style.display = "none";
qrImg.innerHTML = ""; // 清除旧二维码
const loadingEl = qrContainer.querySelector(".qr-loading");
loadingEl.style.display = "block";
// 重新生成二维码
try {
const qrData = await auth.generateQRCode();
if (!qrData) {
statusText.textContent = "生成二维码失败,请再次点击刷新";
loadingEl.style.display = "none";
return;
}
// 显示新二维码
qrImg.style.display = "block";
loadingEl.style.display = "none";
// 生成二维码
new QRCode(qrImg, {
text: qrData.url,
width: 200,
height: 200,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.H,
});
// 重新开始检查扫码状态
let status = false;
const checkInterval = setInterval(async () => {
const result = await auth.checkQRCodeStatus(qrData.qrcodeKey);
if (result.code === 0) {
// 登录成功
clearInterval(checkInterval);
statusText.textContent = "登录成功!";
statusText.style.color = "#4CAF50";
// 更新用户信息
await auth.getUserInfoFromCookies();
// 3秒后关闭二维码窗口
setTimeout(() => {
try {
document.body.removeChild(qrContainer);
// 刷新页面以应用新的登录状态
location.reload();
} catch (e) {
// 忽略可能的错误
}
}, 3000);
} else if (result.code === 86038) {
// 二维码已失效
clearInterval(checkInterval);
statusText.textContent = "二维码已失效,请点击刷新按钮重试";
statusText.style.color = "#F44336";
} else if (result.code === 86090) {
// 已扫码等待确认
if (!status) {
statusText.textContent = "已扫描,请在手机上确认登录";
statusText.style.color = "#FF9800";
status = true;
}
}
}, 1000);
// 60秒后清除轮询
setTimeout(() => {
clearInterval(checkInterval);
// 如果容器仍然存在,显示过期提示
if (document.body.contains(qrContainer)) {
statusText.textContent = "二维码已过期,请点击刷新按钮重试";
statusText.style.color = "#F44336";
}
}, 60000);
} catch (error) {
utils.log("刷新二维码异常", "error");
console.error(error);
statusText.textContent = "刷新二维码出错,请重试";
loadingEl.style.display = "none";
}
});
// 加载QRCode库并生成二维码
try {
await utils.loadQRCodeLib();
const qrData = await auth.generateQRCode();
if (!qrData) {
qrContainer.querySelector(".qr-status").textContent =
"生成二维码失败,请点击刷新按钮重试";
qrContainer.querySelector(".qr-loading").style.display = "none";
return;
}
// 显示二维码
const qrImg = qrContainer.querySelector(".qr-img");
qrImg.style.display = "block";
qrContainer.querySelector(".qr-loading").style.display = "none";
// 生成二维码
new QRCode(qrImg, {
text: qrData.url,
width: 200,
height: 200,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.H,
});
// 轮询检查扫码状态
let status = false;
const statusText = qrContainer.querySelector(".qr-status");
const checkInterval = setInterval(async () => {
const result = await auth.checkQRCodeStatus(qrData.qrcodeKey);
if (result.code === 0) {
// 登录成功
clearInterval(checkInterval);
statusText.textContent = "登录成功!";
statusText.style.color = "#4CAF50";
// 更新用户信息
await auth.getUserInfoFromCookies();
// 3秒后关闭二维码窗口
setTimeout(() => {
try {
document.body.removeChild(qrContainer);
// 刷新页面以应用新的登录状态
location.reload();
} catch (e) {
// 忽略可能的错误
}
}, 3000);
} else if (result.code === 86038) {
// 二维码已失效
clearInterval(checkInterval);
statusText.textContent = "二维码已失效,请点击刷新按钮重试";
statusText.style.color = "#F44336";
} else if (result.code === 86090) {
// 已扫码等待确认
if (!status) {
statusText.textContent = "已扫描,请在手机上确认登录";
statusText.style.color = "#FF9800";
status = true;
}
}
}, 1000);
// 60秒后清除轮询
setTimeout(() => {
clearInterval(checkInterval);
// 如果容器仍然存在,显示过期提示
if (document.body.contains(qrContainer)) {
statusText.textContent = "二维码已过期,请点击刷新按钮重试";
statusText.style.color = "#F44336";
}
}, 60000);
} catch (error) {
utils.log("二维码登录流程异常", "error");
console.error(error);
qrContainer.querySelector(".qr-status").textContent =
"登录过程中出错,请刷新页面重试";
qrContainer.querySelector(".qr-loading").style.display = "none";
}
},
};
// 直播控制相关功能
const liveControls = {
// 获取分区列表
getAreaList: async function () {
try {
const params = livehimeSign({});
const response = await utils.get(
"https://api.live.bilibili.com/room/v1/Area/getList",
params
);
const result = utils.parseJSON(response);
if (result && result.code === 0 && result.data) {
utils.log("获取分区列表成功", "success");
data.areaList = result.data;
return { status: true, data: result.data };
} else {
utils.log("获取分区列表失败", "error");
return { status: false, message: "获取分区列表失败" };
}
} catch (error) {
utils.log("获取分区列表异常", "error");
console.error(error);
return { status: false, message: "获取分区列表过程发生异常" };
}
},
// 获取直播预配置信息 (PreLive接口)
getPreLiveInfo: async function () {
try {
// 创建签名参数
const params = livehimeSign({
area: true,
cover: true,
coverVertical: true,
liveDirectionType: 0,
mobi_app: "pc_link",
schedule: true,
title: true,
});
const response = await utils.get(
"https://api.live.bilibili.com/xlive/app-blink/v1/preLive/PreLive",
params
);
const result = utils.parseJSON(response);
if (result && result.code === 0) {
utils.log("获取直播预配置信息成功", "success");
// console.log(result.data);
// 保存标题信息
if (result.data && result.data.title) {
data.title = result.data.title;
utils.saveData();
}
return { status: true, data: result.data };
} else {
utils.log(
"获取直播预配置信息失败: " + (result ? result.message : "未知错误"),
"error"
);
return {
status: false,
message: result ? result.message : "获取直播预配置信息失败",
};
}
} catch (error) {
utils.log("获取直播预配置信息异常", "error");
console.error(error);
return { status: false, message: "获取直播预配置信息过程发生异常" };
}
},
// 获取直播间详细信息 (GetInfo接口)
getRoomInfo: async function () {
try {
if (!data.userId || data.userId === -1) {
utils.log("未获取到用户ID,无法获取房间信息", "error");
return { status: false, message: "未获取到用户ID" };
}
// 创建签名参数
const params = livehimeSign({
uId: data.userId,
});
const response = await utils.get(
"https://api.live.bilibili.com/xlive/app-blink/v1/room/GetInfo",
params
);
const result = utils.parseJSON(response);
if (result && result.code === 0) {
utils.log("获取直播间信息成功", "success");
// 更新房间信息
if (result.data) {
data.roomId = result.data.room_id;
data.areaId = result.data.area_v2_id;
data.parentAreaId = result.data.parent_id;
data.roomData = {
parent_area: result.data.parent_name,
area: result.data.area_v2_name,
parent_id: result.data.parent_id,
area_id: result.data.area_v2_id,
};
// 更新直播状态
data.liveStatus = result.data.live_status;
// 如果正在直播,尝试获取推流地址
if (result.data.live_status === 1) {
// 通过开播接口获取推流地址,即使显示"重复开播"
this.startLive(
data.title || result.data.title,
result.data.area_v2_id
);
}
utils.saveData();
}
return { status: true, data: result.data };
} else {
utils.log(
"获取直播间信息失败: " + (result ? result.message : "未知错误"),
"error"
);
return {
status: false,
message: result ? result.message : "获取直播间信息失败",
};
}
} catch (error) {
utils.log("获取直播间信息异常", "error");
console.error(error);
return { status: false, message: "获取直播间信息过程发生异常" };
}
},
// 开始直播
startLive: async function (title = "我的直播", areaId = 371) {
try {
if (!data.roomId || data.roomId === -1) {
utils.log("未获取到直播间ID,无法开播", "error");
return { status: false, message: "未获取到直播间ID" };
}
// 准备请求参数 - 与原项目保持一致
const payload = {
room_id: data.roomId,
area_v2: areaId,
type: 2, // 原项目中固定使用type=2
};
// 添加标题参数
if (title) {
payload.title = title;
}
// 添加csrf - 这在原项目中是必须的
if (data.csrf) {
payload.csrf = data.csrf;
payload.csrf_token = data.csrf;
}
const params = livehimeSign(payload);
// 发送开播请求
const response = await utils.post(
"https://api.live.bilibili.com/room/v1/Room/startLive",
params
);
const result = utils.parseJSON(response);
if (result && result.code === 0) {
utils.log("开播成功", "success");
// 保存推流地址
data.rtmpAddr = result.data.rtmp.addr;
data.rtmpCode = result.data.rtmp.code;
data.liveStatus = 1;
utils.saveData();
return { status: true, message: "开播成功", data: result.data };
} else {
utils.log(
"开播失败: " + (result ? result.message : "未知错误"),
"error"
);
return {
status: false,
message: result ? result.message : "开播失败",
};
}
} catch (error) {
utils.log("开播过程异常", "error");
console.error(error);
return { status: false, message: "开播过程发生异常" };
}
},
// 结束直播
stopLive: async function () {
try {
if (!data.roomId || data.roomId === -1) {
utils.log("未获取到直播间ID,无法停播", "error");
return { status: false, message: "未获取到直播间ID" };
}
// 准备请求参数 - 与API文档保持一致
const payload = {
room_id: data.roomId,
platform: "pc",
};
// 添加csrf - 根据API文档,CSRF是必需的
if (data.csrf) {
payload.csrf = data.csrf;
payload.csrf_token = data.csrf;
} else {
utils.log("未获取到CSRF,停播可能会失败", "warning");
}
// 生成签名参数
const params = livehimeSign(payload);
// 确保最终请求中包含CSRF参数
if (data.csrf) {
params.csrf = data.csrf;
params.csrf_token = data.csrf;
}
// 发送停播请求
const response = await utils.post(
"https://api.live.bilibili.com/room/v1/Room/stopLive",
params
);
const result = utils.parseJSON(response);
if (result && result.code === 0) {
utils.log("停播成功", "success");
data.liveStatus = 0;
// 清除推流地址信息
data.rtmpAddr = "";
data.rtmpCode = "";
utils.saveData();
return { status: true, message: "停播成功" };
} else {
utils.log(
"停播失败: " + (result ? result.message : "未知错误"),
"error"
);
return {
status: false,
message: result ? result.message : "停播失败",
};
}
} catch (error) {
utils.log("停播过程异常", "error");
console.error(error);
return { status: false, message: "停播过程发生异常" };
}
},
// 更新直播信息(标题/分区)
updateLiveInfo: async function (title, areaId) {
try {
if (!data.roomId || data.roomId === -1) {
utils.log("未获取到直播间ID,无法更新直播信息", "error");
return { status: false, message: "未获取到直播间ID" };
}
// 准备请求参数
const payload = {
room_id: data.roomId,
};
// 添加标题或分区参数
if (title) {
payload.title = title;
}
if (areaId) {
payload.area_id = areaId;
}
// 添加csrf参数,更新直播信息需要csrf
if (data.csrf) {
payload.csrf = data.csrf;
payload.csrf_token = data.csrf;
} else {
utils.log("未获取到CSRF,更新直播信息可能会失败", "warning");
return { status: false, message: "未获取到CSRF令牌" };
}
// 发送更新请求
const response = await utils.post(
"https://api.live.bilibili.com/room/v1/Room/update",
payload
);
const result = utils.parseJSON(response);
if (result && result.code === 0) {
utils.log("更新直播信息成功", "success");
// 更新本地保存的数据
if (title) {
data.title = title;
}
if (areaId) {
data.areaId = parseInt(areaId);
}
utils.saveData();
return { status: true, message: "更新直播信息成功" };
} else {
utils.log(
"更新直播信息失败: " + (result ? result.message : "未知错误"),
"error"
);
return {
status: false,
message: result ? result.message : "更新直播信息失败",
};
}
} catch (error) {
utils.log("更新直播信息过程异常", "error");
console.error(error);
return { status: false, message: "更新直播信息过程发生异常" };
}
},
};
// 主界面UI
const ui = {
// 显示自定义确认对话框
// 显示消息提示
showMessage: function (message, type = "info") {
// 创建消息元素
const msgEl = document.createElement("div");
msgEl.className = `bili-message bili-message-${type} protected`;
msgEl.innerHTML = `<div class="message-content">${message}</div>`;
// 添加样式
if (!document.querySelector(".bili-message-style")) {
const style = document.createElement("style");
style.className = "bili-message-style";
style.textContent = `
.bili-message {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
border-radius: 4px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
z-index: 10002;
min-width: 240px;
max-width: 400px;
animation: message-fade-in 0.3s, message-fade-out 0.3s 2.7s;
opacity: 0;
}
.bili-message .message-content {
text-align: center;
font-size: 14px;
}
.bili-message-info {
background: #f0f9ff;
border: 1px solid #d0e6fe;
color: #1890ff;
}
.bili-message-success {
background: #f0fff0;
border: 1px solid #d0fed0;
color: #52c41a;
}
.bili-message-warning {
background: #fffbe6;
border: 1px solid #fff6c6;
color: #faad14;
}
.bili-message-error {
background: #fff0f0;
border: 1px solid #fed0d0;
color: #f5222d;
}
@keyframes message-fade-in {
from { opacity: 0; transform: translate(-50%, -20px); }
to { opacity: 1; transform: translate(-50%, 0); }
}
@keyframes message-fade-out {
from { opacity: 1; transform: translate(-50%, 0); }
to { opacity: 0; transform: translate(-50%, -20px); }
}
`;
document.head.appendChild(style);
}
document.body.appendChild(msgEl);
// 显示动画
setTimeout(() => {
msgEl.style.opacity = "1";
}, 0);
// 自动消失
setTimeout(() => {
msgEl.style.opacity = "0";
setTimeout(() => {
if (document.body.contains(msgEl)) {
document.body.removeChild(msgEl);
}
}, 300);
}, 3000);
},
// 更新面板数据的方法
// 使面板可拖动的方法
makePanelDraggable: function(panel) {
let offsetX, offsetY, isDragging = false;
const header = panel.querySelector(".panel-header");
// 阻止冒泡,确保点击面板内容时不触发拖动
const stopPropagation = function(e) {
e.stopPropagation();
};
// 为所有表单元素和按钮添加阻止冒泡
const formElements = panel.querySelectorAll("input, select, button, .btn-primary, .btn-danger, .btn-success, .area-search-container");
formElements.forEach(el => {
el.addEventListener("mousedown", stopPropagation);
});
// 开始拖动事件 - 只在标题栏触发
const dragStart = function(e) {
// 只允许鼠标左键拖动
if (e.button !== 0) return;
// 确保拖动开始于标题栏
if (e.currentTarget === header) {
isDragging = true;
// 获取鼠标在面板中的相对位置
const rect = panel.getBoundingClientRect();
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
// 防止拖动时选中文本
e.preventDefault();
// 添加活动样式
header.style.cursor = "grabbing";
}
};
// 拖动中事件
const dragMove = function(e) {
if (!isDragging) return;
// 计算新位置
const x = e.clientX - offsetX;
const y = e.clientY - offsetY;
// 限制不超出屏幕边界
const maxX = window.innerWidth - panel.offsetWidth;
const maxY = window.innerHeight - panel.offsetHeight;
const boundedX = Math.max(0, Math.min(x, maxX));
const boundedY = Math.max(0, Math.min(y, maxY));
// 设置面板位置
panel.style.left = boundedX + "px";
panel.style.top = boundedY + "px";
panel.style.right = "auto"; // 清除right属性以避免冲突
};
// 结束拖动事件
const dragEnd = function() {
if (isDragging) {
isDragging = false;
// 恢复正常样式
header.style.cursor = "move";
}
};
// 添加事件监听
header.addEventListener("mousedown", dragStart);
document.addEventListener("mousemove", dragMove);
document.addEventListener("mouseup", dragEnd);
// 设置标题栏鼠标样式
header.style.cursor = "move";
// 清理函数 - 当面板被移除时调用
panel.dragCleanup = function() {
document.removeEventListener("mousemove", dragMove);
document.removeEventListener("mouseup", dragEnd);
formElements.forEach(el => {
el.removeEventListener("mousedown", stopPropagation);
});
};
},
updateLivePanel: function(data) {
if (!this._livePanel) return;
const panel = this._livePanel;
// 更新直播状态
const statusValue = panel.querySelector(".status-value");
statusValue.className = `status-value ${data.liveStatus === 1 ? "status-on" : "status-off"}`;
statusValue.textContent = data.liveStatus === 1 ? "直播中" : "未开播";
// 更新直播标题
const titleInput = panel.querySelector("#live-title");
if (data.title) {
titleInput.value = data.title;
}
// 更新分区选择
const parentAreaSelect = panel.querySelector("#parent-area");
const areaIdSelect = panel.querySelector("#area-id");
if (data.areaList && data.areaList.length > 0) {
// 更新主分区选项
parentAreaSelect.innerHTML = "";
data.areaList.forEach(parentArea => {
const option = document.createElement("option");
option.value = parentArea.id;
option.textContent = parentArea.name;
if (data.parentAreaId == parentArea.id) {
option.selected = true;
}
parentAreaSelect.appendChild(option);
});
// 更新子分区选项
areaIdSelect.innerHTML = "";
// 找到当前选中的主分区
const selectedParent = data.areaList.find(p => p.id == data.parentAreaId) || data.areaList[0];
if (selectedParent && selectedParent.list && selectedParent.list.length > 0) {
selectedParent.list.forEach(area => {
const option = document.createElement("option");
option.value = area.id;
option.textContent = area.name;
if (data.areaId == area.id) {
option.selected = true;
}
areaIdSelect.appendChild(option);
});
}
}
// 更新按钮状态
const startLiveBtn = panel.querySelector("#start-live");
const stopLiveBtn = panel.querySelector("#stop-live");
const copyRtmpBtn = panel.querySelector("#copy-rtmp");
const updateLiveInfoBtn = panel.querySelector("#update-live-info");
startLiveBtn.className = data.liveStatus === 1 ? "btn-disabled" : "btn-primary";
stopLiveBtn.className = data.liveStatus !== 1 ? "btn-disabled" : "btn-danger";
copyRtmpBtn.className = !data.rtmpAddr ? "btn-disabled" : "btn-success";
updateLiveInfoBtn.className = data.liveStatus === 1 ? "btn-primary" : "btn-disabled";
},
// 显示自定义确认对话框
showConfirm: function (message, confirmCallback, cancelCallback) {
// 创建对话框容器
const dialog = document.createElement("div");
dialog.className = "bili-confirm-dialog";
dialog.innerHTML = `
<div class="confirm-dialog-content">
<div class="confirm-dialog-message">${message}</div>
<div class="confirm-dialog-buttons">
<button class="btn-cancel">取消</button>
<button class="btn-confirm">确定</button>
</div>
</div>
`;
// 添加样式
const style = document.createElement("style");
style.textContent = `
.bili-confirm-dialog {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10001;
}
.confirm-dialog-content {
background: white;
border-radius: 8px;
width: 300px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
.confirm-dialog-message {
padding: 20px;
text-align: center;
font-size: 16px;
color: #333;
border-bottom: 1px solid #eee;
}
.confirm-dialog-buttons {
display: flex;
padding: 15px;
}
.confirm-dialog-buttons button {
flex: 1;
padding: 8px 0;
border-radius: 4px;
border: none;
cursor: pointer;
margin: 0 5px;
font-size: 14px;
}
.btn-cancel {
background: #f0f0f0;
color: #666;
}
.btn-cancel:hover {
background: #e0e0e0;
}
.btn-confirm {
background: #F44336;
color: white;
}
.btn-confirm:hover {
background: #d32f2f;
}
`;
document.head.appendChild(style);
document.body.appendChild(dialog);
// 绑定按钮事件
const confirmBtn = dialog.querySelector(".btn-confirm");
const cancelBtn = dialog.querySelector(".btn-cancel");
confirmBtn.addEventListener("click", () => {
document.body.removeChild(dialog);
if (typeof confirmCallback === "function") {
confirmCallback();
}
});
cancelBtn.addEventListener("click", () => {
document.body.removeChild(dialog);
if (typeof cancelCallback === "function") {
cancelCallback();
}
});
},
// 创建主界面按钮
createMainButton: function () {
const button = document.createElement("div");
button.className = "bili-live-tool-button protected";
button.innerHTML = `<svg t="1623318424332" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1127" width="24" height="24"><path d="M392.448 275.911111a92.416 92.416 0 1 1-184.832 0 92.416 92.416 0 0 1 184.832 0" fill="#23ADE5" p-id="1128"></path><path d="M826.624 464.583111l-63.4368-235.8784a37.7344 37.7344 0 0 0-46.1312-27.4944l-231.8848 62.1568a37.6832 37.6832 0 0 0-27.4944 46.1312l63.4368 235.8784a37.7344 37.7344 0 0 0 46.1312 27.4944l231.8848-62.1568a37.7344 37.7344 0 0 0 27.4944-46.1312" fill="#23ADE5" p-id="1129"></path><path d="M834.56 938.665911H190.0032c-69.632 0-126.1056-56.32-126.1056-125.7472V211.529311c0-69.4272 56.4736-125.7472 126.1056-125.7472H834.56c69.632 0 126.1056 56.32 126.1056 125.7472v601.4464c0 69.4272-56.4736 125.7472-126.1056 125.7472z m-644.5568-778.5472c-28.2624 0-51.2 22.8864-51.2 51.1488v601.4464c0 28.2624 22.9376 51.1488 51.2 51.1488H834.56c28.2624 0 51.2-22.8864 51.2-51.1488V211.529311c0-28.2624-22.9376-51.1488-51.2-51.1488H190.0032z" fill="#23ADE5" p-id="1130"></path></svg>`;
// 添加样式
const style = document.createElement("style");
style.textContent = `
.bili-live-tool-button {
position: fixed;
right: 20px;
top: 80px;
width: 40px;
height: 40px;
background: white;
border-radius: 50%;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 9999;
}
.bili-live-tool-button:hover {
transform: scale(1.1);
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.3);
}
`;
document.head.appendChild(style);
document.body.appendChild(button);
// 点击事件
button.addEventListener("click", async () => {
const isLoggedIn = await auth.checkLoginStatus();
if (!isLoggedIn) {
// 未登录,启动登录流程
auth.startQRLogin();
} else {
// 已登录,获取用户信息
await auth.getUserInfoFromCookies();
utils.log(
"用户已登录,用户ID: " + data.userId + ", 房间ID: " + data.roomId
);
// // 先获取最新的直播信息
// await liveControls.getPreLiveInfo();
// await liveControls.getRoomInfo();
// await liveControls.getAreaList();
// 显示直播控制面板
ui.showLivePanel(data,true);
}
});
},
// 显示直播面板
// 存储面板引用
_livePanel: null,
showLivePanel: async function (data,ifFresh = false) {
// 检查面板是否已经存在
if (this._livePanel) {
// 如果面板已存在但被隐藏,则显示它
if (this._livePanel.style.display === "none") {
this._livePanel.style.display = "block";
if(ifFresh){
await liveControls.getPreLiveInfo();
await liveControls.getRoomInfo();
await liveControls.getAreaList();
}
// 更新面板数据
this.updateLivePanel(data);
}
return;
}
// 创建面板
const panel = document.createElement("div");
panel.className = "bili-live-panel protected";
// 存储面板引用
this._livePanel = panel;
panel.innerHTML = `
<div class="panel-header">B站直播控制面板<div class="panel-header-controls"><div class="refresh-button" title="刷新数据"><svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg></div></div></div>
<div class="panel-content">
<div class="panel-status">
<div class="status-title">直播状态</div>
<div class="status-value ${data.liveStatus === 1 ? "status-on" : "status-off"
}">
${data.liveStatus === 1 ? "直播中" : "未开播"}
</div>
</div>
<div class="form-group">
<label for="live-title">直播标题</label>
<input type="text" id="live-title" value="${data.title || "我的直播间"
}">
</div>
<div class="form-group">
<label for="parent-area">直播分区</label>
<div style="display: flex; gap: 10px;">
<select id="parent-area" style="flex: 1;">
${data.areaList && data.areaList.length > 0
? data.areaList
.map(
(parentArea) =>
`<option value="${parentArea.id}" ${data.parentAreaId == parentArea.id
? "selected"
: ""
}>${parentArea.name}</option>`
)
.join("")
: `<option value="2">网游</option>
<option value="3">手游</option>
<option value="1">娱乐</option>
<option value="5">电台</option>`
}
</select>
<select id="area-id" style="flex: 1;">
${data.areaList && data.areaList.length > 0
? (() => {
// 找到当前选中的主分区或第一个主分区
const currentParent =
data.areaList.find(
(p) => p.id == data.parentAreaId
) || data.areaList[0];
return currentParent.list
.map(
(area) =>
`<option value="${area.id}" ${data.areaId == area.id
? "selected"
: ""
}>${area.name}</option>`
)
.join("");
})()
: `<option value="371">虚拟主播</option>
<option value="372">唱见电台</option>
<option value="21">视频唱见</option>
<option value="373">舞见</option>
<option value="6">单机游戏</option>`
}
</select>
</div>
<div id="area-search-container" style="margin-top: 10px;">
<input type="text" id="area-search" placeholder="搜索分区..." style="width:100%; padding:5px; border:1px solid #ddd; border-radius:4px;">
<div id="search-results" style="max-height:150px; overflow-y:auto; display:none; position:absolute; z-index:1000; background:white; width:calc(100% - 40px); border:1px solid #ddd; border-radius:4px;"></div>
</div>
</div>
<div class="button-group">
<button id="start-live" class="${data.liveStatus === 1 ? "btn-disabled" : "btn-primary"
}">开始直播</button>
<button id="stop-live" class="${data.liveStatus !== 1 ? "btn-disabled" : "btn-danger"
}">结束直播</button>
<button id="copy-rtmp" class="${!data.rtmpAddr ? "btn-disabled" : "btn-success"
}">复制推流地址</button>
</div>
<div class="button-group" style="margin-top: 10px;">
<button id="update-live-info" class="${data.liveStatus === 1 ? "btn-primary" : "btn-disabled"
}" style="width:100%;">更新直播信息</button>
</div>
</div>
<div class="panel-footer">
<button id="close-panel">关闭</button>
</div>
`;
// 添加样式
const style = document.createElement("style");
style.textContent = `
.bili-live-panel {
position: fixed;
top: 100px;
right: 70px;
background: white;
border-radius: 8px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
width: 350px;
z-index: 10000;
font-family: Arial, sans-serif;
}
.panel-header {
background: #23ADE5;
color: white;
padding: 15px;
border-radius: 8px 8px 0 0;
font-weight: bold;
display: flex;
justify-content: space-between;
align-items: center;
cursor: move; /* 确保标题栏可以作为拖动手柄 */
user-select: none; /* 防止文本被选中影响拖动 */
}
.panel-header-controls {
display: flex;
align-items: center;
}
.refresh-button {
cursor: pointer;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.2);
transition: background-color 0.2s;
}
.refresh-button:hover {
background-color: rgba(255, 255, 255, 0.4);
font-size: 16px;
}
.panel-content {
padding: 20px;
cursor: default; /* 内容区域使用默认鼠标样式 */
}
.panel-footer {
padding: 15px;
text-align: right;
border-top: 1px solid #eee;
}
.panel-status {
margin-bottom: 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
.status-title {
font-weight: bold;
}
.status-value {
padding: 5px 10px;
border-radius: 4px;
font-size: 14px;
}
.status-on {
background: #4CAF50;
color: white;
}
.status-off {
background: #F44336;
color: white;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
font-size: 14px;
}
.form-group input, .form-group select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.button-group {
display: flex;
justify-content: space-between;
margin-top: 20px;
}
.button-group button {
flex: 1;
margin: 0 5px;
}
.button-group button:first-child {
margin-left: 0;
}
.button-group button:last-child {
margin-right: 0;
}
.btn-primary {
background: #23ADE5;
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
}
.btn-danger {
background: #F44336;
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
}
.btn-success {
background: #4CAF50;
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
}
.btn-disabled {
background: #cccccc;
color: #666666;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: not-allowed;
}
#close-panel {
background: #f0f0f0;
border: 1px solid #ddd;
padding: 5px 15px;
border-radius: 4px;
cursor: pointer;
}
#close-panel:hover {
background: #e0e0e0;
}
.btn-primary:hover:not(.btn-disabled) {
background: #1e9cd7;
}
.btn-danger:hover:not(.btn-disabled) {
background: #d32f2f;
}
.btn-success:hover:not(.btn-disabled) {
background: #388e3c;
}
`;
document.head.appendChild(style);
document.body.appendChild(panel);
// 使面板可拖动
this.makePanelDraggable(panel);
// 主分区和子分区联动事件
const parentAreaSelect = panel.querySelector("#parent-area");
const areaIdSelect = panel.querySelector("#area-id");
const searchContainer = panel.querySelector("#area-search-container");
const searchInput = panel.querySelector("#area-search");
const searchResults = panel.querySelector("#search-results");
// 分区搜索功能 - 支持拼音搜索、首字母搜索和area_id搜索
searchInput.addEventListener("input", () => {
const searchTerm = searchInput.value.toLowerCase();
if (searchTerm.length < 1) {
searchResults.style.display = "none";
return;
}
// 清空搜索结果
searchResults.innerHTML = "";
searchResults.style.display = "block";
// 搜索所有子分区
let results = [];
if (data.areaList && data.areaList.length > 0) {
data.areaList.forEach((parentArea) => {
if (parentArea.list) {
parentArea.list.forEach((childArea) => {
// 准备搜索用的数据
const name = childArea.name.toLowerCase();
const pinyin = utils.pinyin.getFullPinyin(childArea.name);
const initials = utils.pinyin.getInitials(childArea.name);
const id = childArea.id.toString();
// 多种匹配方式:名称、拼音、首字母、ID
if (
name.includes(searchTerm) || // 名称匹配
pinyin.includes(searchTerm) || // 拼音匹配
initials.includes(searchTerm) || // 拼音首字母匹配
id === searchTerm
) {
// ID精确匹配
results.push({
parentId: parentArea.id,
parentName: parentArea.name,
id: childArea.id,
name: childArea.name,
pinyin: pinyin,
initials: initials,
});
}
});
}
});
}
// 显示搜索结果
if (results.length > 0) {
results.forEach((result) => {
const resultItem = document.createElement("div");
resultItem.style.padding = "8px";
resultItem.style.cursor = "pointer";
resultItem.style.borderBottom = "1px solid #eee";
resultItem.style.display = "flex";
resultItem.style.justifyContent = "space-between";
resultItem.style.flexDirection = "column";
// 显示更多匹配信息
resultItem.innerHTML = `
<div>
<span>${result.name}</span>
<span style="color:#666; font-size:0.85em; margin-left:6px;">(${result.id})</span>
</div>
<div>
<span style="color:#999; font-size:0.85em;">${result.parentName}</span>
<span style="color:#aaa; font-size:0.8em; margin-left:6px;">${result.initials}</span>
</div>
`;
resultItem.addEventListener("mouseover", () => {
resultItem.style.backgroundColor = "#f0f0f0";
});
resultItem.addEventListener("mouseout", () => {
resultItem.style.backgroundColor = "";
});
resultItem.addEventListener("click", () => {
// 选择父分区
data.parentAreaId = result.parentId;
for (let i = 0; i < parentAreaSelect.options.length; i++) {
if (
parseInt(parentAreaSelect.options[i].value) ===
result.parentId
) {
parentAreaSelect.selectedIndex = i;
break;
}
}
// 更新子分区列表
const selectedParentId = parentAreaSelect.value;
areaIdSelect.innerHTML = "";
if (data.areaList && data.areaList.length > 0) {
const selectedParent = data.areaList.find(
(p) => p.id == selectedParentId
);
if (
selectedParent &&
selectedParent.list &&
selectedParent.list.length > 0
) {
selectedParent.list.forEach((area) => {
const option = document.createElement("option");
option.value = area.id;
option.textContent = area.name;
if (area.id === result.id) {
option.selected = true;
}
areaIdSelect.appendChild(option);
});
}
}
// 选择子分区
data.areaId = result.id;
areaIdSelect.value = result.id;
// 关闭搜索结果
searchResults.style.display = "none";
searchInput.value = result.name;
});
searchResults.appendChild(resultItem);
});
} else {
const noResult = document.createElement("div");
noResult.style.padding = "8px";
noResult.textContent = "没有找到匹配的分区";
searchResults.appendChild(noResult);
}
});
// 点击其他地方关闭搜索结果
document.addEventListener("click", (e) => {
if (e.target !== searchInput && e.target !== searchResults) {
searchResults.style.display = "none";
}
});
parentAreaSelect.addEventListener("change", () => {
const selectedParentId = parentAreaSelect.value;
// 保存当前选择的主分区ID
data.parentAreaId = parseInt(selectedParentId);
searchContainer.style.display = "block";
// 清空并重新填充子分区选项
areaIdSelect.innerHTML = "";
if (data.areaList && data.areaList.length > 0) {
// 找到选中的主分区
const selectedParent = data.areaList.find(
(p) => p.id == selectedParentId
);
if (
selectedParent &&
selectedParent.list &&
selectedParent.list.length > 0
) {
// 填充该主分区下的子分区
selectedParent.list.forEach((area) => {
const option = document.createElement("option");
option.value = area.id;
option.textContent = area.name;
areaIdSelect.appendChild(option);
});
// 默认选中第一个子分区
data.areaId = parseInt(areaIdSelect.options[0].value);
}
}
});
// 子分区点击事件 - 显示搜索框
areaIdSelect.addEventListener("click", () => {
searchContainer.style.display = "block";
});
// 子分区变更事件
areaIdSelect.addEventListener("change", () => {
data.areaId = parseInt(areaIdSelect.value);
});
// 关闭按钮事件
const closeBtn = panel.querySelector("#close-panel");
closeBtn.addEventListener("click", () => {
// 隐藏面板而不是移除
panel.style.display = "none";
});
// 刷新按钮事件
const refreshBtn = panel.querySelector(".refresh-button");
refreshBtn.addEventListener("click", async () => {
// 显示加载提示
ui.showMessage("正在刷新数据...", "info");
try {
// 获取最新的直播状态和信息
await liveControls.getPreLiveInfo();
await liveControls.getRoomInfo();
await liveControls.getAreaList();
// 使用公共方法更新面板
ui.updateLivePanel(data);
// 刷新完成提示
ui.showMessage("数据刷新成功", "success");
utils.log("面板数据已刷新", "success");
} catch (error) {
ui.showMessage("刷新数据失败", "error");
utils.log("刷新数据失败: " + error, "error");
}
});
// 开始直播按钮事件
const startLiveBtn = panel.querySelector("#start-live");
startLiveBtn.addEventListener("click", async () => {
if (data.liveStatus === 1) return;
const title = panel.querySelector("#live-title").value;
const areaId = parseInt(panel.querySelector("#area-id").value);
// 保存选项
data.title = title;
data.areaId = areaId;
utils.saveData();
// 开始直播
const result = await liveControls.startLive(title, areaId);
if (result.status) {
// 更新界面
panel.querySelector(".status-value").className =
"status-value status-on";
panel.querySelector(".status-value").textContent = "直播中";
startLiveBtn.className = "btn-disabled";
panel.querySelector("#stop-live").className = "btn-danger";
panel.querySelector("#copy-rtmp").className = "btn-success";
panel.querySelector("#update-live-info").className = "btn-primary";
utils.log("直播已开始", "success");
} else {
ui.showMessage("开播失败: " + result.message, "error");
}
});
// 结束直播按钮事件
const stopLiveBtn = panel.querySelector("#stop-live");
stopLiveBtn.addEventListener("click", async () => {
if (data.liveStatus !== 1) return;
ui.showConfirm(
'确定要结束本次直播吗?<br><span style="color:#F44336;font-size:14px;">此操作不可撤销</span>',
async () => {
const result = await liveControls.stopLive();
if (result.status) {
// 更新界面
panel.querySelector(".status-value").className =
"status-value status-off";
panel.querySelector(".status-value").textContent = "未开播";
startLiveBtn.className = "btn-primary";
stopLiveBtn.className = "btn-disabled";
panel.querySelector("#update-live-info").className =
"btn-disabled";
utils.log("直播已结束", "success");
} else {
// 使用自定义消息提示替代alert
ui.showMessage("停播失败: " + result.message, "error");
}
}
);
});
// 复制推流地址按钮
const copyRtmpBtn = panel.querySelector("#copy-rtmp");
if (copyRtmpBtn) {
copyRtmpBtn.addEventListener("click", () => {
if (data.rtmpAddr && data.rtmpCode) {
const rtmpUrl = `${data.rtmpAddr}/${data.roomId}/${data.rtmpCode}`;
// 创建临时文本区域复制到剪贴板
const textarea = document.createElement("textarea");
textarea.value = rtmpUrl;
document.body.appendChild(textarea);
textarea.select();
document.execCommand("copy");
document.body.removeChild(textarea);
utils.log("推流地址已复制到剪贴板", "success");
ui.showMessage(
"推流地址已复制到剪贴板,可直接在OBS中使用",
"success"
);
} else {
utils.log("未获取到推流地址,请先开播", "warning");
ui.showMessage("未获取到推流地址,请先开播", "warning");
}
});
}
// 更新直播信息按钮
const updateLiveInfoBtn = panel.querySelector("#update-live-info");
if (updateLiveInfoBtn) {
updateLiveInfoBtn.addEventListener("click", async () => {
if (data.liveStatus !== 1) {
ui.showMessage("只能在直播中更新直播信息", "warning");
return;
}
const title = panel.querySelector("#live-title").value;
const areaId = parseInt(panel.querySelector("#area-id").value);
// 更新直播信息
const result = await liveControls.updateLiveInfo(title, areaId);
if (result.status) {
ui.showMessage("直播信息更新成功", "success");
// 保存最新数据
data.title = title;
data.areaId = areaId;
utils.saveData();
} else {
ui.showMessage("更新失败: " + result.message, "error");
}
});
}
},
};
// 初始化
async function init() {
utils.log("B站直播工具初始化", "info");
// 加载保存的数据
utils.loadData();
// 检查是否已登录
const isLoggedIn = await auth.checkLoginStatus();
if (isLoggedIn) {
await auth.getUserInfoFromCookies();
utils.log("用户已登录,ID: " + data.userId);
// 获取直播间信息
if (data.userId && data.userId !== -1) {
// 获取直播预配置信息
const preLiveResult = await liveControls.getPreLiveInfo();
if (preLiveResult.status) {
utils.log("获取直播预配置信息成功");
}
// 获取分区列表
const areaListResult = await liveControls.getAreaList();
if (areaListResult.status) {
utils.log(
"获取分区列表成功,共" +
(data.areaList ? data.areaList.length : 0) +
"个分区"
);
}
// 获取直播间详细信息
const roomInfoResult = await liveControls.getRoomInfo();
if (roomInfoResult.status) {
utils.log("获取直播间详细信息成功,房间ID: " + data.roomId);
// 确保父分区ID也被设置 - 根据当前子分区找到对应的父分区
if (data.areaList && data.areaId) {
for (const parentArea of data.areaList) {
const childArea = parentArea.list.find(
(area) => area.id === data.areaId
);
if (childArea) {
data.parentAreaId = parentArea.id;
break;
}
}
utils.saveData();
}
}
}
}
// 创建主界面按钮
ui.createMainButton();
}
// 等待页面加载完成后初始化,使用标志位防止重复初始化
let initialized = false;
function safeInit() {
if (!initialized) {
initialized = true;
init();
}
}
if (document.readyState === "complete") {
safeInit();
} else {
window.addEventListener("load", safeInit);
}
})();