// ==UserScript== // @name amobbs 论坛链接播放按钮 // @namespace https://www.amobbs.com/ // @version 1.0.0 // @description 在 amobbs.com 帖子页面中,为哔哩哔哩视频链接(b23.tv / bilibili.com)后面添加播放按钮,点击后弹窗播放 // @author you // @match https://www.amobbs.com/* // @match https://amobbs.com/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @connect b23.tv // @connect api.bilibili.com // @connect bilibili.com // @connect www.bilibili.com // @run-at document-idle // @license MIT // ==/UserScript== (function () { 'use strict'; var LINK_RE = /^(https?:\/\/)?((www\.)?bilibili\.com\/video\/(BV[0-9A-Za-z]+|av\d+)|b23\.tv\/[0-9A-Za-z]+)/i; var POPUP_ID = 'tm-video-popup'; function extractPlayId(url) { var m; m = url.match(/bilibili\.com\/video\/(BV[0-9A-Za-z]+)/i); if (m) return { bvid: m[1] }; m = url.match(/bilibili\.com\/video\/av(\d+)/i); if (m) return { aid: m[1] }; m = url.match(/b23\.tv\/([0-9A-Za-z]+)/i); if (m) return { short: m[1] }; return null; } function buildEmbedUrl(id) { if (id.bvid) { return 'https://player.bilibili.com/player.html?bvid=' + id.bvid + '&page=1&high_quality=1&danmaku=0'; } if (id.aid) { return 'https://player.bilibili.com/player.html?aid=' + id.aid + '&page=1&high_quality=1&danmaku=0'; } return null; } function resolveShort(code) { var cacheKey = 'b23_' + code; var cached = GM_getValue(cacheKey, null); if (cached) return Promise.resolve(cached); return new Promise(function (resolve, reject) { GM_xmlhttpRequest({ method: 'GET', url: 'https://b23.tv/' + code, redirect: 'follow', timeout: 15000, onload: function (resp) { var finalUrl = (resp.finalUrl && resp.finalUrl !== '') ? resp.finalUrl : ''; if (!finalUrl) { var m = resp.responseText && resp.responseText.match(/url\s*=\s*["']([^"']+)["']/i); if (m) finalUrl = m[1]; } var id = extractPlayId(finalUrl); if (id && (id.bvid || id.aid)) { GM_setValue(cacheKey, id); resolve(id); } else { reject(new Error('短链接解析失败')); } }, onerror: function (e) { reject(new Error('短链接请求失败:' + ((e && e.error) ? e.error : '网络错误'))); }, ontimeout: function () { reject(new Error('短链接请求超时')); } }); }); } function getOrCreatePopup() { var popup = document.getElementById(POPUP_ID); if (popup) return popup; var overlay = document.createElement('div'); overlay.id = POPUP_ID; overlay.style.cssText = [ 'position:fixed', 'left:0', 'top:0', 'width:100%', 'height:100%', 'background:rgba(0,0,0,0.65)', 'z-index:2147483000', 'display:none', 'align-items:center', 'justify-content:center' ].join(';'); var box = document.createElement('div'); box.style.cssText = [ 'position:relative', 'width:min(860px,92vw)', 'background:#0b0b0f', 'border-radius:10px', 'box-shadow:0 12px 48px rgba(0,0,0,0.7)', 'overflow:hidden', 'display:flex', 'flex-direction:column' ].join(';'); var bar = document.createElement('div'); bar.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:#18181c;color:#eee;font-size:14px;cursor:move;'; var title = document.createElement('span'); title.textContent = '视频播放'; title.style.cssText = 'white-space:nowrap;overflow:hidden;text-overflow:ellipsis;'; var close = document.createElement('span'); close.textContent = '✕'; close.title = '关闭 (Esc)'; close.style.cssText = 'cursor:pointer;padding:2px 8px;border-radius:6px;background:#333;font-size:16px;line-height:22px;'; close.addEventListener('mouseenter', function () { close.style.background = '#e33'; }); close.addEventListener('mouseleave', function () { close.style.background = '#333'; }); close.addEventListener('click', hidePopup); bar.appendChild(title); bar.appendChild(close); var body = document.createElement('div'); body.style.cssText = 'position:relative;aspect-ratio:16/9;background:#000;'; var msg = document.createElement('div'); msg.style.cssText = 'position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#ccc;font-size:14px;'; msg.textContent = '解析视频链接中…'; body.appendChild(msg); box.appendChild(bar); box.appendChild(body); overlay.appendChild(box); box._title = title; box._body = body; box._msg = msg; overlay._box = box; document.documentElement.appendChild(overlay); return overlay; } function showPopup(url) { var overlay = getOrCreatePopup(); var box = overlay._box; overlay.style.display = 'flex'; box._body.querySelectorAll('iframe').forEach(function (f) { f.remove(); }); box._msg.style.display = 'flex'; box._msg.textContent = '解析视频链接中…'; var id = extractPlayId(url); var promise; if (id && id.short) { promise = resolveShort(id.short); } else if (id && (id.bvid || id.aid)) { promise = Promise.resolve(id); } else { promise = Promise.reject(new Error('无法识别的视频链接')); } promise.then(function (resolved) { var embedUrl = buildEmbedUrl(resolved); if (!embedUrl) throw new Error('无法生成播放地址'); var iframe = document.createElement('iframe'); iframe.src = embedUrl; iframe.setAttribute('scrolling', 'no'); iframe.setAttribute('frameborder', '0'); iframe.setAttribute('allowfullscreen', 'true'); iframe.setAttribute('allow', 'autoplay; fullscreen'); iframe.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;border:0;'; box._msg.style.display = 'none'; box._body.appendChild(iframe); }).catch(function (err) { box._msg.style.display = 'flex'; box._msg.innerHTML = ''; var line = document.createElement('div'); line.style.cssText = 'text-align:center;'; line.textContent = '播放失败:' + err.message; var openLink = document.createElement('a'); openLink.href = url; openLink.target = '_blank'; openLink.rel = 'noopener'; openLink.textContent = ' 在新窗口打开链接'; openLink.style.cssText = 'display:inline-block;margin-top:8px;color:#5ad;'; line.appendChild(openLink); box._msg.appendChild(line); }); } function hidePopup() { var overlay = document.getElementById(POPUP_ID); if (!overlay) return; overlay.style.display = 'none'; var box = overlay._box; if (box) { box._body.querySelectorAll('iframe').forEach(function (f) { f.remove(); }); } } function makePlayButton(url) { var btn = document.createElement('button'); btn.type = 'button'; btn.textContent = '▶ 播放'; btn.title = '弹窗播放:' + url; btn.style.cssText = [ 'display:inline-block', 'margin:0 4px', 'padding:2px 10px', 'background:#fb7299', 'color:#fff', 'border:0', 'border-radius:6px', 'font-size:12px', 'line-height:18px', 'cursor:pointer', 'vertical-align:middle', 'box-shadow:0 1px 3px rgba(0,0,0,0.25)' ].join(';'); btn.addEventListener('mouseenter', function () { btn.style.background = '#ff85a8'; }); btn.addEventListener('mouseleave', function () { btn.style.background = '#fb7299'; }); btn.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); showPopup(url); }); return btn; } function attachButtons() { document.querySelectorAll('a[href]').forEach(function (a) { if (a.dataset.tmPlayBtn === '1') return; var href = a.href || ''; if (!LINK_RE.test(href)) return; a.dataset.tmPlayBtn = '1'; a.insertAdjacentElement('afterend', makePlayButton(href)); }); } document.addEventListener('keydown', function (e) { if (e.key === 'Escape') { var overlay = document.getElementById(POPUP_ID); if (overlay && overlay.style.display === 'flex') hidePopup(); } }); window.addEventListener('click', function (e) { var overlay = document.getElementById(POPUP_ID); if (overlay && e.target === overlay) hidePopup(); }); document.addEventListener('dragstart', function (e) { var overlay = document.getElementById(POPUP_ID); if (!overlay || overlay.style.display !== 'flex') return; var box = overlay._box; var bar = box.querySelector('div'); if (!bar || !bar.contains(e.target)) return; var shiftX = e.clientX - box.getBoundingClientRect().left; var shiftY = e.clientY - box.getBoundingClientRect().top; function move(ev) { var x = ev.clientX - shiftX; var y = ev.clientY - shiftY; overlay.style.alignItems = 'flex-start'; overlay.style.justifyContent = 'flex-start'; box.style.marginLeft = Math.max(0, x) + 'px'; box.style.marginTop = Math.max(0, y) + 'px'; } function up() { document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up); } document.addEventListener('mousemove', move); document.addEventListener('mouseup', up); }); attachButtons(); var observerStarted = false; function startObserver() { var root = document.body || document.documentElement; if (!root) { document.addEventListener('DOMContentLoaded', startObserver, { once: true }); return; } if (observerStarted) return; observerStarted = true; new MutationObserver(function () { attachButtons(); }).observe(root, { childList: true, subtree: true }); } startObserver(); })();