// ==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();
})();