IHateLivehime

在个人直播间添加“开始直播”与“结束直播”按钮,让低粉丝数的用户也能绕开强制要求的直播姬开播。

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name        IHateLivehime
// @name:zh-CN  我讨厌直播姬
// @description 在个人直播间添加“开始直播”与“结束直播”按钮,让低粉丝数的用户也能绕开强制要求的直播姬开播。
// @author      Puqns67
// @copyright   2025, Puqns67 (https://github.com/Puqns67)
// @license     GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt
// @version     0.1.5.2
// @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
// @match       https://live.bilibili.com/*
// @require     https://cdn.jsdelivr.net/npm/[email protected]/md5.min.js
// @grant       GM_addStyle
// @grant       GM_setClipboard
// ==/UserScript==

(async function () {
	'use strict';

	const APPKEY = "aae92bc66f3edfab";
	const APPSEC = "af125a0d5279fd576c1b4418a3e8276d";

	function sleep(time) {
		return new Promise((resolve) => setTimeout(resolve, time));
	}

	function api_alert(object) {
		alert(`${object.msg}\n错误代码:${object.code}\n详细信息:\n${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) {
			alert("无法开始直播\nCookie \"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) {
			alert("无法开始直播\n房间已开播!");
			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) {
			api_alert(response);
			return;
		}

		GM_setClipboard(response.data.rtmp.code);
		alert(`开始直播成功!\n推流密钥已经发送至剪贴板~\n\n推流地址:${response.data.rtmp.addr}\n推流密钥:${response.data.rtmp.code}`);
	}

	async function stop_live(room_id) {
		let bili_jct = get_cookie("bili_jct");
		if (bili_jct === null) {
			alert("无法关闭直播\nCookie \"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)) {
			alert("无法关闭直播\n房间未开播!");
			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;
		}

		alert("关闭直播成功!");
	}

	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 start_live_button = document.createElement("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.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);

	console.log("开/下播按钮已添加");

	// 修复在直播间实验室中启用深色模式后无法点击顶栏中元素的问题(上游 BUG)
	GM_addStyle("html[lab-style*='dark'] #head-info-vm.bg-bright-filter::before { pointer-events: none }");
}());