Skip to content

Instantly share code, notes, and snippets.

@wbmins
Last active November 2, 2025 09:56
Show Gist options
  • Select an option

  • Save wbmins/9e30e6c48bd583459846665cc410819b to your computer and use it in GitHub Desktop.

Select an option

Save wbmins/9e30e6c48bd583459846665cc410819b to your computer and use it in GitHub Desktop.
[tampermonkey ] jable download 脚本,配合 https://github.com/nilaoda/N_m3u8DL-RE使用
// ==UserScript==
// @name Jable 视频下载
// @namespace http://tampermonkey.net/
// @version 1.4
// @description 在 Jable 视频页添加大下载图标按钮,捕获到 m3u8 后再添加按钮
// @author Pluto
// @match https://en.jable.tv/videos/*
// @grant GM_xmlhttpRequest
// @connect 192.168.1.6 //这里指向下面go程序的ip
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
let m3u8Url = null;
let btnCreated = false;
// 创建全局浮动提示函数
function showTip(message, color='green', duration=2000) {
const tip = document.createElement('div');
tip.innerText = message;
tip.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 8px 12px;
background: ${color};
color: #fff;
font-size: 14px;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
z-index: 9999;
opacity: 0;
transition: opacity 0.3s;
`;
document.body.appendChild(tip);
requestAnimationFrame(() => tip.style.opacity = 1);
setTimeout(() => {
tip.style.opacity = 0;
setTimeout(() => document.body.removeChild(tip), 300);
}, duration);
}
// 等待元素出现
function waitForElement(selector, timeout = 5000) {
return new Promise((resolve, reject) => {
const interval = 100;
let elapsed = 0;
const timer = setInterval(() => {
const el = document.querySelector(selector);
if (el) {
clearInterval(timer);
resolve(el);
} else if ((elapsed += interval) >= timeout) {
clearInterval(timer);
reject('元素超时未找到: ' + selector);
}
}, interval);
});
}
// 创建下载按钮
async function createDownloadButton() {
if (btnCreated) return; // 避免重复创建
try {
const container = await waitForElement('.my-3');
const titleEl = document.querySelector('.header-left h4');
let title = '未知';
if (titleEl) {
const parts = titleEl.innerText.trim().split(' ');
title = parts.length > 0 ? parts[0] : '未知';
}
const btn = document.createElement('button');
btn.innerText = '⬇️';
btn.style.cssText = `
cursor: pointer;
background-color: transparent;
color: inherit;
border: none;
border-radius: 16px;
font-size: 28px;
`;
const tip = document.createElement('span');
tip.style.cssText = `
margin-left: 8px;
font-size: 14px;
color: green;
opacity: 0;
transition: opacity 0.3s;
`;
container.appendChild(tip);
btn.addEventListener('click', () => {
if (m3u8Url) { //这里指向下面go程序的ip
const apiUrl = `http://192.168.1.6:8080/add?url=${encodeURIComponent(m3u8Url)}&filename=${encodeURIComponent(title)}`;
console.log('发送请求到:', apiUrl);
GM_xmlhttpRequest({
method: 'GET',
url: apiUrl,
onload: function(res) {
try {
const data = JSON.parse(res.responseText);
if (data.message === '下载任务添加成功') {
showTip(`✅ ${data.message}(队列: ${data.queue_size})`, 'green');
} else {
showTip(`❌ ${data.message}(队列: ${data.queue_size})`, 'orange');
}
} catch (e) {
showTip('⚠️ 返回解析失败', 'orange');
}
},
onerror: function(err) {
showTip('❌ 请求失败', 'red');
console.error(err);
}
});
} else {
showTip('⚠️ m3u8 URL 未捕获', 'orange');
console.warn('⚠️ m3u8 URL 还未捕获到,请稍候刷新页面或等待视频请求发出。');
}
});
container.appendChild(btn);
btnCreated = true;
} catch (err) {
console.error(err);
}
}
// 拦截 fetch 请求,获取 m3u8 链接
const originalFetch = window.fetch;
window.fetch = async function(input, init) {
const response = await originalFetch(input, init);
try {
let url = typeof input === 'string' ? input : input.url;
if (url.includes('.m3u8')) {
if (!m3u8Url) {
m3u8Url = url;
console.log('捕获到 m3u8 URL:', m3u8Url);
createDownloadButton(); // 捕获后创建按钮
}
}
} catch(e) {
console.error(e);
}
return response;
};
// 监听 XMLHttpRequest
const originalXHROpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
this.addEventListener('load', function() {
if (url.includes('.m3u8')) {
if (!m3u8Url) {
m3u8Url = url;
console.log('捕获到 m3u8 URL (XHR):', m3u8Url);
createDownloadButton(); // 捕获后创建按钮
}
}
});
originalXHROpen.apply(this, arguments);
};
})();
package main
import (
"encoding/json"
"fmt"
"net/http"
"os/exec"
"regexp"
"sync"
)
type DownloadTask struct {
URL string `json:"url"`
Filename string `json:"filename"`
}
// 单消费者队列
var (
taskQueue = make(chan DownloadTask, 100) // 实际队列
taskMap = make(map[string]bool) // key: Filename,用于去重
queueLock sync.Mutex
)
// 校验文件名安全性
var validName = regexp.MustCompile(`^[\w\-.]+$`)
// 消费者线程:顺序下载
func consumer() {
for task := range taskQueue {
safeDownload(task)
// 下载完成后从 map 删除
queueLock.Lock()
delete(taskMap, task.Filename)
queueLock.Unlock()
}
}
// 安全下载函数
func safeDownload(task DownloadTask) {
fmt.Printf("🚀 Start downloading: %s -> %s\n", task.URL, task.Filename)
cmd := exec.Command(
"./N_m3u8DL-RE",
task.URL,
"--save-dir", "./mp4",
"--save-name", task.Filename,
)
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("❌ Download failed for %s: %v\nOutput: %s\n", task.Filename, err, string(output))
return
}
fmt.Printf("✅ Download finished: %s\n", task.Filename)
}
// HTTP 添加任务接口
func addTaskHandler(w http.ResponseWriter, r *http.Request) {
url := r.FormValue("url")
filename := r.FormValue("filename")
resp := make(map[string]any)
if url == "" || filename == "" {
resp["message"] = "missing url or filename"
queueLock.Lock()
resp["queue_size"] = len(taskMap)
queueLock.Unlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
return
}
if !validName.MatchString(filename) {
resp["message"] = "invalid filename"
queueLock.Lock()
resp["queue_size"] = len(taskMap)
queueLock.Unlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
return
}
queueLock.Lock()
defer queueLock.Unlock()
// 文件名去重
if taskMap[filename] {
resp["message"] = "下载任务已经添加"
resp["queue_size"] = len(taskMap)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
return
}
// 添加任务
task := DownloadTask{URL: url, Filename: filename}
taskQueue <- task
taskMap[filename] = true
resp["message"] = "下载任务添加成功"
resp["queue_size"] = len(taskMap)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
fmt.Printf("✅ Added task: %+v (queue size: %d)\n", task, len(taskMap))
}
func main() {
// 启动单消费者线程
go consumer()
http.HandleFunc("/add", addTaskHandler)
fmt.Println("🚀 Server running on :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment