// ==UserScript== // @name B站网页版一键开播/关播 // @namespace http://tampermonkey.net/ // @version 0.8 // @description 在B站直播姬网页版添加按钮,动态获取RoomID和分区,选择后一键开播/关播,并用HTML展示结果及复制按钮(成功信息手动关闭)。 // @author YourName // @match https://link.bilibili.com/p/center/index* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @connect api.live.bilibili.com // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/536078/B%E7%AB%99%E7%BD%91%E9%A1%B5%E7%89%88%E4%B8%80%E9%94%AE%E5%BC%80%E6%92%AD%E5%85%B3%E6%92%AD.user.js // @updateURL https://update.greasyfork.icu/scripts/536078/B%E7%AB%99%E7%BD%91%E9%A1%B5%E7%89%88%E4%B8%80%E9%94%AE%E5%BC%80%E6%92%AD%E5%85%B3%E6%92%AD.meta.js // ==/UserScript== (function() { 'use strict'; let currentRoomInfo = null; let availableAreas = null; let csrfTokenCache = null; let resultBoxTimeoutId = null; // Store timeout ID globally for the result box // 1. 函数:从 cookie 中获取 CSRF token (bili_jct) function getCsrfToken() { if (csrfTokenCache) return csrfTokenCache; const cookies = document.cookie.split(';'); for (let i = 0; i < cookies.length; i++) { let cookie = cookies[i].trim(); if (cookie.startsWith('bili_jct=')) { csrfTokenCache = cookie.substring('bili_jct='.length); return csrfTokenCache; } } return null; } // Helper function for making API requests function makeApiRequest(options) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ ...options, headers: { 'Content-Type': options.method === 'POST' ? 'application/x-www-form-urlencoded; charset=UTF-8' : undefined, 'Referer': 'https://live.bilibili.com/p/html/web-hime/index.html', 'Origin': 'https://live.bilibili.com', ...(options.headers || {}) }, onload: function(response) { try { const data = JSON.parse(response.responseText); if (data.code === 0 || (options.url.includes('stopLive') && data.msg === "重复关播")) { resolve(data); } else { reject(new Error(`API Error (${options.url}): ${data.code} - ${data.message || data.msg || 'Unknown API error'}`)); } } catch (e) { console.error("Raw response for error:", response.responseText); reject(new Error(`JSON Parse Error (${options.url}): ${e.message}`)); } }, onerror: function(error) { reject(new Error(`Request Error (${options.url}): ${JSON.stringify(error)}`)); }, ontimeout: function() { reject(new Error(`Request Timeout (${options.url})`)); } }); }); } // 2. 函数:获取房间信息 (RoomID, 当前分区等) async function fetchRoomInfo(forceRefresh = false) { if (currentRoomInfo && !forceRefresh) return currentRoomInfo.data; try { const response = await makeApiRequest({ method: 'GET', url: 'https://api.live.bilibili.com/xlive/app-blink/v1/room/GetInfo?platform=pc' }); currentRoomInfo = response; return response.data; } catch (error) { displayResultMessage(`获取房间信息失败: ${error.message}`, 'error'); console.error('获取房间信息失败:', error); throw error; } } // 3. 函数:获取所有直播分区 async function fetchAreaList() { if (availableAreas) return availableAreas.data; try { const response = await makeApiRequest({ method: 'GET', url: 'https://api.live.bilibili.com/room/v1/Area/getList?show_pinyin=1' }); availableAreas = response; return response.data; } catch (error) { displayResultMessage(`获取分区列表失败: ${error.message}`, 'error'); console.error('获取分区列表失败:', error); throw error; } } // 4. 函数:执行开播请求 async function startLiveStream(roomId, areaV2) { const csrfToken = getCsrfToken(); if (!csrfToken) { displayResultMessage('错误:无法获取到 CSRF token (bili_jct)。请确保您已登录B站。', 'error'); return; } const build = '8786'; const platform = 'pc_link'; const formData = new URLSearchParams(); formData.append('room_id', roomId); formData.append('platform', platform); formData.append('area_v2', areaV2); formData.append('build', build); formData.append('csrf', csrfToken); formData.append('csrf_token', csrfToken); console.log('发送开播请求,数据:', Object.fromEntries(formData)); displayResultMessage('正在尝试开播,请稍候...', 'info', false); // Display "trying to start" message, don't auto-dismiss try { const data = await makeApiRequest({ method: 'POST', url: 'https://api.live.bilibili.com/room/v1/Room/startLive', data: formData.toString(), }); await fetchRoomInfo(true); if (data.data && data.data.rtmp) { const rtmpAddr = data.data.rtmp.addr; const rtmpCode = data.data.rtmp.code; const liveKey = data.data.live_key; const fullRtmpUrl = `${rtmpAddr}${rtmpCode}`; const messageHtml = `
OBS等软件通常需要分别填写“服务器地址”和“串流密钥”。
`; // 修改此处:autoDismiss 设置为 false displayResultMessage(messageHtml, 'success', false); // <<<< MODIFIED HERE } else { const errorDetail = `开播成功,但未找到完整的推流信息。API响应:${JSON.stringify(data, null, 2)}`; displayResultMessage(errorDetail, 'warning', true, 10000); // Warnings can auto-dismiss } hideAreaSelectionModal(); } catch (error) { displayResultMessage(`开播失败: ${error.message}`, 'error'); // Errors can auto-dismiss console.error('开播失败:', error); } } // 新增:执行关播请求的函数 async function stopLiveStream() { const stopButton = document.getElementById('customStopLiveButton'); if(stopButton) { stopButton.disabled = true; stopButton.textContent = '正在关播...'; } const csrfToken = getCsrfToken(); if (!csrfToken) { displayResultMessage('错误:无法获取到 CSRF token (bili_jct)。请确保您已登录B站。', 'error'); if(stopButton) { stopButton.disabled = false; stopButton.textContent = '一键关播'; } return; } let roomIdToStop = null; try { const roomData = await fetchRoomInfo(); roomIdToStop = roomData.room_id; } catch (e) { if(stopButton) { stopButton.disabled = false; stopButton.textContent = '一键关播'; } return; } if (!roomIdToStop) { displayResultMessage('错误:无法获取房间ID以进行关播。', 'error'); if(stopButton) { stopButton.disabled = false; stopButton.textContent = '一键关播'; } return; } const platform = 'pc_link'; const formData = new URLSearchParams(); formData.append('room_id', roomIdToStop); formData.append('platform', platform); formData.append('csrf', csrfToken); formData.append('csrf_token', csrfToken); console.log('发送关播请求,数据:', Object.fromEntries(formData)); displayResultMessage('正在尝试关播,请稍候...', 'info', false); try { const data = await makeApiRequest({ method: 'POST', url: 'https://api.live.bilibili.com/room/v1/Room/stopLive', data: formData.toString(), }); await fetchRoomInfo(true); let message = `关播操作已发送。状态: ${data.data && data.data.status ? data.data.status : '未知'}`; if (data.msg === "重复关播") { message = "当前直播间未在直播状态,或已成功关播。"; displayResultMessage(message, 'info'); // Auto-dismiss for info/warnings } else if (data.code === 0) { message = `关播成功!当前状态: ${data.data && data.data.status ? data.data.status : 'PREPARING'}`; displayResultMessage(message, 'success'); // Auto-dismiss for success } else { displayResultMessage(`关播响应异常: ${data.message || data.msg}`, 'warning'); } console.log('关播API响应:', data); } catch (error) { displayResultMessage(`关播失败: ${error.message}`, 'error'); console.error('关播失败:', error); } finally { if(stopButton) { stopButton.disabled = false; stopButton.textContent = '一键关播'; } } } // 显示结果信息的函数 function displayResultMessage(message, type = 'info', autoDismiss = true, duration = 5000) { let resultBox = document.getElementById('userscriptResultBox'); if (!resultBox) { resultBox = document.createElement('div'); resultBox.id = 'userscriptResultBox'; document.body.appendChild(resultBox); const closeButton = document.createElement('button'); closeButton.id = 'resultBoxCloseButton'; closeButton.innerHTML = '×'; // HTML entity for multiplication sign (X) closeButton.onclick = () => { resultBox.style.display = 'none'; if (resultBoxTimeoutId) clearTimeout(resultBoxTimeoutId); // Clear timeout if manually closed }; resultBox.appendChild(closeButton); } let messageContent = resultBox.querySelector('.message-content'); if (!messageContent) { messageContent = document.createElement('div'); messageContent.className = 'message-content'; if (resultBox.firstChild && resultBox.firstChild.id === 'resultBoxCloseButton' && resultBox.firstChild.nextSibling) { resultBox.insertBefore(messageContent, resultBox.firstChild.nextSibling); } else if (resultBox.firstChild && resultBox.firstChild.id === 'resultBoxCloseButton') { resultBox.appendChild(messageContent); } else { resultBox.appendChild(messageContent); } } messageContent.innerHTML = message; resultBox.className = ''; // Clear existing classes before adding new ones resultBox.classList.add('userscript-result-box-base'); // Add base class resultBox.classList.add(`userscript-result-box-${type}`); // Add type-specific class resultBox.style.display = 'block'; messageContent.querySelectorAll('.copy-btn').forEach(button => { button.onclick = (e) => { const targetId = e.target.getAttribute('data-clipboard-target'); const inputElement = document.querySelector(targetId); if (inputElement) { inputElement.select(); inputElement.setSelectionRange(0, 99999); try { document.execCommand('copy'); e.target.textContent = '已复制!'; setTimeout(() => { e.target.textContent = '复制'; }, 1500); } catch (err) { console.error('复制失败:', err); e.target.textContent = '复制失败'; setTimeout(() => { e.target.textContent = '复制'; }, 1500); } if (window.getSelection) { window.getSelection().removeAllRanges(); } else if (document.selection) { document.selection.empty(); } } }; }); if (resultBoxTimeoutId) { // Clear any existing timeout clearTimeout(resultBoxTimeoutId); resultBoxTimeoutId = null; } if (autoDismiss) { resultBoxTimeoutId = setTimeout(() => { if (resultBox) resultBox.style.display = 'none'; }, duration); } } // 5. 创建和管理分区选择模态框 function createAreaSelectionModal() { if (document.getElementById('areaSelectionModal')) return; const modalHTML = `