v1.8.1: 修复下载得到 txt —— Blob 必须带 MIME(否则 Chrome 嗅探内容把 HTML 错误页存成 .txt);新增内容魔数校验,拦截 200+HTML 错误页与空内容

This commit is contained in:
personal82555 committed 2026-10-13 23:24:41 +08:00
1 parent a320658ac8
commit 3b72783efe
2 files changed
+32 -4

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "B站评论助手",
"version": "1.8.0",
"version": "1.8.1",
"description": "B站网页版关键词评论助手:按搜索关键词找视频,默认模拟真人(打开视频页、滚到评论区、逐字输入、点发布),也可快速API直发;支持下载当前页视频;带防重复记录、可中断执行与页面实时进度浮层。",
"permissions": ["storage", "tabs", "scripting", "sidePanel", "downloads", "declarativeNetRequest"],
"host_permissions": [
+31 -3
View File
@@ -373,7 +373,11 @@ function refreshCustomVisibility() {
function fmtMb(n) { return (n / 1048576).toFixed(1) + " MB"; }
// 面板页自己抓文件(扩展页 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);
if (!resp.ok) throw new Error("HTTP " + resp.status + "(CDN拒绝,可能需要重新打开视频页)");
const total = Number(resp.headers.get("content-length") || 0);
@@ -387,8 +391,31 @@ function refreshCustomVisibility() {
got += value.length;
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() {
const btn = $("btnDownload"), msg = $("dlMsg");
@@ -424,13 +451,14 @@ function refreshCustomVisibility() {
name += "." + (f.ext || "mp4");
try {
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 now = Date.now();
if (now - lastShow > 500) {
lastShow = now;
setText("下载中 " + fmtMb(got) + (total ? " / " + fmtMb(total) : "") + "(" + name.slice(0, 18) + "…)");
}
});
}, MIME_BY_EXT[(f.ext || "mp4").toLowerCase()] || "video/mp4");
const objUrl = URL.createObjectURL(blob);
await chrome.downloads.download({
url: objUrl, filename: "B站视频/" + name, conflictAction: "uniquify", saveAs: false