// ==UserScript==
// @name MP4 转 M3U8 外链工具
// @namespace https://github.com/userscripts/mp4-to-m3u8
// @version 1.0.0
// @description 在任意网页上检测 MP4 视频链接,一键转换为 M3U8 外链,支持自定义服务器地址
// @author Auto
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant GM_setClipboard
// @grant GM_notification
// @connect *
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
/* ========================================================
* 配置项(可在面板中修改并持久保存)
* ======================================================== */
const CONFIG = {
// 你的后端转换服务地址(见下方 Node.js 服务说明)
serverUrl: GM_getValue('serverUrl', 'http://localhost:3700'),
// 切片时长(秒)
segmentTime: GM_getValue('segmentTime', 10),
// 外链基础 URL(留空则使用服务器本地路径)
baseUrl: GM_getValue('baseUrl', ''),
};
/* ========================================================
* 样式注入
* ======================================================== */
GM_addStyle(`
#m3u8-fab {
position: fixed;
right: 20px;
bottom: 80px;
width: 48px;
height: 48px;
border-radius: 50%;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
color: #fff;
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 16px rgba(99,102,241,.5);
z-index: 2147483647;
user-select: none;
transition: transform .2s, box-shadow .2s;
}
#m3u8-fab:hover { transform: scale(1.1); box-shadow: 0 6px 20px rgba(99,102,241,.7); }
#m3u8-panel {
position: fixed;
right: 20px;
bottom: 140px;
width: 400px;
max-height: 80vh;
background: #1e1e2e;
color: #cdd6f4;
border-radius: 14px;
box-shadow: 0 12px 40px rgba(0,0,0,.6);
z-index: 2147483646;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 13px;
display: none;
flex-direction: column;
overflow: hidden;
animation: m3u8-slide-in .2s ease;
}
@keyframes m3u8-slide-in {
from { opacity:0; transform: translateY(12px); }
to { opacity:1; transform: translateY(0); }
}
#m3u8-panel.open { display: flex; }
.m3u8-header {
padding: 14px 16px 10px;
background: #181825;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid rgba(255,255,255,.06);
}
.m3u8-header h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #cba6f7;
display: flex;
align-items: center;
gap: 6px;
}
.m3u8-close {
cursor: pointer;
color: #6c7086;
font-size: 18px;
line-height: 1;
padding: 2px 4px;
border-radius: 4px;
}
.m3u8-close:hover { color: #cdd6f4; background: rgba(255,255,255,.06); }
.m3u8-tabs {
display: flex;
background: #181825;
border-bottom: 1px solid rgba(255,255,255,.06);
}
.m3u8-tab {
flex: 1;
padding: 8px 0;
text-align: center;
cursor: pointer;
color: #6c7086;
font-size: 12px;
transition: color .15s;
}
.m3u8-tab.active {
color: #cba6f7;
border-bottom: 2px solid #cba6f7;
}
.m3u8-body {
flex: 1;
overflow-y: auto;
padding: 12px 14px;
}
.m3u8-body::-webkit-scrollbar { width: 4px; }
.m3u8-body::-webkit-scrollbar-thumb { background: #45475a; border-radius: 4px; }
.m3u8-section { margin-bottom: 14px; }
.m3u8-label {
font-size: 11px;
color: #6c7086;
margin-bottom: 4px;
text-transform: uppercase;
letter-spacing: .5px;
}
.m3u8-input {
width: 100%;
padding: 7px 10px;
background: #313244;
border: 1px solid rgba(255,255,255,.08);
border-radius: 7px;
color: #cdd6f4;
font-size: 12px;
outline: none;
box-sizing: border-box;
transition: border-color .15s;
}
.m3u8-input:focus { border-color: #cba6f7; }
.m3u8-row { display: flex; gap: 8px; }
.m3u8-row .m3u8-input { flex: 1; }
.m3u8-btn {
padding: 7px 14px;
border: none;
border-radius: 7px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
transition: all .15s;
white-space: nowrap;
}
.m3u8-btn-primary {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
color: #fff;
}
.m3u8-btn-primary:hover { filter: brightness(1.1); }
.m3u8-btn-primary:disabled { opacity: .5; cursor: not-allowed; filter: none; }
.m3u8-btn-ghost {
background: rgba(255,255,255,.06);
color: #cdd6f4;
}
.m3u8-btn-ghost:hover { background: rgba(255,255,255,.1); }
.m3u8-result {
background: #181825;
border-radius: 8px;
padding: 10px;
margin-top: 10px;
display: none;
}
.m3u8-result.show { display: block; }
.m3u8-result-url {
font-family: "SF Mono","Fira Code",monospace;
font-size: 11px;
color: #a6e3a1;
word-break: break-all;
margin-bottom: 8px;
}
.m3u8-result-btns { display: flex; gap: 6px; flex-wrap: wrap; }
.m3u8-link-item {
background: #313244;
border-radius: 8px;
padding: 8px 10px;
margin-bottom: 6px;
cursor: pointer;
transition: background .15s;
display: flex;
align-items: center;
gap: 8px;
}
.m3u8-link-item:hover { background: #45475a; }
.m3u8-link-item.selected { border: 1px solid #cba6f7; }
.m3u8-link-text {
flex: 1;
font-size: 11px;
color: #89b4fa;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.m3u8-link-badge {
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
background: rgba(137,180,250,.15);
color: #89b4fa;
}
.m3u8-status {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
padding: 6px 0;
}
.m3u8-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #6c7086;
flex-shrink: 0;
}
.m3u8-status-dot.running {
background: #f9e2af;
animation: m3u8-pulse .8s infinite;
}
.m3u8-status-dot.ok { background: #a6e3a1; }
.m3u8-status-dot.err { background: #f38ba8; }
@keyframes m3u8-pulse {
0%,100% { opacity:1; } 50% { opacity:.3; }
}
.m3u8-progress {
height: 4px;
background: #313244;
border-radius: 4px;
margin-top: 8px;
overflow: hidden;
}
.m3u8-progress-bar {
height: 100%;
background: linear-gradient(90deg, #6366f1, #a6e3a1);
border-radius: 4px;
width: 0%;
transition: width .3s;
}
.m3u8-settings-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px solid rgba(255,255,255,.04);
}
.m3u8-settings-row:last-child { border-bottom: none; }
.m3u8-settings-key { color: #a6adc8; font-size: 12px; }
.m3u8-settings-val { color: #cdd6f4; font-size: 12px; }
.m3u8-save-tip {
font-size: 11px;
color: #a6e3a1;
margin-top: 6px;
display: none;
}
.m3u8-empty {
text-align: center;
padding: 24px 0;
color: #45475a;
font-size: 12px;
}
`);
/* ========================================================
* DOM 构建
* ======================================================== */
const fab = document.createElement('div');
fab.id = 'm3u8-fab';
fab.title = 'MP4 → M3U8 工具';
fab.innerHTML = '📹';
document.body.appendChild(fab);
const panel = document.createElement('div');
panel.id = 'm3u8-panel';
panel.innerHTML = `
M3U8 外链地址
✓ 已保存
📖 后端服务说明
此脚本需配合本地 Node.js 转换服务使用:
1. 安装依赖:
npm install express cors axios
2. 运行服务(需已安装 ffmpeg):
node mp4_to_m3u8_server.js
服务默认监听 http://localhost:3700
`;
document.body.appendChild(panel);
/* ========================================================
* 状态
* ======================================================== */
let currentTab = 'convert';
let detectedLinks = [];
let selectedLink = null;
let resultM3u8Url = '';
let resultPlayerUrl = '';
/* ========================================================
* 工具函数
* ======================================================== */
function setStatus(msg, state = '') {
const dot = document.getElementById('m3u8-dot');
const txt = document.getElementById('m3u8-status-text');
if (dot) {
dot.className = 'm3u8-status-dot' + (state ? ` ${state}` : '');
}
if (txt) txt.textContent = msg;
}
function setProgress(pct) {
const bar = document.getElementById('m3u8-progress');
const fill = document.getElementById('m3u8-progress-bar');
if (!bar || !fill) return;
if (pct < 0) {
bar.style.display = 'none';
} else {
bar.style.display = 'block';
fill.style.width = pct + '%';
}
}
function showResult(m3u8Url, playerUrl) {
resultM3u8Url = m3u8Url;
resultPlayerUrl = playerUrl;
const box = document.getElementById('m3u8-result');
const urlEl = document.getElementById('m3u8-result-url');
if (box) box.classList.add('show');
if (urlEl) urlEl.textContent = m3u8Url;
}
function copyText(text) {
try {
GM_setClipboard(text);
GM_notification({ title: 'M3U8 外链已复制', text: text.slice(0, 80), timeout: 2500 });
} catch (e) {
navigator.clipboard.writeText(text).catch(() => {});
}
}
/* ========================================================
* 页面视频链接检测
* ======================================================== */
function detectVideoLinks() {
const found = new Map();
// 1.