// ==UserScript== // @name Music Room Final Edition // @namespace music-room-bgm // @version 1.7.0 // @author CHANG // @description 一起在bgm听bgm吧! // @match https://bgm.tv/* // @match https://bangumi.tv/* // @match https://chii.in/* // @grant none // @license MIT // @downloadURL none // ==/UserScript== (function () { 'use strict'; // 1. 配置 WebSocket 地址 (请替换成你部署后的域名) const WORKER_WS = "wss://music-room.mikuorz.workers.dev/room/default"; // 2. 抓取 Bangumi 昵称 const getBgmUser = () => { const nickLink = document.querySelector('#header h1 a.l'); if (nickLink && nickLink.innerText.trim()) return nickLink.innerText.trim(); const avatarLink = document.querySelector('a.avatar.l'); if (avatarLink && avatarLink.innerText.trim()) return avatarLink.innerText.trim(); return "游客"; }; // 3. UI 样式注入 const style = document.createElement("style"); style.innerHTML = ` #music-room-panel { position: fixed; top: 100px; right: 20px; width: 320px; background: rgba(17, 17, 17, 0.98); color: #eee; border-radius: 12px; font-family: sans-serif; z-index: 999999; box-shadow: 0 10px 30px rgba(0,0,0,0.5); border: 1px solid #333; display: flex; flex-direction: column; overflow: hidden; transition: width 0.3s; } #music-room-panel.minimized { width: 45px; height: 45px; border-radius: 50%; } #room-header { background: #222; padding: 10px 15px; cursor: move; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #333; user-select: none; } .header-btns span { margin-left: 10px; cursor: pointer; font-size: 16px; color: #888; } .room-content { padding: 15px; } #statusTag { font-size: 10px; padding: 2px 6px; border-radius: 4px; background: #333; color: #7fd; } #current-title { font-size: 15px; font-weight: bold; margin: 10px 0 2px; color: #fff; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #current-user { font-size: 11px; color: #888; margin-bottom: 8px; } #playlist-container { max-height: 100px; overflow-y: auto; font-size: 12px; color: #999; margin: 10px 0; border-top: 1px solid #222; padding-top: 5px; } .song-item { padding: 4px 0; border-bottom: 1px solid #222; display: flex; justify-content: space-between; gap: 10px; } .song-user { color: #555; font-size: 10px; flex-shrink: 0; } .input-group { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; } .input-group input { background: #111; border: 1px solid #444; color: #fff; padding: 6px; border-radius: 4px; font-size: 12px; } .input-group button { background: #1db954; border: none; color: #fff; padding: 8px; border-radius: 4px; cursor: pointer; font-weight: bold; } #room-audio { width: 100%; height: 35px; filter: invert(90%) hue-rotate(180deg); margin-top: 5px; } #mini-icon { display: none; width: 100%; height: 100%; justify-content: center; align-items: center; font-size: 20px; cursor: pointer; background: #1db954; } #music-room-panel.minimized .room-content, #music-room-panel.minimized #room-header { display: none; } #music-room-panel.minimized #mini-icon { display: flex; } `; document.head.appendChild(style); // 4. DOM 结构 const panel = document.createElement("div"); panel.id = "music-room-panel"; panel.innerHTML = `
🎵
🎵 Music Room
CONNECTING 0人
等待播放
00:00 / 00:00
`; document.body.appendChild(panel); const audio = document.getElementById("room-audio"); const titleText = document.getElementById("current-title"); const userText = document.getElementById("current-user"); const listContainer = document.getElementById("playlist-container"); // 5. 窗口交互逻辑 document.getElementById("btn-min").onclick = (e) => { e.stopPropagation(); panel.classList.add("minimized"); }; document.getElementById("mini-icon").onclick = () => panel.classList.remove("minimized"); document.getElementById("btn-close").onclick = () => { if(confirm("确定退出点歌房?")) panel.style.display = "none"; }; let isDragging = false, offsetX, offsetY; document.getElementById("room-header").onmousedown = (e) => { isDragging = true; offsetX = e.clientX - panel.offsetLeft; offsetY = e.clientY - panel.offsetTop; }; document.onmousemove = (e) => { if (!isDragging || panel.classList.contains("minimized")) return; panel.style.left = (e.clientX - offsetX) + "px"; panel.style.top = (e.clientY - offsetY) + "px"; panel.style.right = "auto"; }; document.onmouseup = () => { isDragging = false; }; // 6. WebSocket 业务逻辑 const ws = new WebSocket(WORKER_WS); ws.onmessage = (e) => { const data = JSON.parse(e.data); if (data.type === "state") { if (!data.current) { titleText.textContent = "暂无播放"; userText.textContent = ""; listContainer.innerHTML = ""; audio.src = ""; return; } titleText.textContent = data.current.title; userText.textContent = `点歌人: ${data.current.user || '匿名'}`; listContainer.innerHTML = "队列:"; data.playlist.forEach(s => { const item = document.createElement("div"); item.className = "song-item"; item.innerHTML = `${s.title}${s.user || ''}`; listContainer.appendChild(item); }); const now = Date.now(); if (now >= data.endsAt) { statusTag.textContent = "BUFFERING"; audio.pause(); return; } statusTag.textContent = "LIVE"; if (audio.src !== data.current.url) { audio.src = data.current.url; audio.load(); } const target = (now - data.startedAt) / 1000; if (Math.abs(audio.currentTime - target) > 2) audio.currentTime = target; if (audio.paused) audio.play().catch(() => statusTag.textContent = "MUTED (Click!)"); } else if (data.type === "online") { document.getElementById("onlineCount").textContent = `${data.count}人`; } }; // 7. 点歌处理 (含美化文件名) document.getElementById("addSong").onclick = async () => { const tI = document.getElementById("songTitle"), uI = document.getElementById("songUrl"), btn = document.getElementById("addSong"); const url = uI.value.trim(); if(!url) return; btn.disabled = true; btn.textContent = "解析中..."; try { const dur = await new Promise((res, rej) => { const a = new Audio(url); a.onloadedmetadata = () => res(a.duration); a.onerror = rej; setTimeout(rej, 8000); }); // 美化文件名逻辑 let autoTitle = url.split('/').pop().split('?')[0]; autoTitle = decodeURIComponent(autoTitle).replace(/%20/g, ' '); ws.send(JSON.stringify({ type: "enqueue", song: { id: Date.now().toString(), title: tI.value.trim() || autoTitle, url, duration: Math.ceil(dur), user: getBgmUser() } })); tI.value = ""; uI.value = ""; } catch(e) { alert("解析失败:链接无法加载或时长读取超时"); } btn.disabled = false; btn.textContent = "解析并点歌"; }; document.getElementById("skip-link").onclick = () => ws.send(JSON.stringify({ type: "skip" })); // 8. 实时时间刷新 setInterval(() => { if (audio.src && !audio.paused) { const cur = Math.floor(audio.currentTime); const dur = Math.floor(audio.duration || 0); const format = (s) => `${Math.floor(s/60)}:${(s%60).toString().padStart(2,'0')}`; document.getElementById("time-info").textContent = `${format(cur)} / ${format(dur)}`; } }, 1000); })();