NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name IHateLivehime
// @name:zh-CN 我讨厌直播姬
// @description 在个人直播间添加“开始直播”与“结束直播”按钮,让低粉丝数的用户也能绕开强制要求的直播姬开播。
// @author Puqns67
// @copyright 2025-2026, Puqns67 (https://github.com/Puqns67)
// @license GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt
// @version 0.1.6.1
// @icon https://i0.hdslb.com/bfs/static/jinkela/long/images/favicon.ico
// @homepageURL https://github.com/Puqns67/IHateLivehime
// @supportURL https://github.com/Puqns67/IHateLivehime/issues
// @namespace https://github.com/Puqns67
// @downloadURL https://openuserjs.org/install/Puqns67/IHateLivehime.min.user.js
// @updateURL https://openuserjs.org/meta/Puqns67/IHateLivehime.meta.js
// @match https://live.bilibili.com/*
// @require https://cdn.jsdelivr.net/npm/js-md5/build/md5.min.js
// @require https://cdn.jsdelivr.net/gh/datalog/qrcode-svg/qrcode.min.js
// @grant GM_addStyle
// @grant GM_setClipboard
// ==/UserScript==
(async function () {
'use strict';
const APPKEY = "aae92bc66f3edfab";
const APPSEC = "af125a0d5279fd576c1b4418a3e8276d";
class Popup {
constructor() {
this.popup = document.createElement("div");
this.popup.id = "ihatelivehime-popup";
this.title = document.createElement("p");
this.title.id = "ihatelivehime-popup-title";
this.content = document.createElement("div");
this.content.id = "ihatelivehime-popup-content";
this.actions = document.createElement("div");
this.actions.id = "ihatelivehime-popup-actions";
this.close_button = document.createElement("button");
this.close_button.id = "ihatelivehime-popup-close-button";
this.close_button.appendChild(document.createTextNode("关闭"));
this.close_button.addEventListener("click", async () => this.hide());
this.popup.appendChild(this.title);
this.popup.appendChild(this.content);
this.popup.appendChild(this.actions);
this.popup.appendChild(this.close_button);
document.body.appendChild(this.popup);
GM_addStyle(`
#ihatelivehime-popup {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: none;
flex-direction: column;
padding: 15px;
border: 5px solid #f1a9b4ee;
border-radius: 8px;
color: white;
background-color: #a2757cee;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
z-index: 1000;
}
#ihatelivehime-popup > * {
margin-top: 10px;
}
#ihatelivehime-popup-title {
margin: unset;
font-size: 1.8em;
}
#ihatelivehime-popup-content {
display: flex;
flex-direction: column;
font-size: 1.2em;
}
#ihatelivehime-popup-content > svg {
align-self: center;
fill: #ff0055;
}
#ihatelivehime-popup-actions {
display: flex;
gap: 10px;
}
#ihatelivehime-popup-actions > button, #ihatelivehime-popup-close-button {
flex: auto;
padding: 3px 10px;
font-size: 1.2em;
border: none;
border-radius: 5px;
color: white;
background-color: #ff0055ee;
cursor: pointer;
}
`);
}
action_element(action) {
let button = document.createElement("button");
button.textContent = action.title;
switch (action.type) {
case "exec":
button.addEventListener("click", action.value);
break;
case "copy":
button.addEventListener("click", async () => GM_setClipboard(action.value));
break;
default:
throw new Error(`Unknown action type: ${action.type}`);
}
return button;
}
show(title, content = null, actions = null) {
this.title.textContent = title;
this.content.innerHTML = "";
if (content !== null) {
this.content.style.display = null;
if (typeof content === "string")
this.content.textContent = content;
else
this.content.appendChild(content);
} else {
this.content.style.display = "none";
}
this.actions.innerHTML = "";
if (actions !== null) {
this.actions.style.display = null;
actions.forEach(action => this.actions.appendChild(this.action_element(action)));
} else {
this.actions.style.display = "none";
}
this.popup.style.display = "flex";
}
hide() {
this.popup.style.display = "none";
}
}
let popup = new Popup();
function sleep(time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
function api_alert(object) {
popup.show("请求接口错误", `错误代码:${object.code}<br/>${object.message}`, [
{ title: "复制错误详情", type: "copy", value: JSON.stringify(object) }
]);
}
async function get_element_with_wait(selectors, timeout = 3200, retry_count = 32) {
let timeout_once = timeout / 32;
let retry = 1;
while (retry <= retry_count) {
let result = document.querySelector(selectors);
if (result !== null) return result;
await sleep(timeout_once);
retry++;
}
return null;
}
function get_cookie(name) {
let re = new RegExp(`(?:^|; *)${name}=([^=]+?)(?:;|$)`).exec(document.cookie);
return re === null ? null : re[1];
}
async function get_timestemp() {
return await fetch("https://api.bilibili.com/x/report/click/now").then(r => r.json());
}
async function get_current_liveime_version() {
return await fetch('https://api.live.bilibili.com/xlive/app-blink/v1/liveVersionInfo/getHomePageLiveVersion?system_version=2').then(r => r.json());
}
async function get_current_user_info() {
return await fetch("https://api.bilibili.com/x/space/myinfo", { "credentials": "include" }).then(r => r.json());
}
async function get_room_info_by_room_id(id) {
return await fetch(`https://api.live.bilibili.com/room/v1/Room/get_info?room_id=${id}`, { "credentials": "include" }).then(r => r.json());
}
async function get_room_info_by_user_id(id) {
return await fetch(`https://api.live.bilibili.com/live_user/v1/Master/info?uid=${id}`, { "credentials": "include" }).then(r => r.json());
}
async function start_live(room_id) {
let bili_jct = get_cookie("bili_jct");
if (bili_jct === null) {
popup.show("无法开始直播", 'Cookie "bili_jct" 不存在,请尝试重新登录!');
return;
}
let room_info = await get_room_info_by_room_id(room_id);
if (room_info.code !== 0) {
api_alert(room_info);
return;
}
if (room_info.data.live_status === 1) {
popup.show("无法开始直播", "房间已开播!");
return;
}
let current_timestemp = await get_timestemp();
if (current_timestemp.code !== 0) {
api_alert(current_timestemp);
return;
}
let current_liveime_version = await get_current_liveime_version();
if (current_liveime_version.code !== 0) {
api_alert(current_liveime_version);
return;
}
let data = {
"appkey": APPKEY,
"area_v2": room_info.data.area_id,
"build": current_liveime_version.data.build,
"csrf": bili_jct,
"platform": "pc_link",
"room_id": room_id,
"ts": current_timestemp.data.now,
"version": current_liveime_version.data.curr_version
};
data.sign = md5(new URLSearchParams(data).toString() + APPSEC);
let params = new URLSearchParams(data).toString();
let response = await fetch("https://api.live.bilibili.com/room/v1/Room/startLive?" + params, { "method": "POST", "credentials": "include" }).then(r => r.json());
if (response.code !== 0) {
switch (response.code) {
case 60024:
popup.show("需要人脸验证", QRCode({ msg: response.data.qr, pad: 0 }), [
{ title: "继续", type: "exec", value: async () => start_live(room_id) }
]);
break;
default:
api_alert(response);
break;
}
return;
}
popup.show("开始直播成功", null, [
{ title: "复制推流地址", type: "copy", value: response.data.rtmp.addr },
{ title: "复制推流密钥", type: "copy", value: response.data.rtmp.code }
]);
}
async function stop_live(room_id) {
let bili_jct = get_cookie("bili_jct");
if (bili_jct === null) {
popup.show("无法关闭直播", 'Cookie "bili_jct" 不存在,请尝试重新登录!');
return;
}
let room_info = await get_room_info_by_room_id(room_id);
if (room_info.code !== 0) {
api_alert(room_info);
return;
}
if ([0, 2].includes(room_info.data.live_status)) {
popup.show("无法关闭直播", "房间未开播!");
return;
}
let params = new URLSearchParams({
"platform": "pc_link",
"room_id": room_id,
"csrf": bili_jct
}).toString();
let response = await fetch("https://api.live.bilibili.com/room/v1/Room/stopLive?" + params, { "method": "POST", "credentials": "include" }).then(r => r.json());
if (response.code !== 0) {
api_alert(response);
return;
}
popup.show("关闭直播成功!");
}
let path_room_id = /^\/(\d+)/.exec(document.location.pathname);
if (path_room_id === null) {
console.warn("当前页面并非直播间");
return;
}
let room_id = Number(path_room_id[1]);
let current_user_info = await get_current_user_info();
if (current_user_info.code === -101) {
console.warn("账户未登录");
return;
}
let current_room_info = await get_room_info_by_room_id(room_id);
if (current_user_info.data.mid !== current_room_info.data.uid) {
console.warn("当前直播间不为自己的直播间");
return;
}
let button_area = await get_element_with_wait(".left-header-area");
if (button_area === null) {
console.warn("页面元素不存在");
return;
}
// 不显示“关注主播”按钮(在自己的直播间无法关注自己)
let follow_button = await get_element_with_wait(".follow-ctnr");
if (follow_button !== null)
follow_button.style.display = "none";
let start_live_button = document.createElement("button");
start_live_button.id = "ihatelivehime-start-live-button";
start_live_button.appendChild(document.createTextNode("开始直播"));
start_live_button.addEventListener("click", async () => start_live(room_id));
let stop_live_button = document.createElement("button");
stop_live_button.id = "ihatelivehime-stop-live-button";
stop_live_button.appendChild(document.createTextNode("结束直播"));
stop_live_button.addEventListener("click", async () => stop_live(room_id));
button_area.appendChild(start_live_button);
button_area.appendChild(stop_live_button);
GM_addStyle(`
#ihatelivehime-start-live-button, #ihatelivehime-stop-live-button {
margin-left: 10px;
padding: 2px 10px;
color: white;
font-size: 1em;
font-weight: bold;
background-color: #ef6f82;
cursor: pointer;
border: unset;
border-radius: 8px;
}
`);
console.log("开/下播按钮已添加");
// 修复在直播间实验室中启用深色模式后无法点击顶栏中元素的问题(上游 BUG)
GM_addStyle("html[lab-style*='dark'] #head-info-vm.bg-bright-filter::before { pointer-events: none }");
}());