// ==UserScript== // @name amobbs 论坛链接播放按钮 // @namespace https://www.amobbs.com/ // @version 1.3.2 // @description 在 amobbs.com 帖子页面中,为哔哩哔哩视频链接(b23.tv / bilibili.com)后面添加播放按钮,点击后弹窗播放,支持横竖屏自适应、手动切换与最大化,Windows 风格窗口按钮 // @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 // ==/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'; var ICON_MAX = ''; var ICON_RESTORE = ''; var ICON_CLOSE = ''; 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 fetchVideoMeta(id) { var url; if (id.bvid) { url = 'https://api.bilibili.com/x/web-interface/view?bvid=' + id.bvid; } else if (id.aid) { url = 'https://api.bilibili.com/x/web-interface/view?aid=' + id.aid; } else { return Promise.resolve(null); } return new Promise(function (resolve) { GM_xmlhttpRequest({ method: 'GET', url: url, timeout: 10000, onload: function (resp) { try { var data = JSON.parse(resp.responseText); var dim = data && data.data && data.data.dimension; resolve(dim || null); } catch (e) { resolve(null); } }, onerror: function () { resolve(null); }, ontimeout: function () { resolve(null); } }); }); } function applyOrientationSize(dim, box, mode) { var vw = window.innerWidth; var vh = window.innerHeight; var barH = (box._bar && box._bar.offsetHeight) || 44; var body = box._body; var ratio = 16 / 9; if (dim && dim.width && dim.height) { var w = dim.width, h = dim.height, r = dim.rotate; if (r === 90 || r === 270 || r === -90 || r === -270) { var t = w; w = h; h = t; } if (w > 0 && h > 0) ratio = w / h; } if (mode === 'landscape') { ratio = 16 / 9; } else if (mode === 'portrait') { ratio = 9 / 16; } var availW = vw - 32; var availH = vh - barH - 48; var bw, bh; if (ratio < 1) { bh = Math.min(availH, Math.round(vh * 0.9)); bw = Math.round(bh * ratio); if (bw < 280) bw = 280; if (bw > availW) { bw = availW; bh = Math.round(bw / ratio); } } else { bw = Math.min(availW, 1400); bh = Math.round(bw / ratio); if (bh > availH) { bh = availH; bw = Math.round(bh * ratio); } } body.style.width = '100%'; body.style.height = bh + 'px'; box.style.width = bw + 'px'; box.style.height = (bh + barH) + 'px'; box.style.marginLeft = ''; box.style.marginTop = ''; box._maximized = false; var overlay = box.parentNode; if (overlay && overlay.style) { overlay.style.alignItems = 'center'; overlay.style.justifyContent = 'center'; } if (box._btnAuto && box._btnLand && box._btnPort) { box._btnAuto.style.background = (!mode || mode === 'auto') ? '#fb7299' : '#333'; box._btnLand.style.background = (mode === 'landscape') ? '#fb7299' : '#333'; box._btnPort.style.background = (mode === 'portrait') ? '#fb7299' : '#333'; } if (box._btnMax) { box._btnMax.innerHTML = ICON_MAX; box._btnMax.style.background = 'transparent'; box._btnMax.title = '最大化'; } } 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:stretch;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;'; function makeModeBtn(text, titleText) { var b = document.createElement('button'); b.type = 'button'; b.textContent = text; b.title = titleText; b.style.cssText = 'cursor:pointer;padding:2px 10px;border:0;border-radius:6px;background:#333;color:#eee;font-size:12px;line-height:20px;'; b.addEventListener('mouseenter', function () { if (b.style.background === 'rgb(51, 51, 51)') b.style.background = '#555'; }); b.addEventListener('mouseleave', function () { if (b.style.background === 'rgb(85, 85, 85)') b.style.background = '#333'; }); return b; } var btnAuto = makeModeBtn('自动', '按视频原始比例自适应'); var btnLand = makeModeBtn('横屏', '切换为横屏 16:9'); var btnPort = makeModeBtn('竖屏', '切换为竖屏 9:16'); var btnGroup = document.createElement('div'); btnGroup.style.cssText = 'display:flex;gap:6px;'; btnGroup.appendChild(btnAuto); btnGroup.appendChild(btnLand); btnGroup.appendChild(btnPort); btnAuto.addEventListener('click', function () { box._mode = 'auto'; applyOrientationSize(box._meta, box, 'auto'); }); btnLand.addEventListener('click', function () { box._mode = 'landscape'; applyOrientationSize(box._meta, box, 'landscape'); }); btnPort.addEventListener('click', function () { box._mode = 'portrait'; applyOrientationSize(box._meta, box, 'portrait'); }); function makeWinBtn(html, titleText) { var b = document.createElement('button'); b.type = 'button'; b.innerHTML = html; b.title = titleText; b.style.cssText = 'display:flex;align-items:center;justify-content:center;width:44px;border:0;background:transparent;cursor:default;flex-shrink:0;'; return b; } var btnMax = makeWinBtn(ICON_MAX, '最大化'); btnMax.addEventListener('mouseenter', function () { btnMax.style.background = '#e81123'; }); btnMax.addEventListener('mouseleave', function () { btnMax.style.background = 'transparent'; }); btnMax.addEventListener('click', function () { if (box._maximized) { applyOrientationSize(box._meta, box, box._mode); } else { box._maximized = true; var barH = (box._bar && box._bar.offsetHeight) || 44; box.style.width = '100%'; box.style.height = '100%'; box.style.marginLeft = ''; box.style.marginTop = ''; var overlay = box.parentNode; if (overlay && overlay.style) { overlay.style.alignItems = 'stretch'; overlay.style.justifyContent = 'stretch'; } box._body.style.width = '100%'; box._body.style.height = 'calc(100vh - ' + barH + 'px)'; btnMax.innerHTML = ICON_RESTORE; btnMax.title = '还原'; box._btnAuto.style.background = '#333'; box._btnLand.style.background = '#333'; box._btnPort.style.background = '#333'; } }); var close = makeWinBtn(ICON_CLOSE, '关闭 (Esc)'); close.addEventListener('mouseenter', function () { close.style.background = '#e81123'; }); close.addEventListener('mouseleave', function () { close.style.background = 'transparent'; }); close.addEventListener('click', hidePopup); var leftGroup = document.createElement('div'); leftGroup.style.cssText = 'display:flex;align-items:center;gap:10px;flex:1;min-width:0;padding:10px 14px;cursor:move;'; leftGroup.appendChild(title); leftGroup.appendChild(btnGroup); bar.appendChild(leftGroup); bar.appendChild(btnMax); bar.appendChild(close); var body = document.createElement('div'); body.style.cssText = 'position:relative;width:100%;height:360px;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; box._bar = bar; box._btnAuto = btnAuto; box._btnLand = btnLand; box._btnPort = btnPort; box._btnMax = btnMax; box._mode = 'auto'; box._meta = null; box._maximized = false; overlay._box = box; document.documentElement.appendChild(overlay); return overlay; } function showPopup(url) { var overlay = getOrCreatePopup(); var box = overlay._box; box._mode = 'auto'; box._meta = null; overlay.style.display = 'flex'; box._body.querySelectorAll('iframe').forEach(function (f) { f.remove(); }); box._msg.style.display = 'flex'; box._msg.textContent = '解析视频链接中…'; applyOrientationSize(null, box, 'auto'); 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('无法生成播放地址'); return Promise.all([embedUrl, fetchVideoMeta(resolved)]); }).then(function (pair) { var embedUrl = pair[0]; box._meta = pair[1]; applyOrientationSize(pair[1], box, box._mode); 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; if (box._maximized) return; 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(); })();