v1.8.1: 修复下载得到 txt —— Blob 必须带 MIME(否则 Chrome 嗅探内容把 HTML 错误页存成 .txt);新增内容魔数校验,拦截 200+HTML 错误页与空内容
This commit is contained in:
1 parent
a320658ac8
commit
3b72783efe
2 files changed
+32
-4
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "B站评论助手",
|
"name": "B站评论助手",
|
||||||
"version": "1.8.0",
|
"version": "1.8.1",
|
||||||
"description": "B站网页版关键词评论助手:按搜索关键词找视频,默认模拟真人(打开视频页、滚到评论区、逐字输入、点发布),也可快速API直发;支持下载当前页视频;带防重复记录、可中断执行与页面实时进度浮层。",
|
"description": "B站网页版关键词评论助手:按搜索关键词找视频,默认模拟真人(打开视频页、滚到评论区、逐字输入、点发布),也可快速API直发;支持下载当前页视频;带防重复记录、可中断执行与页面实时进度浮层。",
|
||||||
"permissions": ["storage", "tabs", "scripting", "sidePanel", "downloads", "declarativeNetRequest"],
|
"permissions": ["storage", "tabs", "scripting", "sidePanel", "downloads", "declarativeNetRequest"],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
|
|||||||
+31
-3
@@ -373,7 +373,11 @@ function refreshCustomVisibility() {
|
|||||||
function fmtMb(n) { return (n / 1048576).toFixed(1) + " MB"; }
|
function fmtMb(n) { return (n / 1048576).toFixed(1) + " MB"; }
|
||||||
|
|
||||||
// 面板页自己抓文件(扩展页 fetch 免 CORS,DNR 规则注入 Referer)→ Blob → 触发保存
|
// 面板页自己抓文件(扩展页 fetch 免 CORS,DNR 规则注入 Referer)→ Blob → 触发保存
|
||||||
async function fetchToBlob(url, onProgress) {
|
// 取视频流并封装成 Blob。
|
||||||
|
// 两个必须修的坑:
|
||||||
|
// 1) Blob 必须带 MIME —— 不带的话 Chrome 会去嗅探内容,HTML 错误页会被存成 .txt/.html
|
||||||
|
// 2) 必须校验内容魔数 —— CDN 有时回 200 + HTML 验证页,不校验就会把网页存成"视频"
|
||||||
|
async function fetchToBlob(url, onProgress, mime) {
|
||||||
const resp = await fetch(url);
|
const resp = await fetch(url);
|
||||||
if (!resp.ok) throw new Error("HTTP " + resp.status + "(CDN拒绝,可能需要重新打开视频页)");
|
if (!resp.ok) throw new Error("HTTP " + resp.status + "(CDN拒绝,可能需要重新打开视频页)");
|
||||||
const total = Number(resp.headers.get("content-length") || 0);
|
const total = Number(resp.headers.get("content-length") || 0);
|
||||||
@@ -387,8 +391,31 @@ function refreshCustomVisibility() {
|
|||||||
got += value.length;
|
got += value.length;
|
||||||
if (onProgress) onProgress(got, total);
|
if (onProgress) onProgress(got, total);
|
||||||
}
|
}
|
||||||
return new Blob(chunks);
|
if (!chunks.length) throw new Error("CDN 返回了空内容");
|
||||||
|
// 魔数校验:MP4/FLV/M4S 都是二进制;返回 HTML/JSON 说明拿到的是错误页
|
||||||
|
const head = chunks[0];
|
||||||
|
const b0 = head[0], b1 = head[1], b2 = head[2], b3 = head[3];
|
||||||
|
const isFtyp = head.length >= 12 && b4(head,0)===0x66 && b4(head,1)===0x74 && b4(head,2)===0x79 && b4(head,3)===0x70; // "ftyp"
|
||||||
|
const isFlv = head.length >= 3 && b0===0x46 && b1===0x4C && b2===0x56; // "FLV"
|
||||||
|
const isIso = head.length >= 8 && b0===0x00 && b1===0x00; // MP4 长度前缀
|
||||||
|
const isHtml = (b0===0x3C) || (b0===0xEF && b1===0xBB && b2===0xBF) || // "<" 或 BOM+"<"
|
||||||
|
(b0===0x0A || b0===0x0D || b0===0x20) || // 空白开头
|
||||||
|
(b0===0x7B); // "{" JSON
|
||||||
|
if (isHtml) {
|
||||||
|
throw new Error("CDN 返回的不是视频(是网页/错误页),请刷新视频页后重试");
|
||||||
}
|
}
|
||||||
|
if (!isFtyp && !isFlv && !isIso) {
|
||||||
|
throw new Error("下载内容不是有效的视频格式(文件头异常),已放弃保存");
|
||||||
|
}
|
||||||
|
// MIME:优先用响应头,再按魔数判定,兜底 mp4 —— 绝不给空字符串
|
||||||
|
const ct = (resp.headers.get("content-type") || "").split(";")[0].trim();
|
||||||
|
let type = ct;
|
||||||
|
if (!/^video\//i.test(type) && !/^audio\//i.test(type)) {
|
||||||
|
type = isFtyp || isIso ? "video/mp4" : (isFlv ? "video/x-flv" : "video/mp4");
|
||||||
|
}
|
||||||
|
return new Blob(chunks, { type: type });
|
||||||
|
}
|
||||||
|
function b4(u8, i) { return u8.length > i ? u8[i] : -1; }
|
||||||
|
|
||||||
async function downloadCurrent() {
|
async function downloadCurrent() {
|
||||||
const btn = $("btnDownload"), msg = $("dlMsg");
|
const btn = $("btnDownload"), msg = $("dlMsg");
|
||||||
@@ -424,13 +451,14 @@ function refreshCustomVisibility() {
|
|||||||
name += "." + (f.ext || "mp4");
|
name += "." + (f.ext || "mp4");
|
||||||
try {
|
try {
|
||||||
let lastShow = 0;
|
let lastShow = 0;
|
||||||
|
const MIME_BY_EXT = { mp4: "video/mp4", flv: "video/x-flv", m4s: "video/mp4", m4a: "audio/mp4", aac: "audio/aac" };
|
||||||
const blob = await fetchToBlob(f.url, (got, total) => {
|
const blob = await fetchToBlob(f.url, (got, total) => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - lastShow > 500) {
|
if (now - lastShow > 500) {
|
||||||
lastShow = now;
|
lastShow = now;
|
||||||
setText("下载中 " + fmtMb(got) + (total ? " / " + fmtMb(total) : "") + "(" + name.slice(0, 18) + "…)");
|
setText("下载中 " + fmtMb(got) + (total ? " / " + fmtMb(total) : "") + "(" + name.slice(0, 18) + "…)");
|
||||||
}
|
}
|
||||||
});
|
}, MIME_BY_EXT[(f.ext || "mp4").toLowerCase()] || "video/mp4");
|
||||||
const objUrl = URL.createObjectURL(blob);
|
const objUrl = URL.createObjectURL(blob);
|
||||||
await chrome.downloads.download({
|
await chrome.downloads.download({
|
||||||
url: objUrl, filename: "B站视频/" + name, conflictAction: "uniquify", saveAs: false
|
url: objUrl, filename: "B站视频/" + name, conflictAction: "uniquify", saveAs: false
|
||||||
|
|||||||
Reference in new issue
Block a user