// ==UserScript== // @name 番茄小说助手 // @namespace https://github.com/naiyQAQ/fanqie-assistant // @version 0.0.6 // @author naiyQAQ // @description 番茄小说助手,去广告、去推广、解锁章节、优化体验。 // @license GPLv3 // @icon data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCiAgICA8cGF0aA0KICAgICAgICBkPSJNMjMuMzExNSAxSDYuNjg4NTNDMy41NDY4NCAxIDEgMy41NDY4NCAxIDYuNjg4NTNWMjMuMzExNUMxIDI2LjQ1MzIgMy41NDY4NCAyOSA2LjY4ODUzIDI5SDIzLjMxMTVDMjYuNDUzMiAyOSAyOSAyNi40NTMyIDI5IDIzLjMxMTVWNi42ODg1M0MyOSAzLjU0Njg0IDI2LjQ1MzIgMSAyMy4zMTE1IDFaIg0KICAgICAgICBmaWxsPSJ3aGl0ZSI+PC9wYXRoPg0KICAgIDxwYXRoDQogICAgICAgIGQ9Ik0yMy4zMTE1IDAuNzVINi42ODg1M0MzLjQwODc3IDAuNzUgMC43NSAzLjQwODc3IDAuNzUgNi42ODg1M1YyMy4zMTE1QzAuNzUgMjYuNTkxMiAzLjQwODc3IDI5LjI1IDYuNjg4NTMgMjkuMjVIMjMuMzExNUMyNi41OTEyIDI5LjI1IDI5LjI1IDI2LjU5MTIgMjkuMjUgMjMuMzExNVY2LjY4ODUzQzI5LjI1IDMuNDA4NzcgMjYuNTkxMiAwLjc1IDIzLjMxMTUgMC43NVoiDQogICAgICAgIHN0cm9rZT0iYmxhY2siIHN0cm9rZS1vcGFjaXR5PSIwLjA4IiBzdHJva2Utd2lkdGg9IjAuNSI+PC9wYXRoPg0KICAgIDxtYXNrIGlkPSJtYXNrMF80NzBfNDgzNjQiIG1hc2tVbml0cz0idXNlclNwYWNlT25Vc2UiIHg9IjEiIHk9IjEiIHdpZHRoPSIyOCIgaGVpZ2h0PSIyOCI+DQogICAgICAgIDxwYXRoDQogICAgICAgICAgICBkPSJNMjMuMzExNSAxSDYuNjg4NTNDMy41NDY4NCAxIDEgMy41NDY4NCAxIDYuNjg4NTNWMjMuMzExNUMxIDI2LjQ1MzIgMy41NDY4NCAyOSA2LjY4ODUzIDI5SDIzLjMxMTVDMjYuNDUzMiAyOSAyOSAyNi40NTMyIDI5IDIzLjMxMTVWNi42ODg1M0MyOSAzLjU0Njg0IDI2LjQ1MzIgMSAyMy4zMTE1IDFaIg0KICAgICAgICAgICAgZmlsbD0id2hpdGUiPjwvcGF0aD4NCiAgICA8L21hc2s+DQogICAgPGcgbWFzaz0idXJsKCNtYXNrMF80NzBfNDgzNjQpIj4NCiAgICAgICAgPHBhdGgNCiAgICAgICAgICAgIGQ9Ik0yMy4zMTE1IDFINi42ODg1M0MzLjU0Njg0IDEgMSAzLjU0Njg0IDEgNi42ODg1M1YyMy4zMTE1QzEgMjYuNDUzMiAzLjU0Njg0IDI5IDYuNjg4NTMgMjlIMjMuMzExNUMyNi40NTMyIDI5IDI5IDI2LjQ1MzIgMjkgMjMuMzExNVY2LjY4ODUzQzI5IDMuNTQ2ODQgMjYuNDUzMiAxIDIzLjMxMTUgMVoiDQogICAgICAgICAgICBmaWxsPSJ3aGl0ZSI+PC9wYXRoPg0KICAgICAgICA8cGF0aA0KICAgICAgICAgICAgZD0iTTE1LjAwMDggNDguNjY0MkMyNS40MDE3IDQ4LjY2NDIgMzMuODMzNCA0MC4yMzI2IDMzLjgzMzQgMjkuODMxNkMzMy44MzM0IDE5LjQzMDcgMjUuNDAxNyAxMC45OTkgMTUuMDAwOCAxMC45OTlDNC41OTk4NSAxMC45OTkgLTMuODMxNzkgMTkuNDMwNyAtMy44MzE3OSAyOS44MzE2Qy0zLjgzMTc5IDQwLjIzMjYgNC41OTk4NSA0OC42NjQyIDE1LjAwMDggNDguNjY0MloiDQogICAgICAgICAgICBmaWxsPSJ1cmwoI3BhaW50MF9yYWRpYWxfNDcwXzQ4MzY0KSI+PC9wYXRoPg0KICAgICAgICA8cGF0aCBkPSJNMjMuMjY4OCAxVjcuMjEyOTRMMjAuNjY2MyA1LjcxNDM3TDE4LjA2NzQgNy4yMTI5NFYxSDIzLjI2ODhaIiBmaWxsPSIjRkY1RjAwIj48L3BhdGg+DQogICAgICAgIDxwYXRoDQogICAgICAgICAgICBkPSJNMTUuMTM0MyAxOC44ODFDMTUuMTM0MyAxOC44ODEgMTYuMTAxNCAxNy41NTEzIDE2LjEwMTQgMTYuNDA2NUMxNi4xMDE0IDE1LjI2MTcgMTUuNjY3NiAxNC43MzczIDE1LjEzNDMgMTQuNzM3M0MxNC42MDEgMTQuNzM3MyAxNC4xNjczIDE1LjI2MzUgMTQuMTY3MyAxNi40MDY1QzE0LjE2NzMgMTcuNTQ5NiAxNS4xMzQzIDE4Ljg4MSAxNS4xMzQzIDE4Ljg4MVoiDQogICAgICAgICAgICBmaWxsPSJ3aGl0ZSI+PC9wYXRoPg0KICAgICAgICA8cGF0aA0KICAgICAgICAgICAgZD0iTTcuNjI3MjQgMjIuNjU4NUM4Ljc3MjA1IDIyLjY1ODUgMTAuMTAxNyAyMy42MjU2IDEwLjEwMTcgMjMuNjI1NkMxMC4xMDE3IDIzLjYyNTYgOC43NzAyNyAyNC41OTI2IDcuNjI3MjQgMjQuNTkyNkM2LjQ4NDIgMjQuNTkyNiA1Ljk1ODAxIDI0LjE1ODkgNS45NTgwMSAyMy42MjU2QzUuOTU4MDEgMjMuMDkyMyA2LjQ4MjQyIDIyLjY1ODUgNy42MjcyNCAyMi42NTg1WiINCiAgICAgICAgICAgIGZpbGw9IndoaXRlIj48L3BhdGg+DQogICAgICAgIDxwYXRoDQogICAgICAgICAgICBkPSJNMjIuNjM5NiAyNC41OTI2QzIxLjQ5NDggMjQuNTkyNiAyMC4xNjUxIDIzLjYyNTYgMjAuMTY1MSAyMy42MjU2QzIwLjE2NTEgMjMuNjI1NiAyMS40OTY2IDIyLjY1ODUgMjIuNjM5NiAyMi42NTg1QzIzLjc4MjYgMjIuNjU4NSAyNC4zMDg4IDIzLjA5MjMgMjQuMzA4OCAyMy42MjU2QzI0LjMwODggMjQuMTU4OSAyMy43ODQ0IDI0LjU5MjYgMjIuNjM5NiAyNC41OTI2WiINCiAgICAgICAgICAgIGZpbGw9IndoaXRlIj48L3BhdGg+DQogICAgICAgIDxwYXRoDQogICAgICAgICAgICBkPSJNMTAuNDU1NSAxOC4zMTM5QzExLjI2NDMgMTkuMTIyNyAxMS41MjIxIDIwLjc0NzUgMTEuNTIyMSAyMC43NDc1QzExLjUyMjEgMjAuNzQ3NSA5Ljg5NzMyIDIwLjQ4OTcgOS4wODg0OCAxOS42ODA5QzguMjc5NjQgMTguODcyMSA4LjIxMzg3IDE4LjE5NDggOC41OTI1MSAxNy44MTYxQzguOTcxMTUgMTcuNDM3NSA5LjY0NjY2IDE3LjUwMzMgMTAuNDU3MyAxOC4zMTIxTDEwLjQ1NTUgMTguMzEzOVoiDQogICAgICAgICAgICBmaWxsPSJ3aGl0ZSI+PC9wYXRoPg0KICAgICAgICA8cGF0aA0KICAgICAgICAgICAgZD0iTTE4Ljc0NjUgMjAuNzQ3NkMxOC43NDY1IDIwLjc0NzYgMTkuMDA0MyAxOS4xMjI4IDE5LjgxMzEgMTguMzE0TDE5LjgxMTMgMTguMzEyMkMyMC42MjIgMTcuNTAzMyAyMS4yOTkzIDE3LjQzOTMgMjEuNjc2MSAxNy44MTYyQzIyLjA1NDggMTguMTk0OSAyMS45ODkgMTguODcyMSAyMS4xODAyIDE5LjY4MUMyMC4zNzEzIDIwLjQ4OTggMTguNzQ2NSAyMC43NDc2IDE4Ljc0NjUgMjAuNzQ3NloiDQogICAgICAgICAgICBmaWxsPSJ3aGl0ZSI+PC9wYXRoPg0KICAgIDwvZz4NCiAgICA8ZGVmcz4NCiAgICAgICAgPHJhZGlhbEdyYWRpZW50IGlkPSJwYWludDBfcmFkaWFsXzQ3MF80ODM2NCIgY3g9IjAiIGN5PSIwIiByPSIxIg0KICAgICAgICAgICAgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiDQogICAgICAgICAgICBncmFkaWVudFRyYW5zZm9ybT0idHJhbnNsYXRlKDE1LjAwMDggMjkuODMxNikgc2NhbGUoMTguODMyNikiPg0KICAgICAgICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iI0NDMDUwMCI+PC9zdG9wPg0KICAgICAgICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjRkY1RjAwIj48L3N0b3A+DQogICAgICAgIDwvcmFkaWFsR3JhZGllbnQ+DQogICAgPC9kZWZzPg0KPC9zdmc+ // @match *://*.fanqienovel.com/* // @require https://registry.npmmirror.com/vue/3.5.40/files/dist/vue.global.prod.js // @require https://registry.npmmirror.com/moment/2.30.1/files/min/moment.min.js // @require https://registry.npmmirror.com/jszip/3.10.1/files/dist/jszip.min.js // @connect fanqienovel.com // @connect jxbhmy.com // @connect snssdk.com // @connect byteimg.com // @connect fqnovelpic.com // @connect bytecdn.cn // @connect fqnovelvod.com // @connect novelfmvod.com // @connect volcautovod.com // @grant GM_addStyle // @grant GM_cookie // @grant GM_deleteValue // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @grant unsafeWindow // @run-at document-start // ==/UserScript== (function (vue, moment, JSZip) { 'use strict'; var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); const shared_key = new Uint8Array([172, 37, 198, 125, 221, 143, 56, 193, 179, 122, 35, 72, 130, 142, 34, 46]).buffer; const fetch$1 = unsafeWindow.fetch; unsafeWindow.XMLHttpRequest; const defaultConfig = { install_id: "2187355326270644", device_id: "2187355326004404", device_type: "P30" }; const _config = { currentConfig: defaultConfig }; const scriptcss = "/* 移除章节锁定图标 */\n.muyeicon-lock {\n display: none;\n}\n/* 移除APP推广图标 */\n.muye-to-fanqie {\n display: none!important;\n}\n.reader-toolbar-item-download {\n display: none!important;\n}\n.download-btn {\n display: none!important;\n}\n.download-icon {\n display: none!important;\n}\n\n.fqa-hide {\n display: none!important;\n}\n/* 404 */\n.no-content {\n display: none!important;\n}\n\n.fqa-comic-img {\n width: 100%!important;\n height: 100%!important;\n max-width: 100%!important;\n max-height: 100%!important;\n padding-top: 0!important;\n padding-bottom: 0!important;\n margin-top: 0!important;\n margin-bottom: 0!important;\n}\n\n.fqa-comic-reader {\n line-height: 0!important;\n}\n\n.fqa-menu-item,\n.arco-menu-item {\n width: 100%!important;\n}\n\n#dynamic-el {\n display: none!important;\n}\n\n.fqa-footnote-ref {\n display: inline-block;\n margin: 0 0.15em;\n padding: 0 0.25em;\n font-size: 0.7em;\n line-height: 1.4;\n vertical-align: super;\n color: var(--web-brand_normal, #f14646);\n cursor: pointer;\n user-select: none;\n border-radius: 3px;\n text-indent: 0;\n}\n\n.fqa-footnote-ref:hover,\n.fqa-footnote-ref:focus-visible {\n background: var(--web-brand_light, rgba(241, 70, 70, 0.12));\n outline: none;\n}\n\n\n.fqa-footnote {\n margin-top: 2em;\n padding-top: 1em;\n border-top: 1px solid var(--web-gray_20, rgba(128, 128, 128, 0.25));\n font-size: var(--fqa-body-size, 1.6rem);\n}\n\n.muye-reader-content-16 .fqa-footnote { font-size: var(--fqa-body-size, 1.6rem); }\n.muye-reader-content-20 .fqa-footnote { font-size: var(--fqa-body-size, 2rem); }\n.muye-reader-content-24 .fqa-footnote { font-size: var(--fqa-body-size, 2.4rem); }\n.muye-reader-content-28 .fqa-footnote { font-size: var(--fqa-body-size, 2.8rem); }\n.muye-reader-content-32 .fqa-footnote { font-size: var(--fqa-body-size, 3.2rem); }\n\n.fqa-footnote-title {\n margin-bottom: 0.6em;\n font-size: 0.85em;\n font-weight: 600;\n color: var(--web-gray_40, #8a8a8a);\n text-indent: 0;\n}\n\n.fqa-footnote-list {\n margin: 0;\n padding-left: 1.6em;\n font-size: 0.85em;\n line-height: 1.7;\n color: var(--web-gray_40, #8a8a8a);\n}\n\n.fqa-footnote-list li {\n margin-bottom: 0.5em;\n text-indent: 0;\n transition: background-color 0.3s ease;\n}\n\n.fqa-footnote-list li.fqa-footnote-active {\n background: var(--web-brand_light, rgba(241, 70, 70, 0.12));\n border-radius: 4px;\n}\n\n.muye-reader-content > body {\n background-color: var(--web-bg)!important;\n}\n\n.fqa-icon-dark {\n color: #B3B3B3\n}\n\n/* ----------------------------- 右键菜单 / Toast ----------------------------- */\n\n/*\n * 书架与搜索共用。两者都把菜单 Teleport 到 body,\n * 拿不到各自根节点上的变量,所以在这里声明一份全局色板。\n */\n.fqa-menu {\n --fqa-menu-bg: #fff;\n --fqa-menu-text: #1f2329;\n --fqa-menu-sub: #8f959e;\n --fqa-menu-hover: rgba(31, 35, 41, 0.06);\n --fqa-menu-danger: #f5222d;\n\n position: fixed;\n z-index: 2147483001;\n min-width: 132px;\n max-width: 240px;\n padding: 4px;\n box-sizing: border-box;\n background: var(--fqa-menu-bg);\n border: 1px solid rgba(31, 35, 41, 0.08);\n border-radius: 8px;\n box-shadow: 0 6px 24px rgba(31, 35, 41, 0.16);\n font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Arial,\n sans-serif;\n font-size: 13px;\n color: var(--fqa-menu-text);\n user-select: none;\n}\n\n/* 二级面板:分组数量多时可滚动 */\n.fqa-menu-sub {\n max-height: 320px;\n overflow-y: auto;\n}\n\n.fqa-menu-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 7px 10px;\n border-radius: 5px;\n line-height: 1.4;\n cursor: pointer;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.fqa-menu-row > span:first-child {\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.fqa-menu-row:hover,\n.fqa-menu-row.fqa-menu-open {\n background: var(--fqa-menu-hover);\n}\n\n.fqa-menu-arrow {\n color: var(--fqa-menu-sub);\n font-size: 15px;\n line-height: 1;\n}\n\n.fqa-menu-danger {\n color: var(--fqa-menu-danger);\n}\n\n.fqa-menu-disabled {\n color: var(--fqa-menu-sub);\n cursor: not-allowed;\n}\n\n.fqa-menu-disabled:hover {\n background: transparent;\n}\n\n/* 操作结果提示 */\n.fqa-toast {\n position: fixed;\n left: 50%;\n bottom: 48px;\n transform: translateX(-50%);\n z-index: 2147483002;\n max-width: 80vw;\n padding: 10px 18px;\n box-sizing: border-box;\n background: rgba(31, 35, 41, 0.88);\n color: #fff;\n border-radius: 8px;\n font-size: 13px;\n line-height: 1.4;\n box-shadow: 0 6px 24px rgba(31, 35, 41, 0.24);\n pointer-events: none;\n}\n\n/* 骨架屏微光。书架与搜索共用同一个动画名 */\n@keyframes fqa-shimmer {\n 100% {\n transform: translateX(100%);\n }\n}\n\n@media (prefers-color-scheme: dark) {\n .fqa-menu {\n --fqa-menu-bg: #23272e;\n --fqa-menu-text: #e5e6eb;\n --fqa-menu-sub: #8f959e;\n --fqa-menu-hover: rgba(255, 255, 255, 0.08);\n border-color: rgba(255, 255, 255, 0.1);\n }\n}\n\n.info {\n width: 100%!important;\n}\n\n.reader-toolbar {\n user-select: none;\n}"; async function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function nextFrame() { return new Promise((resolve) => { let settled = false; const done = () => { if (settled) return; settled = true; resolve(); }; const raf = unsafeWindow.requestAnimationFrame; if (typeof raf === "function") raf(() => done()); setTimeout(done, 32); }); } function cloneElement(element) { return element.cloneNode(true); } function waitForElement(selector, timeout = 15e3) { const existing = document.querySelector(selector); if (existing) return Promise.resolve(existing); return new Promise((resolve) => { let timer; const observer2 = new MutationObserver(() => { const el = document.querySelector(selector); if (el) { if (timer) clearTimeout(timer); observer2.disconnect(); resolve(el); } }); const start = () => { observer2.observe(document.documentElement, { childList: true, subtree: true }); timer = setTimeout(() => { observer2.disconnect(); resolve(document.querySelector(selector)); }, timeout); }; if (document.documentElement) { start(); } else { document.addEventListener("DOMContentLoaded", start, { once: true }); } }); } function chunk(list, size) { const result = []; for (let i2 = 0; i2 < list.length; i2 += size) { result.push(list.slice(i2, i2 + size)); } return result; } function concatArrayBuffers(...buffers) { const totalLength = buffers.reduce((sum, buf) => sum + buf.byteLength, 0); const result = new Uint8Array(totalLength); let offset = 0; for (const buf of buffers) { result.set(new Uint8Array(buf), offset); offset += buf.byteLength; } return result.buffer; } async function inject$1() { while (!document.body) { console.log("Waiting for body..."); await sleep(200); } GM_addStyle(scriptcss); console.log("CSS injected successfully!"); } unsafeWindow.localStorage; function write(key, value) { GM_setValue(key, JSON.stringify(value)); } function read(key) { return JSON.parse(GM_getValue(key) || "null"); } function del(key) { GM_deleteValue(key); } const STORE_KEY$1 = "settings"; const DEFAULT_SETTINGS = { decryptFont: true, blockReport: true, allowCopy: true, shelfRemoveConfirm: true, logoutConfirm: true, readerFont: "", customCssEnabled: false, customCss: "", // 原站书架点封面是继续阅读,保持一致 bookshelfClickAction: "read", enhanceSearch: true, // 默认关:携带登录态属于额外的隐私暴露,交给用户显式开启 searchPersonalized: false, enableDownload: true, downloadFormat: "epub", downloadCharset: "utf-8", // 30 是接口单请求返回正文的上限,再大也只回 30 条 downloadBatchSize: 30, // 实测 750ms 能稳定拿满,更短会被限流成每次 1 条 downloadInterval: 750, downloadRetries: 3, downloadVolumePage: false, downloadImages: true, downloadBookCss: true, audiobookChapterEnd: "next", audiobookFollow: true, apiPreference: "app", deviceId: "", installId: "", deviceType: "" }; function normalize(raw) { const s = { ...DEFAULT_SETTINGS }; if (!raw || typeof raw !== "object") return s; const o = raw; for (const key of Object.keys(DEFAULT_SETTINGS)) { const v = o[key]; if (v === void 0 || v === null) continue; if (typeof DEFAULT_SETTINGS[key] === typeof v) { s[key] = v; } } if (s.apiPreference !== "app" && s.apiPreference !== "redcandle") { s.apiPreference = DEFAULT_SETTINGS.apiPreference; } if (s.downloadFormat !== "epub" && s.downloadFormat !== "txt") { s.downloadFormat = DEFAULT_SETTINGS.downloadFormat; } if (s.downloadCharset !== "utf-8" && s.downloadCharset !== "gbk") { s.downloadCharset = DEFAULT_SETTINGS.downloadCharset; } if (s.bookshelfClickAction !== "read" && s.bookshelfClickAction !== "detail") { s.bookshelfClickAction = DEFAULT_SETTINGS.bookshelfClickAction; } if (s.audiobookChapterEnd !== "next" && s.audiobookChapterEnd !== "stop") { s.audiobookChapterEnd = DEFAULT_SETTINGS.audiobookChapterEnd; } s.downloadBatchSize = clampInt(s.downloadBatchSize, 1, 30, DEFAULT_SETTINGS.downloadBatchSize); s.downloadInterval = clampInt(s.downloadInterval, 0, 1e4, DEFAULT_SETTINGS.downloadInterval); s.downloadRetries = clampInt(s.downloadRetries, 0, 10, DEFAULT_SETTINGS.downloadRetries); return s; } function clampInt(value, min, max, fallback) { const n = Math.round(Number(value)); if (!Number.isFinite(n)) return fallback; return Math.min(max, Math.max(min, n)); } const settings$1 = vue.reactive(normalize(read(STORE_KEY$1))); let saveTimer; vue.watch( settings$1, () => { if (saveTimer) clearTimeout(saveTimer); saveTimer = setTimeout(() => { saveTimer = void 0; write(STORE_KEY$1, { ...settings$1 }); }, 200); }, { deep: true } ); function flushSettings() { if (saveTimer) { clearTimeout(saveTimer); saveTimer = void 0; } write(STORE_KEY$1, { ...settings$1 }); } const code_ed = 58715; const code_st = 58344; const mapping = { "DNMrHsV173Pd4pgy": [ "D在主特家军然表场4要只v和?6别还g现儿岁??此象月3出战工相", "o男直失世F都平文什VO将真T那当?会立些u是十张学气大爱两命全", "后东性通被1它乐接而感车山公了常以何可话先pi叫轻M士w着变尔快", "l个说少色里安花远7难师放t报认面道S?克地度I好机U民写把万同", "水新没书电吃像斯5为y白几日教看但第加候作上拉住有法r事应位利你", "声身国问马女他Y比父xAHNsX边美对所金活回意到z从j知又内因", "点Q三定8Rb正或夫向德听更?得告并本q过记L让打f人就者去原满", "体做经K走如孩cG给使物?最笑部?员等受k行一条果动光门头见往自", "解成处天能于名其发总母的死手入路进心来h时力多开已许d至由很界n", "小与Z想代么分生口再妈望次西风种带J?实情才这?E我神格长觉间年", "眼无不亲关结0友信下却重己老2音字m呢明之前高PB目太e9起稜她", "也W用方子英每理便四数期中C外样a海们任" ], "fKts9tCXDjS49UhH": [ "体y十现快使话却月物水的放知爱方?表风理O老也p常克平几最主她s", "将法情o光a我呢J员太每望受教w利军已U人如变得要少斯门电m男没", "AK国时中走么何口小向问轻Td神下间车fG度D又大面远就写j给通", "起实E?它去S到道数吃们加P是无把事西多界?发新外活解孩只作前Y", "尔经?u心告父等Q民全这9果安?i母8r说任先和地C张战场g像c", "q你使?样总目x性处音头?应乐关能花I当名手4重字声力友然生代内", "里本回真入师象?0点R亲V种动英命ZhX做特边高有B为期自年马认", "出接至H正方感所明者棱F住学还分意更其n但比觉以由死家让失士L2", "I金叫身报听W再原山海白很见5直位第工个开岁好用都于可同3次四?", "日信与女笑满并部什不从或机此?了记三e些bN夫会才几眼两美被一公", "来立z长对己看k许因相色后往打结格过世气7子条在书之定v拉成进带", "着东上想天他妈1文而路那别德6Mt行候难" ], "_search": [ "?s?作口在他能并B士4U克才正们字声高全尔活者动其主报多望放h", "w次年?中3特于十入要男同G面分方K什再教本己结1等世N?说gu", "期Z外美M行给9文将两许张友0英应向像此白安少何打气常定间花见孩", "它直风数使道第水已女山解dP的通关性叫几L妈问回神来S?四里前国", "些OvIA心平自无车光代是好却c得种就意先立z子过Yj表?么所接", "了名金受J满眼没部那m每车度可R斯经现门明V如走命y6E战很上f", "月西7长夫想话变海机x到W一成生信笑但父开内东马日小而后带以三几", "为认X死员目位之学远入音呢我q乐象重对个被别F也书棱D写还因家发", "时i或住德当oI比觉然吃去公a老亲情体太b方C电理?失力更拉物着", "原她工实色感记看出相路大你候2和?与p样新只便最不进Tr做格母总", "爱身师轻知往加从?天eH?听场由快边让把任8条头事至起点真手这难", "都界用法n处下文Q告地5kt岁有会果利民" ] }; const NO_GLYPH = "?"; const flatCache = /* @__PURE__ */ new Map(); function tableOf(fontId) { const cached = flatCache.get(fontId); if (cached) return cached; const rows = mapping[fontId]; if (!rows) return null; const flat = [...rows.join("")]; const expected = code_ed - code_st + 1; if (flat.length !== expected) { console.error( `[fqa:font] 码表 ${fontId} 长度异常:${flat.length},应为 ${expected},已禁用该字体的解密` ); flatCache.set(fontId, []); return []; } flatCache.set(fontId, flat); return flat; } const enTag = Object.keys(mapping).map((id) => `.font-${id}`).join(", "); function fontIdOf(element) { for (const cls of element.classList) { if (!cls.startsWith("font-")) continue; const id = cls.slice(5); if (mapping[id]) return id; } return null; } function decryptText(text, fontId) { if (window.location.pathname.startsWith("/search")) fontId = "_search"; const table2 = tableOf(fontId); if (!table2 || table2.length === 0) return text; let result = ""; let changed = false; for (const char of text) { const codePoint = char.codePointAt(0); if (typeof codePoint !== "number") { return text; } if (codePoint < code_st || codePoint > code_ed) { result += char; continue; } const mapped = table2[codePoint - code_st]; if (mapped && mapped !== NO_GLYPH) { result += mapped; changed = true; } else { result += char; } } return changed ? result : text; } function decryptElement(element) { if (!settings$1.decryptFont) return; const fontId = fontIdOf(element); if (!fontId) return; const walker = document.createTreeWalker( element, NodeFilter.SHOW_TEXT, { // 后代里可能嵌着另一套字体的节点,那部分要用它自己的码表, // 交给针对它的 decryptElement 处理,这里跳过整棵子树 acceptNode(node) { var _a; const owner = (_a = node.parentElement) == null ? void 0 : _a.closest(enTag); return owner && owner !== element ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT; } } ); let textNode; while (textNode = walker.nextNode()) { const oldText = textNode.nodeValue; if (!oldText) { continue; } const newText = decryptText(oldText, fontId); if (newText !== oldText) { textNode.nodeValue = newText; } } } function decryptPage(root) { if (root instanceof Element && root.matches(enTag)) { decryptElement(root); } root.querySelectorAll(enTag).forEach(decryptElement); } function initFontDecrypt() { const observer2 = new MutationObserver((mutations) => { var _a, _b; for (const mutation of mutations) { if (mutation.type === "characterData") { const encryptedElement = (_a = mutation.target.parentElement) == null ? void 0 : _a.closest( enTag ); if (encryptedElement) { decryptElement(encryptedElement); } continue; } for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { decryptPage(node); continue; } if (node.nodeType === Node.TEXT_NODE) { const encryptedElement = (_b = node.parentElement) == null ? void 0 : _b.closest( enTag ); if (encryptedElement) { decryptElement(encryptedElement); } } } } }); observer2.observe(document, { subtree: true, childList: true, characterData: true }); decryptPage(document); vue.watch( () => settings$1.decryptFont, (on) => { if (on) decryptPage(document); } ); } const STYLE_ID$6 = "fqa-user-style"; const READER_SCOPE = "#fqa-reader-content, .muye-reader-content"; function buildCss() { const parts = []; const font = settings$1.readerFont.trim(); if (font) { const family = /^["']|,/.test(font) ? font : `"${font}"`; parts.push(`${READER_SCOPE}, ${READER_SCOPE} p { font-family: ${family}, inherit !important; }`); } if (settings$1.customCssEnabled && settings$1.customCss.trim()) { parts.push(settings$1.customCss); } return parts.join("\n"); } function apply() { const css = buildCss(); let el = document.getElementById(STYLE_ID$6); if (!css) { el == null ? void 0 : el.remove(); return; } if (!el) { el = document.createElement("style"); el.id = STYLE_ID$6; document.head.appendChild(el); } el.textContent = css; } function initUserStyle() { apply(); vue.watch( () => [settings$1.readerFont, settings$1.customCssEnabled, settings$1.customCss], apply ); } class EmptyResponseError extends Error { constructor(status) { super(`服务端返回了空响应体(HTTP ${status})`); __publicField(this, "status"); this.name = "EmptyResponseError"; this.status = status; } } function isEmptyResponse(res) { const text = res.responseText; return typeof text !== "string" || text === ""; } const supportedMethods = /* @__PURE__ */ new Set([ "GET", "HEAD", "POST", "PUT", "DELETE" ]); function apiFetch(url, options = {}) { return new Promise((resolve, reject) => { const { signal } = options; if (signal == null ? void 0 : signal.aborted) { reject(signal.reason ?? new DOMException("The operation was aborted", "AbortError")); return; } const headers = normalizeHeaders$1(options.headers); const data = normalizeBody(options.body); const method = options.method ?? (data ? "POST" : "GET"); if (!supportedMethods.has(method)) { reject(new TypeError(`Unsupported request method: ${method}`)); return; } let request; const abort = () => request == null ? void 0 : request.abort(); function cleanup() { signal == null ? void 0 : signal.removeEventListener("abort", abort); } request = GM_xmlhttpRequest({ url, method, headers, data, anonymous: options.credentials === "omit", redirect: options.redirect === "error" ? "error" : "follow", onload(response) { cleanup(); resolve(Object.assign(response, { json() { if (isEmptyResponse(this)) { throw new EmptyResponseError(this.status); } return JSON.parse(this.responseText); } })); }, onerror(response) { cleanup(); reject(createRequestError("Network request failed", response)); }, ontimeout() { cleanup(); reject(createRequestError("Network request timed out", { status: 0, statusText: "Timeout", url })); }, onabort() { cleanup(); reject( (signal == null ? void 0 : signal.reason) ?? new DOMException("The operation was aborted", "AbortError") ); } }); signal == null ? void 0 : signal.addEventListener("abort", abort, { once: true }); }); } function fetchArrayBuffer(url) { return fetch$1(url, { referrerPolicy: "no-referrer" }).then((res) => { if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.arrayBuffer(); }).catch((err) => { console.debug("[fqa:img] 页面 fetch 失败,改用 GM_xmlhttpRequest:", url, err); return gmArrayBuffer(url); }); } function gmArrayBuffer(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ url, method: "GET", responseType: "arraybuffer", onload(response) { const buf = response.response; if (response.status >= 200 && response.status < 300 && (buf == null ? void 0 : buf.byteLength)) { resolve(buf); } else { reject(new Error(`GM 请求失败(${response.status})`)); } }, onerror() { reject(new Error(`GM 请求出错,检查 @connect 是否覆盖该域名: ${url}`)); }, ontimeout: () => reject(new Error("GM 请求超时")) }); }); } function normalizeHeaders$1(headers) { if (!headers) { return void 0; } return Object.fromEntries(new Headers(headers).entries()); } function normalizeBody(body) { if (body == null) { return void 0; } if (body instanceof URLSearchParams) { return body.toString(); } if (typeof body === "string" || body instanceof Blob || body instanceof ArrayBuffer || body instanceof FormData) { return body; } if (ArrayBuffer.isView(body)) { return body.buffer.slice( body.byteOffset, body.byteOffset + body.byteLength ); } throw new TypeError( "GM_xmlhttpRequest does not support ReadableStream request bodies" ); } function createRequestError(message, response) { const error = new TypeError(message); Object.defineProperty(error, "response", { configurable: true, enumerable: false, value: response }); return error; } function rotateLeft(value, shiftBits) { return value << shiftBits | value >>> 32 - shiftBits; } function addUnsigned(left, right) { return left + right >>> 0; } function f(x, y, z) { return x & y | ~x & z; } function g(x, y, z) { return x & z | y & ~z; } function h(x, y, z) { return x ^ y ^ z; } function i(x, y, z) { return y ^ (x | ~z); } function ff(a, b, c, d, x, s, ac) { return addUnsigned(rotateLeft(addUnsigned(a, addUnsigned(addUnsigned(f(b, c, d), x), ac)), s), b); } function gg(a, b, c, d, x, s, ac) { return addUnsigned(rotateLeft(addUnsigned(a, addUnsigned(addUnsigned(g(b, c, d), x), ac)), s), b); } function hh(a, b, c, d, x, s, ac) { return addUnsigned(rotateLeft(addUnsigned(a, addUnsigned(addUnsigned(h(b, c, d), x), ac)), s), b); } function ii(a, b, c, d, x, s, ac) { return addUnsigned(rotateLeft(addUnsigned(a, addUnsigned(addUnsigned(i(b, c, d), x), ac)), s), b); } function convertToWordArray(bytes) { const wordCount = Math.ceil((bytes.length + 9) / 64) * 16; const words = new Array(wordCount).fill(0); for (let byteIndex = 0; byteIndex < bytes.length; byteIndex++) { const wordIndex = Math.floor(byteIndex / 4); const bytePosition = byteIndex % 4 * 8; words[wordIndex] = words[wordIndex] | bytes[byteIndex] << bytePosition; } const paddingWordIndex = Math.floor(bytes.length / 4); const paddingBytePosition = bytes.length % 4 * 8; words[paddingWordIndex] = words[paddingWordIndex] | 128 << paddingBytePosition; words[wordCount - 2] = bytes.length << 3; words[wordCount - 1] = bytes.length >>> 29; return words; } function wordToHex(value) { let hex2 = ""; for (let byteIndex = 0; byteIndex < 4; byteIndex++) { const byte = value >>> byteIndex * 8 & 255; hex2 += byte.toString(16).padStart(2, "0"); } return hex2; } function md5(input) { const bytes = typeof input === "string" ? new TextEncoder().encode(input) : new Uint8Array(input); const words = convertToWordArray(bytes); let a = 1732584193; let b = 4023233417; let c = 2562383102; let d = 271733878; for (let k = 0; k < words.length; k += 16) { const aa = a; const bb = b; const cc = c; const dd = d; a = ff(a, b, c, d, words[k], 7, 3614090360); d = ff(d, a, b, c, words[k + 1], 12, 3905402710); c = ff(c, d, a, b, words[k + 2], 17, 606105819); b = ff(b, c, d, a, words[k + 3], 22, 3250441966); a = ff(a, b, c, d, words[k + 4], 7, 4118548399); d = ff(d, a, b, c, words[k + 5], 12, 1200080426); c = ff(c, d, a, b, words[k + 6], 17, 2821735955); b = ff(b, c, d, a, words[k + 7], 22, 4249261313); a = ff(a, b, c, d, words[k + 8], 7, 1770035416); d = ff(d, a, b, c, words[k + 9], 12, 2336552879); c = ff(c, d, a, b, words[k + 10], 17, 4294925233); b = ff(b, c, d, a, words[k + 11], 22, 2304563134); a = ff(a, b, c, d, words[k + 12], 7, 1804603682); d = ff(d, a, b, c, words[k + 13], 12, 4254626195); c = ff(c, d, a, b, words[k + 14], 17, 2792965006); b = ff(b, c, d, a, words[k + 15], 22, 1236535329); a = gg(a, b, c, d, words[k + 1], 5, 4129170786); d = gg(d, a, b, c, words[k + 6], 9, 3225465664); c = gg(c, d, a, b, words[k + 11], 14, 643717713); b = gg(b, c, d, a, words[k], 20, 3921069994); a = gg(a, b, c, d, words[k + 5], 5, 3593408605); d = gg(d, a, b, c, words[k + 10], 9, 38016083); c = gg(c, d, a, b, words[k + 15], 14, 3634488961); b = gg(b, c, d, a, words[k + 4], 20, 3889429448); a = gg(a, b, c, d, words[k + 9], 5, 568446438); d = gg(d, a, b, c, words[k + 14], 9, 3275163606); c = gg(c, d, a, b, words[k + 3], 14, 4107603335); b = gg(b, c, d, a, words[k + 8], 20, 1163531501); a = gg(a, b, c, d, words[k + 13], 5, 2850285829); d = gg(d, a, b, c, words[k + 2], 9, 4243563512); c = gg(c, d, a, b, words[k + 7], 14, 1735328473); b = gg(b, c, d, a, words[k + 12], 20, 2368359562); a = hh(a, b, c, d, words[k + 5], 4, 4294588738); d = hh(d, a, b, c, words[k + 8], 11, 2272392833); c = hh(c, d, a, b, words[k + 11], 16, 1839030562); b = hh(b, c, d, a, words[k + 14], 23, 4259657740); a = hh(a, b, c, d, words[k + 1], 4, 2763975236); d = hh(d, a, b, c, words[k + 4], 11, 1272893353); c = hh(c, d, a, b, words[k + 7], 16, 4139469664); b = hh(b, c, d, a, words[k + 10], 23, 3200236656); a = hh(a, b, c, d, words[k + 13], 4, 681279174); d = hh(d, a, b, c, words[k], 11, 3936430074); c = hh(c, d, a, b, words[k + 3], 16, 3572445317); b = hh(b, c, d, a, words[k + 6], 23, 76029189); a = hh(a, b, c, d, words[k + 9], 4, 3654602809); d = hh(d, a, b, c, words[k + 12], 11, 3873151461); c = hh(c, d, a, b, words[k + 15], 16, 530742520); b = hh(b, c, d, a, words[k + 2], 23, 3299628645); a = ii(a, b, c, d, words[k], 6, 4096336452); d = ii(d, a, b, c, words[k + 7], 10, 1126891415); c = ii(c, d, a, b, words[k + 14], 15, 2878612391); b = ii(b, c, d, a, words[k + 5], 21, 4237533241); a = ii(a, b, c, d, words[k + 12], 6, 1700485571); d = ii(d, a, b, c, words[k + 3], 10, 2399980690); c = ii(c, d, a, b, words[k + 10], 15, 4293915773); b = ii(b, c, d, a, words[k + 1], 21, 2240044497); a = ii(a, b, c, d, words[k + 8], 6, 1873313359); d = ii(d, a, b, c, words[k + 15], 10, 4264355552); c = ii(c, d, a, b, words[k + 6], 15, 2734768916); b = ii(b, c, d, a, words[k + 13], 21, 1309151649); a = ii(a, b, c, d, words[k + 4], 6, 4149444226); d = ii(d, a, b, c, words[k + 11], 10, 3174756917); c = ii(c, d, a, b, words[k + 2], 15, 718787259); b = ii(b, c, d, a, words[k + 9], 21, 3951481745); a = addUnsigned(a, aa); b = addUnsigned(b, bb); c = addUnsigned(c, cc); d = addUnsigned(d, dd); } return (wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d)).toLowerCase(); } function rotl(value, shift) { const normalizedShift = shift & 31; return value << normalizedShift | value >>> 32 - normalizedShift; } function p0(value) { return value ^ rotl(value, 9) ^ rotl(value, 17); } function p1(value) { return value ^ rotl(value, 15) ^ rotl(value, 23); } function sm3(input) { const inputLength = input.length; const paddedLength = Math.ceil((inputLength + 9) / 64) * 64; const message = new Uint8Array(paddedLength); message.set(input); message[inputLength] = 128; const bitLength = BigInt(inputLength) * 8n; for (let i2 = 0; i2 < 8; i2++) { message[paddedLength - 1 - i2] = Number(bitLength >> BigInt(i2 * 8) & 0xffn); } const dataView = new DataView(message.buffer); const state2 = new Uint32Array([ 1937774191, 1226093241, 388252375, 3666478592, 2842636476, 372324522, 3817729613, 2969243214 ]); const words = new Uint32Array(68); const expandedWords = new Uint32Array(64); for (let block = 0; block < message.length / 64; block++) { const start = block * 64; for (let i2 = 0; i2 < 16; i2++) { words[i2] = dataView.getUint32(start + i2 * 4, false); } for (let i2 = 16; i2 < 68; i2++) { words[i2] = p1(words[i2 - 16] ^ words[i2 - 9] ^ rotl(words[i2 - 3], 15)) ^ rotl(words[i2 - 13], 7) ^ words[i2 - 6]; } for (let i2 = 0; i2 < 64; i2++) { expandedWords[i2] = words[i2] ^ words[i2 + 4]; } let a = state2[0]; let b = state2[1]; let c = state2[2]; let d = state2[3]; let e = state2[4]; let f2 = state2[5]; let g2 = state2[6]; let h2 = state2[7]; for (let i2 = 0; i2 < 64; i2++) { const t = i2 <= 15 ? 2043430169 : 2055708042; const ss1 = rotl(rotl(a, 12) + e + rotl(t, i2), 7); const ss2 = ss1 ^ rotl(a, 12); const tt1 = (i2 <= 15 ? a ^ b ^ c : a & b | a & c | b & c) + d + ss2 + expandedWords[i2]; const tt2 = (i2 <= 15 ? e ^ f2 ^ g2 : e & f2 | ~e & g2) + h2 + ss1 + words[i2]; d = c; c = rotl(b, 9); b = a; a = tt1; h2 = g2; g2 = rotl(f2, 19); f2 = e; e = p0(tt2); } state2[0] = state2[0] ^ a; state2[1] = state2[1] ^ b; state2[2] = state2[2] ^ c; state2[3] = state2[3] ^ d; state2[4] = state2[4] ^ e; state2[5] = state2[5] ^ f2; state2[6] = state2[6] ^ g2; state2[7] = state2[7] ^ h2; } const result = new Uint8Array(32); for (let i2 = 0; i2 < state2.length; i2++) { const word = state2[i2]; result[i2 * 4] = word >>> 24; result[i2 * 4 + 1] = word >>> 16; result[i2 * 4 + 2] = word >>> 8; result[i2 * 4 + 3] = word; } return result; } function getCrypto() { const c = globalThis.crypto ?? unsafeWindow.crypto; if (!(c == null ? void 0 : c.subtle)) { throw new Error("Crypto API不可用,请检查浏览器版本是否支持该API"); } return c; } function getSubtle() { return getCrypto().subtle; } function b64decode(b64) { const binaryString = atob(b64); const len = binaryString.length; const bytes = new Uint8Array(len); for (let i2 = 0; i2 < len; i2++) { bytes[i2] = binaryString.charCodeAt(i2); } return bytes.buffer; } function b64encode(buffer) { const bytes = new Uint8Array(buffer); const chunkSize = 32768; const chunks = []; for (let i2 = 0; i2 < bytes.length; i2 += chunkSize) { chunks.push( String.fromCharCode(...bytes.subarray(i2, i2 + chunkSize)) ); } return btoa(chunks.join("")); } function unhex(hex2) { if (hex2.length % 2 !== 0) { throw new Error("Invalid hex string"); } const bytes = new Uint8Array(hex2.length / 2); for (let i2 = 0; i2 < hex2.length; i2 += 2) { const byte = parseInt(hex2.slice(i2, i2 + 2), 16); if (Number.isNaN(byte)) { throw new Error("Invalid hex string"); } bytes[i2 / 2] = byte; } return bytes.buffer; } function hex(buffer) { const bytes = new Uint8Array(buffer); let hexString = ""; for (let i2 = 0; i2 < bytes.length; i2++) { hexString += bytes[i2].toString(16).padStart(2, "0"); } return hexString; } function pkcs7Pad(data, blockSize = 16) { const padLength = blockSize - data.length % blockSize; const padded = new Uint8Array(data.length + padLength); padded.set(data); padded.fill(padLength, data.length); return padded; } function randomString(length) { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let result = ""; const array = new Uint8Array(length); getCrypto().getRandomValues(array); for (let i2 = 0; i2 < length; i2++) { result += chars.charAt(array[i2] % chars.length); } return result; } function toBytes$1(input) { if (typeof input === "string") { return new TextEncoder().encode(input); } if (input instanceof Uint8Array) { return new Uint8Array(input); } return new Uint8Array(input); } const hash = { sha256: async (input) => { const subtle = getSubtle(); const digest = await subtle.digest("SHA-256", toBytes$1(input)); return hex(digest); }, sha256bytes: async (input) => { const subtle = getSubtle(); return subtle.digest("SHA-256", toBytes$1(input)); }, sha512: async (input) => { const subtle = getSubtle(); const digest = await subtle.digest("SHA-512", toBytes$1(input)); return hex(digest); }, sha512bytes: async (input) => { const subtle = getSubtle(); return subtle.digest("SHA-512", toBytes$1(input)); }, md5: async (input) => md5( typeof input === "string" ? input : toBytes$1(input).buffer ), md5bytes: async (input) => unhex(md5( typeof input === "string" ? input : toBytes$1(input).buffer )), sm3: async (input) => hex(sm3(toBytes$1(input)).buffer), sm3bytes: async (input) => sm3(toBytes$1(input)).buffer }; const WIRE_VARINT = 0; const WIRE_BYTES = 2; const WIRE_FIXED32 = 5; class ProtobufWriter { constructor() { __publicField(this, "buf", []); } writeVarint(value) { let v = value >>> 0; while (v >= 128) { this.buf.push(v & 127 | 128); v >>>= 7; } this.buf.push(v & 127); return this; } writeKey(fieldNumber, wireType) { return this.writeVarint(fieldNumber << 3 | wireType); } varint(fieldNumber, value) { return this.writeKey(fieldNumber, WIRE_VARINT).writeVarint(value); } fixed32(fieldNumber, value) { this.writeKey(fieldNumber, WIRE_FIXED32); const v = value >>> 0; this.buf.push(v & 255, v >>> 8 & 255, v >>> 16 & 255, v >>> 24 & 255); return this; } bytes(fieldNumber, data) { this.writeKey(fieldNumber, WIRE_BYTES).writeVarint(data.length); for (let i2 = 0; i2 < data.length; i2++) { this.buf.push(data[i2] & 255); } return this; } string(fieldNumber, value) { return this.bytes(fieldNumber, new TextEncoder().encode(value)); } message(fieldNumber, build) { const sub = new ProtobufWriter(); build(sub); return this.bytes(fieldNumber, sub.toBytes()); } toBytes() { return Uint8Array.from(this.buf); } } const ROUNDS$1 = 72; const MASK64$1 = 0xffffffffffffffffn; const Z4 = 0x3dc94c3a046d678bn; function getBit(value, position) { return value >> BigInt(position) & 1n; } function rotateLeft64(v, n) { return (v << n | v >> 64n - n) & MASK64$1; } function rotateRight64(v, n) { return (v << 64n - n | v >> n) & MASK64$1; } function keyExpansion(key) { const k = [key[0] & MASK64$1, key[1] & MASK64$1, key[2] & MASK64$1, key[3] & MASK64$1]; for (let i2 = 4; i2 < ROUNDS$1; i2++) { let tmp = rotateRight64(k[i2 - 1], 3n); tmp ^= k[i2 - 3]; tmp ^= rotateRight64(tmp, 1n); k.push((~k[i2 - 4] ^ tmp ^ getBit(Z4, (i2 - 4) % 62) ^ 3n) & MASK64$1); } return k; } function simonEncrypt(plaintext, key) { const k = keyExpansion(key); let x = plaintext[0] & MASK64$1; let y = plaintext[1] & MASK64$1; for (let i2 = 0; i2 < ROUNDS$1; i2++) { const tmp = y; const f2 = rotateLeft64(y, 1n) & rotateLeft64(y, 8n); y = (x ^ f2 ^ rotateLeft64(y, 2n) ^ k[i2]) & MASK64$1; x = tmp; } return [x, y]; } const LOW_RAND = new Uint8Array([242, 129]); const HIGH_RAND = new Uint8Array([97, 111]); const XOR_PREFIX = new Uint8Array([242, 247, 252, 255, 242, 247, 252, 255]); function sm3Prefix6(data) { return sm3(data).slice(0, 6); } function decodeStub(xssStub) { const bytes = new Uint8Array(16); if (xssStub.length >= 32) { for (let i2 = 0; i2 < 16; i2++) { bytes[i2] = parseInt(xssStub.slice(i2 * 2, i2 * 2 + 2), 16) & 255; } } return bytes; } function buildProtobuf(query, xssStub, timestamp, config) { const params = new URLSearchParams(query); const deviceId = params.get("device_id") ?? ""; const versionName = params.get("version_name") ?? ""; const bodyHash = sm3Prefix6(xssStub === "" ? new Uint8Array(16) : decodeStub(xssStub)); const queryHash = sm3Prefix6( query === "" ? new Uint8Array(16) : new TextEncoder().encode(query) ); const rand = getCrypto().getRandomValues(new Uint32Array(1))[0] % 2147483647; return new ProtobufWriter().varint(1, 538970409 * 2).varint(2, 2).varint(3, rand).string(4, config.aid).string(5, deviceId).string(6, config.licenseId).string(7, versionName).string(8, config.sdkVersion).varint(9, config.sdkVersionInt).bytes(10, new Uint8Array(8)).varint(11, 0).varint(12, timestamp * 2).bytes(13, bodyHash).bytes(14, queryHash).message(15, (sub) => { sub.varint(1, 1).varint(2, 1).varint(3, 1).varint(7, 3348294860); }).string(16, "").string(20, "none").varint(21, config.callType).message(23, (sub) => { sub.string(1, "NX551J").varint(2, 8196).varint(4, 2162219008); }).varint(25, 2).toBytes(); } async function getArgus(query, xssStub, timestamp, config) { const { signKey } = config; if (signKey.length !== 32) { throw new Error(`Sign key must be 32 bytes, got ${signKey.length}`); } const protobuf = pkcs7Pad(buildProtobuf(query, xssStub, timestamp, config), 16); const sm3Input = new Uint8Array(signKey.length * 2 + LOW_RAND.length + HIGH_RAND.length); sm3Input.set(signKey, 0); sm3Input.set(LOW_RAND, signKey.length); sm3Input.set(HIGH_RAND, signKey.length + LOW_RAND.length); sm3Input.set(signKey, signKey.length + LOW_RAND.length + HIGH_RAND.length); const sm3Output = sm3(sm3Input); const keyView = new DataView(sm3Output.buffer, sm3Output.byteOffset, sm3Output.byteLength); const simonKey = [ keyView.getBigUint64(0, true), keyView.getBigUint64(8, true), keyView.getBigUint64(16, true), keyView.getBigUint64(24, true) ]; const encrypted = new Uint8Array(protobuf.length); const pbView = new DataView(protobuf.buffer, protobuf.byteOffset, protobuf.byteLength); const encView = new DataView(encrypted.buffer); for (let offset = 0; offset < protobuf.length; offset += 16) { const [low, high] = simonEncrypt( [pbView.getBigUint64(offset, true), pbView.getBigUint64(offset + 8, true)], simonKey ); encView.setBigUint64(offset, low, true); encView.setBigUint64(offset + 8, high, true); } const data = new Uint8Array(XOR_PREFIX.length + encrypted.length); data.set(XOR_PREFIX); data.set(encrypted, XOR_PREFIX.length); for (let i2 = XOR_PREFIX.length; i2 < data.length; i2++) { data[i2] ^= data[i2 % 8]; } data.reverse(); const header = new Uint8Array([166, 110, 173, 159, 119, 1, 208, 12, 24]); const plaintext = new Uint8Array(header.length + data.length + HIGH_RAND.length); plaintext.set(header); plaintext.set(data, header.length); plaintext.set(HIGH_RAND, header.length + data.length); const subtle = getSubtle(); const aesKey = await subtle.importKey( "raw", await hash.md5bytes(signKey.slice(0, 16)), { name: "AES-CBC" }, false, ["encrypt"] ); const iv = await hash.md5bytes(signKey.slice(16)); const ciphertext = new Uint8Array( await subtle.encrypt({ name: "AES-CBC", iv }, aesKey, plaintext) ); const result = new Uint8Array(LOW_RAND.length + ciphertext.length); result.set(LOW_RAND); result.set(ciphertext, LOW_RAND.length); return b64encode(result.buffer); } const ROUNDS = 34; const MASK64 = 0xffffffffffffffffn; const WORD_SIZE = 64n; const ALPHA = 8n; const BETA = 3n; function readUint64LE(view, offset) { return view.getBigUint64(offset, true); } function keySchedule(key) { const view = new DataView(key.buffer, key.byteOffset, key.byteLength); const ks = [readUint64LE(view, 0) & MASK64]; const numWords = key.length * 8 / Number(WORD_SIZE); const ls = []; for (let i2 = 1; i2 < numWords; i2++) { ls.push(readUint64LE(view, i2 * 8) & MASK64); } for (let x = 0; x < ROUNDS - 1; x++) { const rsX = (ls[x] << WORD_SIZE - ALPHA) + (ls[x] >> ALPHA) & MASK64; const addSxy = rsX + ks[x] & MASK64; const newX = BigInt(x) ^ addSxy; const lsY = (ks[x] >> WORD_SIZE - BETA) + (ks[x] << BETA) & MASK64; ls.push(newX); ks.push(newX ^ lsY); } return ks; } function encryptBlock(ks, block, out, outOffset) { const view = new DataView(block.buffer, block.byteOffset, block.byteLength); let y = readUint64LE(view, 0); let x = readUint64LE(view, 8); for (const k of ks) { const rsX = (x << WORD_SIZE - ALPHA) + (x >> ALPHA) & MASK64; const addSxy = rsX + y & MASK64; x = k ^ addSxy; const lsY = (y >> WORD_SIZE - BETA) + (y << BETA) & MASK64; y = x ^ lsY; } const outView = new DataView(out.buffer, out.byteOffset, out.byteLength); outView.setBigUint64(outOffset, y & MASK64, true); outView.setBigUint64(outOffset + 8, x & MASK64, true); } function speckEncrypt(key, plaintext) { if (key.length !== 32) { throw new Error(`Speck key must be 32 bytes, got ${key.length}`); } const padded = pkcs7Pad(plaintext, 16); const ks = keySchedule(key); const out = new Uint8Array(padded.length); for (let i2 = 0; i2 < padded.length; i2 += 16) { encryptBlock(ks, padded.subarray(i2, i2 + 16), out, i2); } return out; } async function generateLadonKey(randomBytes, aid) { const aidBytes = new TextEncoder().encode(aid); const input = new Uint8Array(randomBytes.length + aidBytes.length); input.set(randomBytes); input.set(aidBytes, randomBytes.length); const hex2 = await hash.md5(input); return new TextEncoder().encode(hex2); } async function getLadon(timestamp, config) { const randomBytes = getCrypto().getRandomValues(new Uint8Array(4)); const plaintext = new TextEncoder().encode( `${timestamp}-${config.licenseId}-${config.aid}` ); const key = await generateLadonKey(randomBytes, config.aid); const encrypted = speckEncrypt(key, plaintext); const result = new Uint8Array(randomBytes.length + encrypted.length); result.set(randomBytes); result.set(encrypted, randomBytes.length); return b64encode(result.buffer); } const defaultUnidbgConfig = { signKey: new Uint8Array( unhex("ac1adaae95a7af94a5114ab3b3a97dd80050aa0a39314c40528caec95256c28c") ), aid: "1967", licenseId: "1611921764", sdkVersion: "v04.04.05-ov-android", sdkVersionInt: 134744640, callType: 738 }; async function generateHeaders(rawQuery, xssStub = "", timestamp = Math.floor(Date.now() / 1e3), config = defaultUnidbgConfig) { const [argus, ladon] = await Promise.all([ getArgus(rawQuery, xssStub, timestamp, config), getLadon(timestamp, config) ]); return { "x-argus": argus, "x-ladon": ladon, "x-khronos": String(timestamp) }; } async function signRequest(url, body, config = defaultUnidbgConfig) { const rawQuery = new URL(url).search.replace(/^\?/, ""); const hasBody = typeof body === "string" ? body.length > 0 : ((body == null ? void 0 : body.byteLength) ?? 0) > 0; const xssStub = hasBody ? await hash.md5(body) : ""; const now = Date.now(); const headers = await generateHeaders( rawQuery, xssStub, Math.floor(now / 1e3), config ); headers["x-ss-req-ticket"] = String(now); if (hasBody) { headers["X-SS-STUB"] = xssStub; } return headers; } async function gzip(data) { if (typeof data === "string") { data = new TextEncoder().encode(data).buffer; } const encoder = new CompressionStream("gzip"); const stream = new Blob([data]).stream().pipeThrough(encoder); const compressed = new Response(stream).arrayBuffer(); return compressed; } async function gunzip(data) { const decoder = new DecompressionStream("gzip"); const stream = new Blob([data]).stream().pipeThrough(decoder); const decompressed = new Response(stream).arrayBuffer(); return decompressed; } const FIXED_STRING = b64decode( "TdTC5rgxYgkOUrPHpnM7pByyRiuCmrWKGWs521cXdST0m69/COjWjSanLjfBqVovHwWlGJKu8pSXMrYqOKrdWA==" ); async function encrypt(data) { const crypto = getCrypto(); const subtle = getSubtle(); const randomBytes = crypto.getRandomValues(new Uint8Array(32)); const hashValue = await hash.sha512bytes( concatArrayBuffers(await hash.sha512bytes(randomBytes), FIXED_STRING) ); const k = hashValue.slice(0, 16); const iv = hashValue.slice(16, 32); const compressedData = await gzip(data); const hashedData = concatArrayBuffers( await hash.sha512bytes(compressedData), compressedData ); const key = await subtle.importKey("raw", k, { name: "AES-CBC", length: 128 }, false, ["encrypt"]); const encryptedData = await subtle.encrypt({ name: "AES-CBC", iv }, key, hashedData); return concatArrayBuffers( new Uint8Array([116, 99, 5, 16, 0, 0]).buffer, randomBytes.buffer, encryptedData ); } const ANDROID_VERSIONS = [ { version: "9", api: 29 }, { version: "10", api: 30 }, { version: "11", api: 31 }, { version: "12", api: 32 }, { version: "13", api: 33 }, { version: "14", api: 34 } ]; const DEVICE_MODELS = [ "RMX1931", "MI8", "Honor10", "P30", "V1921A", "Redmi Note 7", "Redmi K20 Pro", "MI 9", "Mi 10 Pro", "SM-G9750", "Pixel 6", "HD1910", "M2011K2C", "LIO-AN00", "VOG-TL00", "PCLM10", "GM1900", "Pixel 7 Pro", "Pixel 6a", "SM-N9760", "POCO F1" ]; const DEVICE_BRANDS = [ "realme", "Xiaomi", "Huawei", "OPPO", "vivo", "samsung", "OnePlus", "google", "Redmi", "HONOR", "motorola", "POCO" ]; const HEX_LOW = "0123456789abcdef"; const deviceValue = { aid: "1967", appName: "novelapp", channel: "0", platform: "android", osVersion: "0", versionCode: { str: "6.3.9.32", val: "63932" } }; function randomInt(max) { return getCrypto().getRandomValues(new Uint32Array(1))[0] % max; } function randomItem(list) { return list[randomInt(list.length)]; } function randomHex(k) { if (k <= 0) return ""; const bytes = getCrypto().getRandomValues(new Uint8Array(k)); let result = ""; for (let i2 = 0; i2 < k; i2++) { result += HEX_LOW[bytes[i2] % 16]; } return result; } function uuid() { const c = getCrypto(); if (typeof c.randomUUID === "function") { return c.randomUUID(); } const b = c.getRandomValues(new Uint8Array(16)); b[6] = b[6] & 15 | 64; b[8] = b[8] & 63 | 128; const h2 = [...b].map((x) => x.toString(16).padStart(2, "0")).join(""); return `${h2.slice(0, 8)}-${h2.slice(8, 12)}-${h2.slice(12, 16)}-${h2.slice(16, 20)}-${h2.slice(20)}`; } function ipv6LinkLocal() { return "::1"; } function ipv6UniqueLocal() { const x = randomInt(1 << 8); let result = `fd${HEX_LOW[x >> 4]}${HEX_LOW[x & 15]}`; for (let i2 = 0; i2 < 7; i2++) { const v = randomInt(1 << 16); result += ":" + HEX_LOW[v >> 12 & 15] + HEX_LOW[v >> 8 & 15] + HEX_LOW[v >> 4 & 15] + HEX_LOW[v & 15]; } return result; } function generateRequestBody() { const osInfo = randomItem(ANDROID_VERSIONS); const deviceBrand = randomItem(DEVICE_BRANDS); const genTime = Date.now(); const romVersion = "coloros__" + randomHex(4).toUpperCase() + "." + String(randomInt(1e6)).padStart(6, "0") + "." + String(randomInt(1e8)).padStart(8, "0") + " release-keys"; return { magic_tag: "ss_app_log", header: { display_name: "番茄免费小说", aid: 1967, channel: "43536163a", package: "com.dragon.read", sdk_version: "3.7.0-rc.25-fanqie-xiaoshuo", sdk_target_version: 29, git_hash: "711d1a7", density_dpi: 240, display_density: "hdpi", resolution: "720x1280", language: "zh", timezone: 8, access: "wifi", not_request_sender: 0, carrier: "CHINA MOBILE", mcc_mnc: "46000", region: "CN", tz_name: "Asia/Shanghai", tz_offset: 28800, sim_region: "cn", sim_serial_number: [], oaid_may_support: false, device_platform: "android", custom: { host_bit: 32, dragon_device_type: 0 }, pre_installed_channel: "", is_system_app: 0, sdk_flavor: "china", guest_mode: 0, // 设备硬件与系统信息 os: "Android", os_version: osInfo.version, os_api: osInfo.api, device_model: randomItem(DEVICE_MODELS), device_brand: deviceBrand, device_manufacturer: deviceBrand, cpu_abi: "arm64-v8a", release_build: randomHex(7), cdid: uuid(), sig_hash: "a4a27c2633195374c15651ffc3c4a497", openudid: randomHex(20), clientudid: uuid(), req_id: uuid(), // 可选字段 rom: randomHex(14).toUpperCase(), rom_version: romVersion, apk_first_install_time: genTime - randomInt(365) * 864e5, ipv6_list: [ { type: "client_anpi", value: ipv6LinkLocal() }, { type: "client_anpi", value: ipv6UniqueLocal() }, { type: "client_anpi", value: ipv6UniqueLocal() } ] }, _gen_time: genTime }; } const REGISTER_URL = "https://i.snssdk.com/service/2/device_register/?tt_data=a"; const READING_BASE = "https://reading.snssdk.com"; const USER_AGENT = "com.dragon.read"; const APP_REQUEST = { credentials: "omit" }; function buildQuery(device, versionCode, extra) { return new URLSearchParams({ device_id: device.device_id, iid: device.install_id, device_type: device.device_type, aid: deviceValue.aid, app_name: deviceValue.appName, channel: deviceValue.channel, device_platform: deviceValue.platform, os_version: deviceValue.osVersion, version_code: versionCode, ...extra }); } async function registerDevice() { const body = generateRequestBody(); const encrypted = await encrypt( new TextEncoder().encode(JSON.stringify(body)).buffer ); const res = await apiFetch(REGISTER_URL, { ...APP_REQUEST, method: "POST", headers: { "User-Agent": "okhttp/4.10.0", "Content-Type": "application/octet-stream; tt-data=a" }, body: encrypted }); if (res.status !== 200) { throw new Error(`设备注册失败: HTTP ${res.status} ${res.statusText}`); } const json = res.json(); if (!(json == null ? void 0 : json.device_id) || !json.device_id_str || !json.install_id_str) { throw new Error(`设备注册失败: device_id 无效, 响应=${res.responseText}`); } const device = { device_id: json.device_id_str, install_id: json.install_id_str, device_type: body.header.device_model }; console.log("设备注册成功!", device); return device; } async function activatePremium(device) { var _a; const url = `${READING_BASE}/reading/user/privilege/add/v?` + buildQuery(device, deviceValue.versionCode.val, { manifest_version_code: deviceValue.versionCode.val, update_version_code: deviceValue.versionCode.val }).toString(); const body = `{"add_count_daily":0,"amount":2592000,"privilege_id":7210376203117531962,"from":8,"unique_key":"${Date.now()}"}`; try { const headers = await signRequest(url, body); const res = await apiFetch(url, { ...APP_REQUEST, method: "POST", headers: { ...headers, "User-Agent": USER_AGENT, "Content-Type": "application/json; charset=utf-8" }, body }); const json = res.json(); if ((json == null ? void 0 : json.code) !== 0) { console.warn("设备会员激活失败:", res.responseText); return ""; } const expireTime = ((_a = json.data) == null ? void 0 : _a.expire_time) ?? ""; console.log("设备会员已成功激活!过期时间:", expireTime); return expireTime; } catch (e) { console.warn("设备会员激活失败:", e); return ""; } } async function registerKey(device) { var _a, _b; const url = `${READING_BASE}/reading/crypt/registerkey?` + buildQuery(device, deviceValue.versionCode.str).toString(); const idBytes = new Uint8Array(16); let id = BigInt(device.device_id); for (let i2 = 15; i2 >= 0; i2--) { idBytes[i2] = Number(id & 0xffn); id >>= 8n; } idBytes.reverse(); const subtle = getSubtle(); const iv = getCrypto().getRandomValues(new Uint8Array(16)); const key = await subtle.importKey("raw", shared_key, { name: "AES-CBC" }, false, ["encrypt"]); const encrypted = new Uint8Array( await subtle.encrypt({ name: "AES-CBC", iv }, key, idBytes) ); const content = new Uint8Array(iv.length + encrypted.length); content.set(iv); content.set(encrypted, iv.length); const plainBody = JSON.stringify({ content: b64encode(content.buffer) }); const gzipped = await gzip(plainBody); const headers = await signRequest(url, gzipped); const res = await apiFetch(url, { ...APP_REQUEST, method: "POST", headers: { ...headers, "User-Agent": USER_AGENT, "Content-Type": "application/json; charset=utf-8", "Content-Encoding": "gzip" }, body: gzipped }); if (res.status !== 200) { throw new Error(`密钥注册失败: HTTP ${res.status} ${res.statusText}`); } const json = res.json(); const encryptedKey = (_a = json == null ? void 0 : json.data) == null ? void 0 : _a.key; if (!encryptedKey) { throw new Error(`密钥注册失败: 响应缺少 key, 响应=${res.responseText}`); } const buf = b64decode(encryptedKey); const decryptKey = await subtle.importKey( "raw", shared_key, { name: "AES-CBC" }, false, ["decrypt"] ); const finalKey = await subtle.decrypt( { name: "AES-CBC", iv: buf.slice(0, 16) }, decryptKey, buf.slice(16) ); const keyInfo = { key: finalKey, keyver: (_b = json.data) == null ? void 0 : _b.keyver }; console.log("密钥获取成功,版本:", keyInfo.keyver, "key:", hex(finalKey)); return keyInfo; } const STORE_KEY = "device"; function load() { const s = read(STORE_KEY); if (!(s == null ? void 0 : s.device_id) || !(s == null ? void 0 : s.install_id)) return null; if (s.key) { if (!read("keyinfo")) { write("keyinfo", { key: s.key, keyver: s.keyver }); } delete s.key; delete s.keyver; write(STORE_KEY, s); } return { device_id: s.device_id, install_id: s.install_id, device_type: s.device_type, device_brand: s.device_brand }; } function save(c, vipExpireTime) { const s = { device_id: c.device_id, install_id: c.install_id, device_type: c.device_type, device_brand: c.device_brand, vip_expire_time: vipExpireTime }; write(STORE_KEY, s); } async function provisionDevice() { const dev = await registerDevice(); const vipExpireTime = await activatePremium(dev); const keyInfo = await registerKey(dev); const c = { device_id: dev.device_id, install_id: dev.install_id, device_type: dev.device_type, key_info: keyInfo }; _config.currentConfig = c; save(c, vipExpireTime); write("keyinfo", { key: b64encode(keyInfo.key), keyver: keyInfo.keyver }); return c; } async function ensureDevice() { const { deviceId, installId, deviceType } = settings$1; if (deviceId.trim() && installId.trim()) { const manual = { device_id: deviceId.trim(), install_id: installId.trim(), device_type: deviceType.trim() || void 0, // 手填设备没有密钥,正文接口会按需自行注册 key_info: void 0 }; _config.currentConfig = manual; console.log("使用设置里手填的设备:", manual.device_id); return manual; } const cached = load(); if (cached) { _config.currentConfig = cached; console.log("复用已缓存设备:", cached.device_id); return cached; } try { return await provisionDevice(); } catch (e) { console.warn("设备注册失败,回退到内置匿名设备:", e); _config.currentConfig = defaultConfig; return defaultConfig; } } let replaceInflight = null; let unverified = false; function markDeviceHealthy() { unverified = false; } function replaceDevice() { if (replaceInflight) return replaceInflight; replaceInflight = doReplace().finally(() => { replaceInflight = null; }); return replaceInflight; } async function doReplace() { if (unverified) { console.warn( "[fqa:device] 刚换过设备仍然收到空响应,问题多半不在设备(检查网络代理是否拦了 reading.snssdk.com)" ); return null; } if (settings$1.deviceId.trim() && settings$1.installId.trim()) { console.warn("[fqa:device] 当前是手填设备,不自动替换。若接口持续失败,请到设置里清空或改用自动注册"); return null; } unverified = true; console.warn("[fqa:device] 当前设备已被服务端作废,正在注册新设备…"); try { del("keyinfo"); _config.currentConfig.key_info = void 0; const c = await provisionDevice(); console.log("[fqa:device] 已换到新设备:", c.device_id); return c; } catch (e) { console.error("[fqa:device] 注册新设备失败:", e); return null; } } const appBaseUrl = "https://reading.snssdk.com/reading"; const redcandleBaseUrl = "https://api5-sinfonlinec.jxbhmy.com/reading"; const webBaseUrl = "https://fanqienovel.com/reading"; const appUserAgent = "com.dragon.read"; function buildAppQuery(extra) { const c = _config.currentConfig; return new URLSearchParams({ iid: c.install_id, device_id: c.device_id, ac: "wifi", channel: "43536163a", aid: "1967", app_name: "novelapp", version_code: "70132", version_name: "7.0.1.32", device_platform: "android", os: "android", ssmix: "a", os_version: "10", device_type: c.device_type || "P30", device_brand: c.device_brand || "realme", update_version_code: "70132", manifest_version_code: "70132", pv_player: "70132", ...extra }); } async function webGet(path, query, credentials2 = "omit") { const url = `${webBaseUrl}${path}?${buildAppQuery(query).toString()}`; const signed = await signRequest(url); const res = await fetch$1(url, { headers: signed, credentials: credentials2 }); if (!res.ok) { throw new Error(`请求失败(${res.status})`); } return res.json(); } function isUsable(res) { if (!res || res.status !== 200) return false; try { const j = res.json(); return !j || j.code === void 0 || j.code === 0; } catch { return false; } } async function requestApp(path, query, headers) { const url = `${appBaseUrl}${path}?${buildAppQuery(query).toString()}`; const signed = await signRequest(url); return apiFetch(url, { method: "GET", headers: { ...signed, "User-Agent": appUserAgent, ...headers } }); } async function requestRedcandle(path, query, headers) { const url = `${redcandleBaseUrl}${path}?${buildAppQuery(query).toString()}`; return apiFetch(url, { method: "GET", headers: { "User-Agent": appUserAgent, ...headers } }); } async function requestAppWithRecovery(path, query, headers) { const res = await requestApp(path, query, headers); if (!isEmptyResponse(res)) { markDeviceHealthy(); return res; } console.warn(`[fqa:api] ${path} 返回空响应体,判定当前设备已失效`); if (!await replaceDevice()) return res; const retry = await requestApp(path, query, headers); if (!isEmptyResponse(retry)) markDeviceHealthy(); return retry; } async function appGet(path, query, headers) { if (settings$1.apiPreference === "redcandle") { try { const res = await requestRedcandle(path, query, headers); if (isUsable(res)) return res; console.warn(`[fqa:api] 红烛接口数据不全,回落到番茄 APP: ${path}`); } catch (e) { console.warn(`[fqa:api] 红烛接口请求失败,回落到番茄 APP: ${path}`, e); } } return requestAppWithRecovery(path, query, headers); } async function appPost(path, body, query, headers) { return postSigned(appBaseUrl + path, body, query, headers); } async function postSigned(base, body, query, headers) { const url = `${base}?${buildAppQuery(query).toString()}`; const signed = await signRequest(url, body); console.log("---start--- APP POST ", url); const res = await apiFetch(url, { method: "POST", headers: { ...signed, "User-Agent": appUserAgent, "Content-Type": "application/json; charset=utf-8", ...headers }, body }); console.log("---complete--- APP POST ", url, res); return res; } async function decryptChapter(encrypted, rawData, config = defaultConfig) { var _a; if (!encrypted) { throw new Error("Invalid encrypted chapter"); } const buf = b64decode(encrypted); const iv = buf.slice(0, 16); const data = buf.slice(16); const key = (_a = config.key_info) == null ? void 0 : _a.key; if (!key) { throw new Error("Missing decrypt key"); } const subtle = getSubtle(); const cryptoKey = await subtle.importKey( "raw", key, { name: "AES-CBC" }, false, ["decrypt"] ); return subtle.decrypt( { name: "AES-CBC", iv }, cryptoKey, data ).then(async (decrypted) => { if (rawData && (rawData == null ? void 0 : rawData.compress_status) === 1) { decrypted = await gunzip(decrypted); } const decoder = new TextDecoder(); const plain = decoder.decode(decrypted); if (plain.trim().startsWith("<")) { return plain; } try { return JSON.parse(plain); } catch (e) { console.warn("Invalid chapter content: ", plain, e); return void 0; } }); } async function decryptComicImage(image, key) { const subtle = getSubtle(); const cryptoKey = await subtle.importKey( "raw", unhex(key), { name: "AES-GCM" }, false, ["decrypt"] ); const iv = image.slice(0, 12); const data = image.slice(12); return await subtle.decrypt( { name: "AES-GCM", iv }, cryptoKey, data ); } function reverseHex(value) { const be = BigInt(value).toString(16).padStart(32, "0"); let result = ""; for (let i2 = be.length; i2 > 0; i2 -= 2) result += be.slice(i2 - 2, i2); return result; } async function encryptKeyinfoBody(config) { const deviceId = config.device_id; const iv = new TextEncoder().encode(randomString(16)); const data = new Uint8Array(unhex(reverseHex(deviceId))).slice(0, 8); console.log(data); const subtle = getSubtle(); const k = await subtle.importKey( "raw", shared_key, { name: "AES-CBC" }, false, ["encrypt"] ); const encrypted = await subtle.encrypt( { name: "AES-CBC", iv }, k, data ); const final = new Uint8Array(iv.length + encrypted.byteLength); console.log(final); final.set(iv, 0); final.set(new Uint8Array(encrypted), iv.length); return JSON.stringify({ content: b64encode(final.buffer) }); } async function decryptKeyinfoResponse(encrypted) { const buf = b64decode(encrypted); const iv = buf.slice(0, 16); const data = buf.slice(16); const subtle = getSubtle(); const k = await subtle.importKey( "raw", shared_key, { name: "AES-CBC" }, false, ["decrypt"] ); return subtle.decrypt( { name: "AES-CBC", iv }, k, data ); } async function refreshKeyinfo() { var _a, _b, _c; const b = await encryptKeyinfoBody(_config.currentConfig); const res = await appPost("/crypt/registerkey", b); const j = res.json(); const ek = (_a = j == null ? void 0 : j.data) == null ? void 0 : _a.key; if (!ek) { throw new Error(`Failed to get key info: ${res.responseText}`); } const key = await decryptKeyinfoResponse(ek); const keyinfo = { key, keyver: (_b = j == null ? void 0 : j.data) == null ? void 0 : _b.keyver }; console.log("Refreshed key info:", keyinfo); _config.currentConfig.key_info = keyinfo; write("keyinfo", { key: b64encode(key), keyver: (_c = j == null ? void 0 : j.data) == null ? void 0 : _c.keyver }); } let refreshInflight = null; function refreshKey() { if (!refreshInflight) { refreshInflight = refreshKeyinfo().finally(() => { refreshInflight = null; }); } return refreshInflight; } async function ensureKeyinfo(expectedKeyVersion) { const keyinfo = _config.currentConfig.key_info; const cachedKeyInfo = read("keyinfo"); console.log("cached key info: ", cachedKeyInfo); if (cachedKeyInfo) { const cki = { key: b64decode(cachedKeyInfo.key), keyver: cachedKeyInfo.keyver }; if (typeof expectedKeyVersion === "undefined" || cki.keyver === expectedKeyVersion) { _config.currentConfig.key_info = cki; return; } } if (!keyinfo) { return await refreshKey(); } if ((keyinfo == null ? void 0 : keyinfo.keyver) !== expectedKeyVersion) { return await refreshKey(); } } async function getChapter(itemId2, _retry) { var _a, _b; if (typeof _retry === "undefined") _retry = 0; if (_retry > 5) { throw new Error(`Failed to get chapter: ${itemId2}`); } if (!_config.currentConfig.key_info) { await ensureKeyinfo(); } const res = await appGet("/reader/full/v", { item_id: itemId2, req_type: "1" }); const j = (_a = res.json()) == null ? void 0 : _a.data; if (!j) { console.warn("Failed to get chapter: ", itemId2, ", response: ", res.responseText); return await getChapter(itemId2, _retry + 1); } if ((j == null ? void 0 : j.content) === "Invalid" || (j == null ? void 0 : j.key_version) !== ((_b = _config.currentConfig.key_info) == null ? void 0 : _b.keyver)) { console.warn("Key reg expired, regster again and retrying..."); if ((j == null ? void 0 : j.content) === "Invalid") { await refreshKey(); } else { await ensureKeyinfo(parseInt(j == null ? void 0 : j.key_version)); } return await getChapter(itemId2, _retry + 1); } j.content = await decryptChapter(j == null ? void 0 : j.content, j, _config.currentConfig); return j; } async function getChapters(itemIds, bookId2 = "0", _retry = 0) { var _a, _b; if (itemIds.length === 0) return {}; if (!_config.currentConfig.key_info) { await ensureKeyinfo(); } const res = await appGet("/reader/batch_full/v", { item_ids: itemIds.join(","), book_id: bookId2, novel_text_type: "1", req_type: "1" }); const raw = (_a = res.json()) == null ? void 0 : _a.data; const entries = raw && typeof raw === "object" ? Array.isArray(raw) ? raw.map((it) => { var _a2; return [String((it == null ? void 0 : it.item_id) ?? ((_a2 = it == null ? void 0 : it.novel_data) == null ? void 0 : _a2.item_id) ?? ""), it]; }) : Object.entries(raw) : []; if (entries.length === 0) { throw new Error(`Failed to batch get chapters: ${res.responseText}`); } const localKeyver = (_b = _config.currentConfig.key_info) == null ? void 0 : _b.keyver; const expired = entries.filter( ([, item]) => (item == null ? void 0 : item.code) === 0 || (item == null ? void 0 : item.code) === void 0 ? (item == null ? void 0 : item.content) === "Invalid" || (item == null ? void 0 : item.key_version) !== void 0 && Number(item.key_version) !== localKeyver : false ); if (expired.length > 0 && _retry < 2) { const [, sample] = expired[0]; console.warn( `[fqa:api] 批量正文密钥失效(${expired.length}/${entries.length} 章),重新注册后重试。本地 keyver=${localKeyver},服务端=${sample == null ? void 0 : sample.key_version}` ); await refreshKey(); await sleep(800); return await getChapters(itemIds, bookId2, _retry + 1); } const results = {}; for (const [id, item] of entries) { if (!id) continue; if ((item == null ? void 0 : item.code) !== void 0 && item.code !== 0) { results[id] = { ...item, item_id: id, error: `code ${item.code}` }; continue; } if (!(item == null ? void 0 : item.content) || item.content === "Invalid") { results[id] = { ...item, item_id: id, error: "Invalid content" }; continue; } try { results[id] = { ...item, item_id: id, novel_data: item.novel_data, content: await decryptChapter(item.content, item, _config.currentConfig) }; } catch (e) { results[id] = { ...item, item_id: id, error: String(e) }; } } return results; } async function getCatalogRaw(bookId2) { var _a; const response = await appGet("/bookapi/directory/all_items/v", { book_id: bookId2 }); const j = response.json(); const items = (_a = j == null ? void 0 : j.data) == null ? void 0 : _a.item_data_list; if ((j == null ? void 0 : j.code) !== 0 || !Array.isArray(items) || items.length === 0) { return [null, null]; } return [items, items.map((it) => String(it.item_id))]; } async function webCatalog(bookId2) { const url = `https://fanqienovel.com/api/reader/directory/detail?bookId=${bookId2}`; const response = await apiFetch(url); const rj = response.json(); const d = rj.data; const allItems = d.allItemIds; const cs = []; const vname = d.volumeNameList; for (let i2 = 0; i2 < vname.length; i2++) { const volumeName = vname[i2]; if (volumeName !== void 0) { cs.push(...d.chapterListWithVolume[i2]); } } return [cs, allItems]; } async function getCatalog(bookId2) { const r = await getCatalogRaw(bookId2); let catalogRaw = r[0]; let allItemIds = r[1]; console.log("catalogRaw", catalogRaw, "allItemIds", allItemIds); if (!catalogRaw || !allItemIds) { const rw = await webCatalog(bookId2); catalogRaw = rw[0]; allItemIds = rw[1]; console.log("webCatalog", catalogRaw, "allItemIds", allItemIds); } const vmap = {}; const chapters = []; catalogRaw.forEach((item) => { const volumeName = item.volume_name ?? ""; const chapterItem = { item_id: String(item.item_id || item.itemId), title: item.title, // YYYY-MM-DD HH:mm:ss update_time: moment((item.first_pass_time || item.firstPassTime) * 1e3).format("YYYY-MM-DD HH:mm:ss"), char_count: item.chapter_word_number || 0, volume_title: volumeName }; chapters.push(chapterItem); if (!vmap[volumeName]) { vmap[volumeName] = { title: volumeName, book_id: bookId2, chapter_list: [] }; } vmap[volumeName].chapter_list.push(chapterItem); }); return { book_id: bookId2, volume_list: Object.values(vmap), chapter_list: chapters, all_item_ids: allItemIds }; } function mappingCreationStatus(status) { switch (status) { case "0": return "完结"; case "1": return "连载"; case "4": return "断更"; default: return "未知"; } } async function getBookInfoRaw(bookId2) { const response = await appGet("/bookapi/detail/v", { book_id: bookId2 }); const j = response.json(); console.log("Book Info:", j); return j.data; } async function getBookInfo(bookId2) { const bookInfo = await getBookInfoRaw(bookId2); if (!bookInfo) { throw new Error("Book not found"); } return { book_id: bookInfo.book_id, title: bookInfo.book_name || bookInfo.original_book_name, author: bookInfo.author, cover_url: bookInfo.thumb_url, summary: bookInfo.abstract, // volume_list: bookInfo.volume_list, update_time: moment(bookInfo.last_chapter_first_pass_time * 1e3).format("YYYY-MM-DD HH:mm:ss"), status: mappingCreationStatus(bookInfo.creation_status) // chapter_count: bookInfo.chapter_count, }; } async function getBookInfoAndCatalog(book) { if (typeof book !== "string") { book = book.book_id; } const bookInfo = await getBookInfo(book); if (!bookInfo) { throw new Error("Book not found"); } const catalog = await getCatalog(bookInfo.book_id); console.log("Catalog:", catalog); bookInfo.volume_list = catalog.volume_list; bookInfo.chapter_list = catalog.chapter_list; return bookInfo; } const CDN_PREFIX = "https://p3-novel.byteimg.com/origin/"; const cssCache = /* @__PURE__ */ new Map(); function stripComments(css) { return css.replace(/\/\*[\s\S]*?\*\//g, ""); } function scopeSelector(selector, scope) { const s = selector.trim(); if (!s) return ""; const where = `:where(${scope})`; if (/^(body|html|:root)$/i.test(s)) return where; const m = s.match(/^(body|html)\b([\s\S]*)$/i); if (m) return `${where}${m[2]}`; return `${where} ${s}`; } function stripRootDecls(body) { return body.split(";").filter((decl) => !/^\s*(color|background|background-color|font-size)\s*:/i.test(decl)).join(";"); } function scopeCss(css, scope) { const src = stripComments(css); let out = ""; let buf = ""; let i2 = 0; while (i2 < src.length) { const ch = src[i2]; if (ch === "{") { const prelude = buf.trim(); buf = ""; i2++; if (prelude.startsWith("@")) { if (/^@(media|supports|document)\b/i.test(prelude)) { const inner2 = readBlock(src, i2); out += `${prelude}{${scopeCss(inner2.text, scope)}}`; i2 = inner2.end; } else { const inner2 = readBlock(src, i2); out += `${prelude}{${inner2.text}}`; i2 = inner2.end; } continue; } const inner = readBlock(src, i2); const selectors = prelude.split(",").map((s) => scopeSelector(s, scope)).filter(Boolean); const isRoot = selectors.length === 1 && selectors[0] === `:where(${scope})`; const declarations = isRoot ? stripRootDecls(inner.text) : inner.text; if (selectors.length && declarations.trim()) { out += `${selectors.join(",")}{${declarations}}`; } i2 = inner.end; continue; } if (ch === ";" && buf.trim().startsWith("@")) { buf = ""; i2++; continue; } buf += ch; i2++; } return out; } function readBlock(src, start) { let depth = 1; let i2 = start; while (i2 < src.length && depth > 0) { const c = src[i2]; if (c === "{") depth++; else if (c === "}") depth--; if (depth === 0) break; i2++; } return { text: src.slice(start, i2), end: Math.min(i2 + 1, src.length) }; } function parseCssMap(cssMap) { if (!cssMap || typeof cssMap !== "string") return {}; try { const parsed = JSON.parse(cssMap); return parsed && typeof parsed === "object" ? parsed : {}; } catch { return {}; } } async function fetchCss(uri) { const cached = cssCache.get(uri); if (cached !== void 0) return cached; try { const res = await fetch(CDN_PREFIX + uri); const text = res.ok ? await res.text() : ""; cssCache.set(uri, text); return text; } catch (e) { console.warn("获取书籍样式表失败:", uri, e); cssCache.set(uri, ""); return ""; } } async function getScopedBookCss(cssMap, scope) { const map = parseCssMap(cssMap); const uris = Object.values(map).filter(Boolean); if (uris.length === 0) return ""; const sheets = await Promise.all(uris.map(fetchCss)); return sheets.filter(Boolean).map((css) => scopeCss(css, scope)).join("\n"); } async function applyBookCss(cssMap, scope, styleId = "fqa-book-style") { const css = await getScopedBookCss(cssMap, scope); let el = document.getElementById(styleId); if (!css) { el == null ? void 0 : el.remove(); return false; } if (!el) { el = document.createElement("style"); el.id = styleId; document.head.appendChild(el); } el.textContent = css; return true; } const FOOTNOTE_CLASS = "fqa-footnote"; const FOOTNOTE_REF_CLASS = "fqa-footnote-ref"; const FOOTNOTE_LIST_CLASS = "fqa-footnote-list"; function byAttr(root, tag, attr, value) { return [...root.querySelectorAll(tag)].filter((el) => { const v = el.getAttribute(attr); if (v === null) return false; return value === void 0 || v === value; }); } function processFootnotes(root) { const notes = /* @__PURE__ */ new Map(); const sections = byAttr(root, "section", "epub:type", "footnotes"); for (const section of sections) { for (const aside of section.querySelectorAll("aside")) { const id = aside.getAttribute("id"); if (id) notes.set(id, aside.innerHTML.trim()); } } if (notes.size === 0) { for (const aside of byAttr(root, "aside", "epub:type", "footnote")) { const id = aside.getAttribute("id"); if (id) notes.set(id, aside.innerHTML.trim()); } } const collectRefs = (scope) => { const set = new Set(byAttr(scope, "a", "epub:type", "noteref")); for (const img of scope.querySelectorAll("img.bdFootnote")) { const a = img.closest('a[href^="#"]'); if (a && scope.contains(a)) set.add(a); } return [...scope.querySelectorAll("a")].filter((a) => set.has(a)); }; const inSection = (el) => sections.some((s) => s.contains(el)); const refs = collectRefs(root).filter((a) => !inSection(a)); if (refs.length === 0 && notes.size === 0) return 0; const ordered = []; const numberOf = /* @__PURE__ */ new Map(); let counter = 0; const makeSup = (num2, text) => { const sup = document.createElement("sup"); sup.className = FOOTNOTE_REF_CLASS; sup.textContent = String(num2); sup.setAttribute("role", "button"); sup.setAttribute("tabindex", "0"); sup.title = stripTags(text); return sup; }; refs.forEach((ref2) => { const href = ref2.getAttribute("href") ?? ""; const id = href.startsWith("#") ? href.slice(1) : ""; const text = notes.get(id); if (text === void 0) return; counter += 1; numberOf.set(id, counter); ordered.push({ num: counter, text }); ref2.replaceWith(makeSup(counter, text)); }); for (const [id, text] of notes) { if (numberOf.has(id)) continue; counter += 1; numberOf.set(id, counter); ordered.push({ num: counter, text }); } for (const section of sections) section.remove(); if (ordered.length > 0) { const list = document.createElement("ol"); list.className = FOOTNOTE_LIST_CLASS; for (const { num: num2, text } of ordered) { const li = document.createElement("li"); li.id = `fqa-fn-${num2}`; li.innerHTML = text; for (const inner of collectRefs(li)) { const innerId = (inner.getAttribute("href") ?? "").replace(/^#/, ""); const innerNum = numberOf.get(innerId); const innerText = notes.get(innerId); if (innerNum && innerText !== void 0) { inner.replaceWith(makeSup(innerNum, innerText)); } else { inner.remove(); } } list.appendChild(li); } const wrapper = document.createElement("section"); wrapper.className = FOOTNOTE_CLASS; const heading = document.createElement("div"); heading.className = "fqa-footnote-title"; heading.textContent = "注释"; wrapper.appendChild(heading); wrapper.appendChild(list); root.appendChild(wrapper); } for (const img of root.querySelectorAll("img.bdFootnote")) img.remove(); return ordered.length; } function stripTags(html) { const el = document.createElement("div"); el.innerHTML = html; return (el.textContent ?? "").replace(/\s+/g, " ").trim(); } const KNOWN_TIERS = /muye-reader-content-(16|20|24|28|32)\b/; function syncFootnoteFontSize(container2) { const box = container2.closest('[class*="muye-reader-content-"]'); const apply2 = () => { container2.style.removeProperty("--fqa-body-size"); if (!box || KNOWN_TIERS.test(box.className)) return; const p = container2.querySelector("p"); if (!p) return; const size = getComputedStyle(p).fontSize; if (size) container2.style.setProperty("--fqa-body-size", size); }; apply2(); if (!box) return; const holder = container2; if (holder.fqaFontObserver) return; const observer2 = new MutationObserver(apply2); observer2.observe(box, { attributes: true, attributeFilter: ["class"] }); holder.fqaFontObserver = observer2; } function bindFootnoteInteraction(container2) { syncFootnoteFontSize(container2); if (container2.dataset.fqaFootnoteBound === "1") return; container2.dataset.fqaFootnoteBound = "1"; const activate = (sup) => { var _a; const num2 = (_a = sup.textContent) == null ? void 0 : _a.trim(); if (!num2) return; const target = container2.querySelector(`#fqa-fn-${num2}`); if (!target) return; target.scrollIntoView({ behavior: "smooth", block: "center" }); target.classList.add("fqa-footnote-active"); setTimeout(() => target.classList.remove("fqa-footnote-active"), 1600); }; container2.addEventListener("click", (e) => { var _a, _b; const sup = (_b = (_a = e.target) == null ? void 0 : _a.closest) == null ? void 0 : _b.call(_a, `.${FOOTNOTE_REF_CLASS}`); if (sup) { e.preventDefault(); activate(sup); } }); container2.addEventListener("keydown", (e) => { var _a, _b; const ke = e; if (ke.key !== "Enter" && ke.key !== " ") return; const sup = (_b = (_a = ke.target) == null ? void 0 : _a.closest) == null ? void 0 : _b.call(_a, `.${FOOTNOTE_REF_CLASS}`); if (sup) { ke.preventDefault(); activate(sup); } }); } const leftIcon = ''; const playingIcon = '\n\n\n\n \n \n \n \n \n \n \n image/svg+xml\n \n \n \n \n \n \n \n \n \n\n'; const pausedIcon = '\n \n \n \n \n\n'; function decodeBase36(c) { if (c >= 48 && c <= 57) return c - 48; if (c >= 97 && c <= 122) return c - 97 + 10; return 255; } function bitCount(n) { n = n - (n >> 1 & 1431655765); n = (n & 858993459) + (n >> 2 & 858993459); return (n + (n >> 4) & 252645135) * 16843009 >> 24; } function decryptSpadeInner(spadeKey) { const result = new Uint8Array(spadeKey); const buff = new Uint8Array(2 + spadeKey.length); buff.set([250, 85], 0); buff.set(spadeKey, 2); for (let i2 = 0; i2 < result.length; i2++) { let v = (spadeKey[i2] ^ buff[i2]) - bitCount(i2) - 21; while (v < 0) { v += 255; } result[i2] = v; } return result; } function decryptSpade(spadeKeyBytes) { const spadeKeyLen = spadeKeyBytes.length; if (spadeKeyLen < 3) return ""; const paddingLen = (spadeKeyBytes[0] ^ spadeKeyBytes[1] ^ spadeKeyBytes[2]) - 48; if (spadeKeyLen < paddingLen + 2) return ""; const innerInput = spadeKeyBytes.slice(1, spadeKeyLen - paddingLen); const tmpBuff = decryptSpadeInner(innerInput); if (tmpBuff.length === 0) return ""; const skipBytes = decodeBase36(tmpBuff[0]); const decodedMessageLen = spadeKeyLen - paddingLen - 2; const endIndex = 1 + decodedMessageLen - skipBytes; if (endIndex > tmpBuff.length) return ""; const finalBytes = tmpBuff.slice(1, endIndex); return new TextDecoder("utf-8").decode(finalBytes); } function decryptSpadeA(spadeAStr) { try { const bytes = new Uint8Array(b64decode(spadeAStr)); return decryptSpade(bytes); } catch (e) { console.error("Spade parsing error", e); return ""; } } async function getPlayInfo(item_id, tone_id) { const item_ids = Array.isArray(item_id) ? item_id.join(",") : item_id; const resp = await appGet("/reader/audio/playinfo/", { item_ids, tone_id: tone_id.toString() }); const j = resp.json(); const results = []; if (Array.isArray(j.data)) { for (let i2 of j.data) { results.push({ urls: [i2.main_url, i2.backup_url].filter(Boolean), vid: i2.vid, key: i2.is_encrypt ? decryptSpadeA(i2.encryption_key) : "", item_id: i2.item_id }); } } return results; } async function getBookAvailableTones(book_id) { var _a; const resp = await appGet("/bookapi/audio/toneinfo/", { book_id }); const j = resp.json(); const results = []; if (Array.isArray((_a = j == null ? void 0 : j.data) == null ? void 0 : _a.tts_tones)) { for (let i2 of j.data.tts_tones) { results.push({ id: i2.id, name: i2.title, gender: i2.tone_gender, icon: i2.icon_url, description: i2.description }); } } return results; } async function getChapterParagraphTimeTag(item_id, tone_id) { var _a; const resp = await appGet("/reader/audio/timepoint/", { item_id, tone_id: tone_id.toString(), req_type: "1" }); const j = resp.json(); const results = []; if (Array.isArray((_a = j == null ? void 0 : j.data) == null ? void 0 : _a.time_points)) { for (let i2 of j.data.time_points) { results.push({ startms: i2.start_time, endms: i2.end_time, startidx: i2.start_para, endidx: i2.end_para, is_title: i2.start_para === 1e4 }); } } return results; } function asBytes(value) { if (value instanceof Uint8Array) { return new Uint8Array(value); } return new Uint8Array(value); } function concatBytes(...arrays) { const length = arrays.reduce((sum, array) => sum + array.byteLength, 0); const result = new Uint8Array(length); let offset = 0; for (const array of arrays) { result.set(array, offset); offset += array.byteLength; } return result; } function ascii(text) { const result = new Uint8Array(text.length); for (let i2 = 0; i2 < text.length; i2++) { result[i2] = text.charCodeAt(i2) & 255; } return result; } function hexToBytes(text) { const hex2 = text.replace(/\s+/g, ""); if (!/^[0-9a-fA-F]*$/.test(hex2) || hex2.length % 2 !== 0) { throw new Error("无效的十六进制数据"); } const result = new Uint8Array(hex2.length / 2); for (let i2 = 0; i2 < result.length; i2++) { result[i2] = parseInt(hex2.slice(i2 * 2, i2 * 2 + 2), 16); } return result; } function uintBytes(value) { if (!Number.isSafeInteger(value) || value < 0) { throw new Error(`无效的无符号整数: ${value}`); } if (value === 0) return new Uint8Array([0]); const result = []; let current = value; while (current > 0) { result.unshift(current & 255); current = Math.floor(current / 256); } return new Uint8Array(result); } function be16(value) { const result = new Uint8Array(2); new DataView(result.buffer).setUint16(0, value); return result; } function be32(value) { const result = new Uint8Array(4); new DataView(result.buffer).setUint32(0, value >>> 0); return result; } function be64(value) { const result = new Uint8Array(8); new DataView(result.buffer).setBigUint64(0, BigInt(value)); return result; } function findBytes(buffer, fourcc, from = 0, to = buffer.length - 4) { if (fourcc.length !== 4) throw new Error(`fourcc 必须是 4 个字符: ${fourcc}`); const c0 = fourcc.charCodeAt(0); const c1 = fourcc.charCodeAt(1); const c2 = fourcc.charCodeAt(2); const c3 = fourcc.charCodeAt(3); for (let i2 = from; i2 <= to; i2++) { if (buffer[i2] === c0 && buffer[i2 + 1] === c1 && buffer[i2 + 2] === c2 && buffer[i2 + 3] === c3) return i2; } return -1; } function strBytes(text) { return new TextEncoder().encode(text); } function readFourcc(buffer, offset) { return String.fromCharCode( buffer[offset] ?? 0, buffer[offset + 1] ?? 0, buffer[offset + 2] ?? 0, buffer[offset + 3] ?? 0 ); } const CONTAINERS = /* @__PURE__ */ new Set([ "moov", "trak", "mdia", "minf", "stbl", "edts", "dinf", "udta", "mvex", "meta" ]); function readBox(buffer, offset, limit) { if (offset + 8 > limit) return null; const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); const size32 = view.getUint32(offset); const type = readFourcc(buffer, offset + 4); let headerSize = 8; let size; if (size32 === 1) { if (offset + 16 > limit) return null; const extended = view.getBigUint64(offset + 8); if (extended > BigInt(Number.MAX_SAFE_INTEGER)) { throw new Error(`MP4 box 过大: ${type}`); } size = Number(extended); headerSize = 16; } else if (size32 === 0) { size = limit - offset; } else { size = size32; } if (size < headerSize || offset + size > limit) return null; return { type, offset, size, headerSize, end: offset + size }; } function walkBoxes(buffer, start, end, visit, depth = 0) { if (depth > 16) return; let offset = start; while (offset + 8 <= end) { const box = readBox(buffer, offset, end); if (!box) return; visit(box); if (CONTAINERS.has(box.type)) { const childStart = box.offset + box.headerSize + (box.type === "meta" ? 4 : 0); if (childStart < box.end) walkBoxes(buffer, childStart, box.end, visit, depth + 1); } offset = box.end; } } function findBox(boxes, type) { return boxes.find((box) => box.type === type); } function findMdatStart(buffer) { let offset = 0; while (offset + 8 <= buffer.length) { const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); const size32 = view.getUint32(offset); const type = readFourcc(buffer, offset + 4); const headerSize = size32 === 1 ? 16 : 8; if (type === "mdat") return offset + headerSize; if (size32 === 0) return -1; if (size32 === 1) { if (offset + 16 > buffer.length) return -1; const size = view.getBigUint64(offset + 8); if (size > BigInt(Number.MAX_SAFE_INTEGER)) return -1; offset += Number(size); } else { offset += size32; } } return -1; } function parseSampleSizes(box, view) { if (box.offset + 20 > box.end) throw new Error("stsz box 不完整"); const sampleSize = view.getUint32(box.offset + 12); const count = view.getUint32(box.offset + 16); const sizes = new Uint32Array(count); if (sampleSize !== 0) { sizes.fill(sampleSize); return sizes; } if (box.offset + 20 + count * 4 > box.end) throw new Error("stsz 采样表不完整"); for (let i2 = 0; i2 < count; i2++) sizes[i2] = view.getUint32(box.offset + 20 + i2 * 4); return sizes; } function parseDurations(box, sampleCount, view) { if (box.offset + 16 > box.end) throw new Error("stts box 不完整"); const entryCount = view.getUint32(box.offset + 12); const durations = new Uint32Array(sampleCount); let index = 0; for (let i2 = 0; i2 < entryCount; i2++) { const offset = box.offset + 16 + i2 * 8; if (offset + 8 > box.end) throw new Error("stts 采样表不完整"); const count = view.getUint32(offset); const duration = view.getUint32(offset + 4); for (let j = 0; j < count && index < sampleCount; j++) durations[index++] = duration; } if (index !== sampleCount) throw new Error(`stts 采样数不一致: ${index}/${sampleCount}`); return durations; } function parseIvs(box, sampleCount, view) { if (box.offset + 16 > box.end) throw new Error("senc box 不完整"); const flags = view.getUint32(box.offset + 8) & 16777215; if ((flags & 2) !== 0) throw new Error("暂不支持带 subsample encryption 的音频"); const count = view.getUint32(box.offset + 12); if (count !== sampleCount) throw new Error(`senc 采样数不一致: ${count}/${sampleCount}`); const dataStart = box.offset + 16; const dataLength = box.end - dataStart; if (count === 0 || dataLength % count !== 0) throw new Error("senc IV 表不完整"); const ivLength = dataLength / count; if (ivLength !== 8 && ivLength !== 16) throw new Error(`不支持的 CENC IV 长度: ${ivLength}`); return asBytes(new Uint8Array(view.buffer, view.byteOffset + dataStart, dataLength)); } function parseTimescale(box, view) { if (box.offset + 24 > box.end) throw new Error("mdhd box 不完整"); const version2 = view.getUint8(box.offset + 8); const offset = version2 === 1 ? box.offset + 28 : box.offset + 20; if (offset + 4 > box.end) throw new Error("mdhd timescale 缺失"); const timescale = view.getUint32(offset); if (timescale === 0) throw new Error("MP4 timescale 为 0"); return timescale; } function parseCodec(buffer, mdatStart) { const end = Math.max(0, Math.min(mdatStart - 1, buffer.length - 4)); const dOps = findBytes(buffer, "dOps", 0, end); if (dOps >= 0) { const channels2 = buffer[dOps + 5] ?? 1; const preSkip = (buffer[dOps + 6] ?? 0) << 8 | (buffer[dOps + 7] ?? 0); return { codec: "opus", channels: channels2, preSkip }; } const encodedEntry = findBytes(buffer, "enca", 0, end); const plainEntry = findBytes(buffer, "mp4a", 0, end); const entryType = encodedEntry >= 0 ? encodedEntry : plainEntry; const channels = entryType >= 0 && entryType + 22 <= buffer.length ? new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength).getUint16(entryType + 20) : 1; if (findBytes(buffer, "dfLa", 0, end) >= 0) { return { codec: "flac", channels: channels || 1, preSkip: 0 }; } if (entryType >= 0) { return { codec: "aac", channels: channels || 1, preSkip: 0 }; } throw new Error("未识别的音频编码(需要 Opus、AAC 或 FLAC)"); } function parseCencMetadata(head) { const mdatStart = findMdatStart(head); if (mdatStart < 0) throw new Error("未找到 mdat,MP4 头可能尚未拉完整"); const boxes = []; walkBoxes(head, 0, Math.min(mdatStart - 8, head.length), (box) => boxes.push(box)); const stsz = findBox(boxes, "stsz"); const stts = findBox(boxes, "stts"); const senc = findBox(boxes, "senc"); const mdhd = findBox(boxes, "mdhd"); if (!stsz || !stts || !senc || !mdhd) { throw new Error("CENC MP4 头缺少 stsz/stts/senc/mdhd"); } const view = new DataView(head.buffer, head.byteOffset, head.byteLength); const sizes = parseSampleSizes(stsz, view); const durations = parseDurations(stts, sizes.length, view); const ivs = parseIvs(senc, sizes.length, view); const timescale = parseTimescale(mdhd, view); const codec = parseCodec(head, mdatStart); const offsets = new Float64Array(sizes.length + 1); for (let i2 = 0; i2 < sizes.length; i2++) { offsets[i2 + 1] = offsets[i2] + sizes[i2]; if (!Number.isSafeInteger(offsets[i2 + 1])) throw new Error("音频采样数据超过 JavaScript 安全整数范围"); } let totalTicks = 0; for (const duration of durations) { totalTicks += duration; if (!Number.isSafeInteger(totalTicks)) throw new Error("音频总时长超过 JavaScript 安全整数范围"); } return { ...codec, sampleCount: sizes.length, sizes, ivs, durations, timescale, mdatStart, offsets, totalTicks }; } function cencIvSize(meta) { return meta.ivs.length / meta.sampleCount; } function normalizeCencKey(value) { if (typeof value === "string") { const bytes2 = hexToBytes(value); if (bytes2.length !== 16) throw new Error("AES-128 key 必须是 32 位 hex"); return bytes2; } const bytes = asBytes(value); if (bytes.length !== 16) throw new Error("AES-128 key 必须是 16 字节"); return bytes.slice(); } async function importCencKey(value) { return getSubtle().importKey( "raw", normalizeCencKey(value), { name: "AES-CTR" }, false, ["decrypt"] ); } function makeCencCounter(iv, counterStart) { if (iv.length !== 8 && iv.length !== 16) throw new Error(`无效的 CENC IV 长度: ${iv.length}`); const counter = new Uint8Array(16); counter.set(iv); if (counterStart === 1) counter[15] = 1; return counter; } async function decryptCencSample(key, iv, ciphertext, counterStart) { const data = asBytes(ciphertext); const plain = await getSubtle().decrypt( { name: "AES-CTR", counter: makeCencCounter(iv, counterStart), length: 64 }, key, data ); return new Uint8Array(plain); } function ebmlVint(value, length) { if (!Number.isSafeInteger(value) || value < 0 || length < 1 || length > 8) { throw new Error(`无效的 EBML VINT: ${value}`); } const result = new Uint8Array(length); result[0] = 128 >> length - 1; let current = value; for (let i2 = length - 1; i2 > 0; i2--) { result[i2] = current & 255; current = Math.floor(current / 256); } const max = Math.pow(2, 7 * length); if (current >= max) throw new Error(`EBML VINT 溢出: ${value}`); result[0] |= current; return result; } function ebmlSize(value) { for (let length = 1; length <= 8; length++) { if (value < Math.pow(2, 7 * length) - 1) return ebmlVint(value, length); } throw new Error(`EBML size 过大: ${value}`); } function ebmlElement(id, payload) { return concatBytes(hexToBytes(id), ebmlSize(payload.length), payload); } function mp4Box(type, payload) { return concatBytes(be32(8 + payload.length), ascii(type), payload); } function matrixIdentity() { const matrix = new Uint8Array(36); const view = new DataView(matrix.buffer); view.setUint32(0, 65536); view.setUint32(16, 65536); view.setUint32(32, 1073741824); return matrix; } function signedBe16(value) { const result = new Uint8Array(2); new DataView(result.buffer).setInt16(0, value); return result; } function buildWebmInit(channels, preSkip, durationSeconds) { const ebml = ebmlElement("1A45DFA3", concatBytes( ebmlElement("4286", hexToBytes("01")), ebmlElement("42F7", hexToBytes("01")), ebmlElement("42F2", hexToBytes("04")), ebmlElement("42F3", hexToBytes("08")), ebmlElement("4282", strBytes("webm")), ebmlElement("4287", hexToBytes("04")), ebmlElement("4285", hexToBytes("02")) )); const codecDelayNs = Math.round(preSkip * 1e9 / 48e3); const duration = new Uint8Array(8); new DataView(duration.buffer).setFloat64(0, durationSeconds, false); const info = ebmlElement("1549A966", concatBytes( ebmlElement("2AD7B1", uintBytes(1e6)), ebmlElement("4489", duration), ebmlElement("4D80", strBytes("clearKeyStreamer")), ebmlElement("5741", strBytes("clearKeyStreamer")) )); const sampleRate = new Uint8Array(4); new DataView(sampleRate.buffer).setFloat32(0, 48e3, false); const audio2 = ebmlElement("E1", concatBytes( ebmlElement("B5", sampleRate), ebmlElement("9F", uintBytes(channels)) )); const track = ebmlElement("AE", concatBytes( ebmlElement("D7", hexToBytes("01")), ebmlElement("73C5", uintBytes(1)), ebmlElement("83", hexToBytes("02")), ebmlElement("86", strBytes("A_OPUS")), ebmlElement("63A2", buildOpusHead(channels, preSkip)), ebmlElement("56AA", uintBytes(codecDelayNs)), ebmlElement("56BB", uintBytes(8e7)), audio2 )); const tracks = ebmlElement("1654AE6B", track); return concatBytes(ebml, hexToBytes("18538067"), hexToBytes("01FFFFFFFFFFFFFF"), info, tracks); } function buildOpusHead(channels, preSkip) { const head = new Uint8Array(19); head.set([79, 112, 117, 115, 72, 101, 97, 100], 0); const view = new DataView(head.buffer); head[8] = 1; head[9] = channels; view.setUint16(10, preSkip, true); view.setUint32(12, 48e3, true); view.setInt16(16, 0, true); head[18] = 0; return head; } function buildWebmCluster(baseMs, relativeMs, packets) { if (relativeMs.length !== packets.length) throw new Error("WebM 时间戳和采样数不一致"); const blocks = []; for (let i2 = 0; i2 < packets.length; i2++) { const relative = relativeMs[i2]; if (relative === void 0 || relative < -32768 || relative > 32767) { throw new Error(`WebM 相对时间戳超出 Int16: ${relative}`); } const trackAndFlags = new Uint8Array([129, 128]); blocks.push(ebmlElement("A3", concatBytes( trackAndFlags.slice(0, 1), signedBe16(relative), trackAndFlags.slice(1), packets[i2] ))); } return ebmlElement("1F43B675", concatBytes( ebmlElement("E7", uintBytes(baseMs)), concatBytes(...blocks) )); } function replaceFourcc(buffer, offset, type) { buffer.set(ascii(type), offset); } function extractSampleEntry(head, mdatStart) { const encoded = findBytes(head, "enca", 0, mdatStart - 1); const plain = findBytes(head, "mp4a", 0, mdatStart - 1); const typeOffset = encoded >= 0 ? encoded : plain; if (typeOffset < 4) throw new Error("未找到 AAC 音频采样入口"); const entryOffset = typeOffset - 4; const view = new DataView(head.buffer, head.byteOffset, head.byteLength); const size = view.getUint32(entryOffset); if (size < 8 || entryOffset + size > head.length) throw new Error("AAC 采样入口不完整"); const entry = new Uint8Array(head.slice(entryOffset, entryOffset + size)); const enca = findBytes(entry, "enca", 0); if (enca >= 0) replaceFourcc(entry, enca, "mp4a"); const sinf = findBytes(entry, "sinf", 8); if (sinf >= 0) replaceFourcc(entry, sinf, "free"); return entry; } function extractFlacEntry(head, mdatStart) { const encoded = findBytes(head, "enca", 0, mdatStart - 1); if (encoded < 4) throw new Error("未找到 FLAC 加密采样入口"); const entryOffset = encoded - 4; const view = new DataView(head.buffer, head.byteOffset, head.byteLength); const size = view.getUint32(entryOffset); if (size < 8 || entryOffset + size > head.length) throw new Error("FLAC 采样入口不完整"); const entry = new Uint8Array(head.slice(entryOffset, entryOffset + size)); replaceFourcc(entry, encoded - entryOffset, "fLaC"); const sinf = findBytes(entry, "sinf", 8); if (sinf >= 0) replaceFourcc(entry, sinf, "free"); return entry; } function buildMp4Init(sampleEntry, timescale, totalTicks) { const totalSeconds = totalTicks / timescale; const matrix = matrixIdentity(); const mvhd = mp4Box("mvhd", concatBytes( be32(0), be32(0), be32(0), be32(1e3), be32(Math.round(totalSeconds * 1e3)), be32(65536), be16(256), be16(0), new Uint8Array(8), matrix, new Uint8Array(24), be32(2) )); const tkhd = mp4Box("tkhd", concatBytes( be32(7), be32(0), be32(0), be32(1), be32(0), be32(0), new Uint8Array(8), be16(0), be16(0), be16(256), be16(0), matrix, be32(0), be32(0) )); const mdhd = mp4Box("mdhd", concatBytes( be32(0), be32(0), be32(0), be32(timescale), be32(totalTicks), be16(21956), be16(0) )); const hdlr = mp4Box("hdlr", concatBytes( be32(0), be32(0), ascii("soun"), new Uint8Array(12) )); const smhd = mp4Box("smhd", concatBytes(be32(0), be16(0), be16(0))); const url = mp4Box("url ", be32(1)); const dref = mp4Box("dref", concatBytes(be32(0), be32(1), url)); const dinf = mp4Box("dinf", dref); const stsd = mp4Box("stsd", concatBytes(be32(0), be32(1), sampleEntry)); const stts = mp4Box("stts", concatBytes(be32(0), be32(0))); const stsc = mp4Box("stsc", concatBytes(be32(0), be32(0))); const stsz = mp4Box("stsz", concatBytes(be32(0), be32(0), be32(0))); const stco = mp4Box("stco", concatBytes(be32(0), be32(0))); const stbl = mp4Box("stbl", concatBytes(stsd, stts, stsc, stsz, stco)); const minf = mp4Box("minf", concatBytes(smhd, dinf, stbl)); const mdia = mp4Box("mdia", concatBytes(mdhd, hdlr, minf)); const trak = mp4Box("trak", concatBytes(tkhd, mdia)); const trex = mp4Box("trex", concatBytes(be32(0), be32(1), be32(1), be32(0), be32(0), be32(0))); const mvex = mp4Box("mvex", trex); const moov = mp4Box("moov", concatBytes(mvhd, trak, mvex)); const ftyp = mp4Box("ftyp", concatBytes(ascii("isom"), be32(512), ascii("isomiso2mp41dash"))); return concatBytes(ftyp, moov); } function buildMp4Segment(baseDecodeTick, samples, sequence) { if (samples.length === 0) throw new Error("MP4 segment 不能没有采样"); const sampleTable = samples.flatMap((sample) => [be32(sample.duration), be32(sample.data.length)]); const mfhd = mp4Box("mfhd", concatBytes(be32(0), be32(sequence))); const tfhd = mp4Box("tfhd", concatBytes(be32(131072), be32(1))); const tfdt = mp4Box("tfdt", concatBytes(be32(16777216), be64(baseDecodeTick))); const makeTrun = (offset) => mp4Box("trun", concatBytes( be32(769), be32(samples.length), be32(offset), concatBytes(...sampleTable) )); const makeMoof = (offset) => mp4Box("moof", concatBytes( mfhd, mp4Box("traf", concatBytes(tfhd, tfdt, makeTrun(offset))) )); const mediaPayload = concatBytes(...samples.map((sample) => sample.data)); const dataOffset = makeMoof(0).length + 8; return concatBytes(makeMoof(dataOffset), mp4Box("mdat", mediaPayload)); } function codecMime(codec) { if (codec === "opus") return 'audio/webm;codecs="opus"'; if (codec === "flac") return 'audio/mp4; codecs="fLaC"'; return 'audio/mp4; codecs="mp4a.40.2"'; } function initSegment(meta, sampleEntry) { if (meta.codec === "opus") { const playableTicks = Math.max(0, meta.totalTicks - meta.preSkip); return buildWebmInit(meta.channels, meta.preSkip, playableTicks / meta.timescale); } if (!sampleEntry) throw new Error("MP4 编码缺少 sample entry"); return buildMp4Init(sampleEntry, meta.timescale, meta.totalTicks); } const DEFAULT_HEAD_BYTES = 512 * 1024; const DEFAULT_MAX_HEAD_BYTES = 8 * 1024 * 1024; const DEFAULT_SEGMENT_SECONDS = 15; const MAX_WEBM_SEGMENT_SECONDS = 30; const DEFAULT_MAX_BUFFER_AHEAD = 40; const DEFAULT_KEEP_BEHIND = 10; const DEFAULT_DECRYPT_CONCURRENCY = 24; function asError(value) { return value instanceof Error ? value : new Error(String(value)); } function abortError$1() { return new DOMException("播放器会话已取消", "AbortError"); } function isAbort(value) { return value instanceof DOMException && value.name === "AbortError"; } function waitForSourceBufferIdle(sourceBuffer, signal) { if (!sourceBuffer.updating) return Promise.resolve(); return new Promise((resolve) => { const done = () => { sourceBuffer.removeEventListener("updateend", done); sourceBuffer.removeEventListener("abort", done); sourceBuffer.removeEventListener("error", done); signal.removeEventListener("abort", done); resolve(); }; sourceBuffer.addEventListener("updateend", done); sourceBuffer.addEventListener("abort", done); sourceBuffer.addEventListener("error", done); signal.addEventListener("abort", done, { once: true }); }); } function describeMediaError(audio2) { const error = audio2.error; if (!error) return ""; const names = { 1: "MEDIA_ERR_ABORTED", 2: "MEDIA_ERR_NETWORK", 3: "MEDIA_ERR_DECODE(数据不是合法音频,多半在传输中被破坏)", 4: "MEDIA_ERR_SRC_NOT_SUPPORTED" }; const name2 = names[error.code] ?? `code ${error.code}`; return `:${name2}${error.message ? ` - ${error.message}` : ""}`; } function validatePositive(value, fallback, name2) { const result = value ?? fallback; if (!Number.isFinite(result) || result <= 0) throw new Error(`${name2} 必须是正数`); return result; } async function mapLimit(items, limit, worker) { const results = new Array(items.length); let next = 0; async function consume() { while (next < items.length) { const index = next++; results[index] = await worker(items[index]); } } await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => consume())); return results; } class SequentialByteStream { constructor(initial = new Uint8Array(), reader = null, openReader = null) { __publicField(this, "chunks", []); __publicField(this, "chunkOffset", 0); __publicField(this, "available", 0); __publicField(this, "reader"); __publicField(this, "openReader"); __publicField(this, "eof", false); if (initial.length > 0) { const copy = new Uint8Array(initial); this.chunks.push(copy); this.available = copy.length; } this.reader = reader; this.openReader = openReader; } /** 至少缓存 length 字节;流先结束时返回 false。 */ async bufferAtLeast(length) { while (this.available < length && !this.eof) await this.pull(); return this.available >= length; } /** 复制当前全部未消费数据,不移动读取位置。 */ snapshot() { const result = new Uint8Array(this.available); let output = 0; for (let i2 = 0; i2 < this.chunks.length; i2++) { const chunk2 = this.chunks[i2]; const start = i2 === 0 ? this.chunkOffset : 0; result.set(chunk2.subarray(start), output); output += chunk2.length - start; } return result; } async skip(length) { await this.readExactly(length); } async readExactly(length) { if (!Number.isSafeInteger(length) || length < 0) { throw new Error(`无效的流读取长度: ${length}`); } if (length === 0) return new Uint8Array(); if (!await this.bufferAtLeast(length)) { throw new Error(`音频流提前结束(还需 ${length - this.available} 字节)`); } const result = new Uint8Array(length); let output = 0; while (output < length) { const chunk2 = this.chunks[0]; const take = Math.min(length - output, chunk2.length - this.chunkOffset); result.set(chunk2.subarray(this.chunkOffset, this.chunkOffset + take), output); output += take; this.chunkOffset += take; this.available -= take; if (this.chunkOffset === chunk2.length) { this.chunks.shift(); this.chunkOffset = 0; } } return result; } async cancel() { var _a; this.eof = true; try { await ((_a = this.reader) == null ? void 0 : _a.cancel()); } catch { } this.reader = null; } async pull() { if (!this.reader && this.openReader) this.reader = await this.openReader(); if (!this.reader) { this.eof = true; return; } const { value, done } = await this.reader.read(); if (done) { this.eof = true; return; } if (value == null ? void 0 : value.length) { const copy = new Uint8Array(value); this.chunks.push(copy); this.available += copy.length; } } } class CencAudioPlayer { constructor(audio2, options = {}) { __publicField(this, "audio"); __publicField(this, "options"); __publicField(this, "state", "idle"); __publicField(this, "session", 0); __publicField(this, "streamToken", 0); __publicField(this, "seekToken", 0); __publicField(this, "sessionController", null); __publicField(this, "streamController", null); __publicField(this, "sourceBuffer", null); __publicField(this, "mediaSource", null); __publicField(this, "objectUrl", null); __publicField(this, "sourceBufferChain", Promise.resolve()); __publicField(this, "context", null); __publicField(this, "info", null); __publicField(this, "autoplay", false); __publicField(this, "segmentSequence", 0); __publicField(this, "onSeeking", () => { const context = this.context; if (!context || this.state === "destroyed" || this.session === 0) return; if (this.audio.error) return; const target = this.audio.currentTime; if (!Number.isFinite(target) || this.isTimeBuffered(target)) return; const wasPlaying = !this.audio.paused; void this.restartAt(target, wasPlaying); }); __publicField(this, "onPlay", () => { if (this.context && this.state !== "destroyed") this.emitState("playing"); }); __publicField(this, "onPause", () => { if (this.context && this.state === "playing" && !this.audio.ended) this.emitState("paused"); }); __publicField(this, "onEnded", () => { if (this.context && this.state !== "destroyed") this.emitState("ended"); }); this.audio = audio2; const fetcher = (options.fetch ?? fetch$1).bind(unsafeWindow); this.options = { headBytes: validatePositive(options.headBytes, DEFAULT_HEAD_BYTES, "headBytes"), maxHeadBytes: validatePositive(options.maxHeadBytes, DEFAULT_MAX_HEAD_BYTES, "maxHeadBytes"), segmentSeconds: validatePositive(options.segmentSeconds, DEFAULT_SEGMENT_SECONDS, "segmentSeconds"), maxBufferAheadSeconds: validatePositive(options.maxBufferAheadSeconds, DEFAULT_MAX_BUFFER_AHEAD, "maxBufferAheadSeconds"), keepBehindSeconds: validatePositive(options.keepBehindSeconds, DEFAULT_KEEP_BEHIND, "keepBehindSeconds"), decryptConcurrency: Math.max(1, Math.floor(validatePositive(options.decryptConcurrency, DEFAULT_DECRYPT_CONCURRENCY, "decryptConcurrency"))), onStateChange: options.onStateChange, onProgress: options.onProgress, onMessage: options.onMessage, // config.fetch 保存的是页面 fetch 的原始引用,避免被字节 SDK 改写。 fetch: fetcher }; this.audio.addEventListener("seeking", this.onSeeking); this.audio.addEventListener("play", this.onPlay); this.audio.addEventListener("pause", this.onPause); this.audio.addEventListener("ended", this.onEnded); } get currentState() { return this.state; } get mediaInfo() { return this.info; } get element() { return this.audio; } /** 加载并开始后台缓冲;autoplay 只表示首次缓冲后尝试调用 audio.play。 */ async load(source, autoplay = false) { this.ensureAlive(); const session = this.beginSession(); this.autoplay = autoplay; this.emitState("loading"); let initialStream = null; try { const url = this.validateUrl(source.url); this.streamController = new AbortController(); initialStream = this.createNetworkStream(url, 0, this.streamController.signal); let { head, meta } = await this.parseHeadFromStream(initialStream, session); this.assertSession(session); const key = await importCencKey(source.key); this.assertSession(session); let counterStart = 0; if (meta.codec === "opus") { const probeEnd = meta.mdatStart + meta.offsets[Math.min(6, meta.sampleCount)]; if (head.length < probeEnd) { await initialStream.bufferAtLeast(probeEnd); head = initialStream.snapshot(); } counterStart = await this.detectCounterStart(meta, head, key); } this.assertSession(session); const sampleEntry = meta.codec === "aac" ? extractSampleEntry(head, meta.mdatStart) : meta.codec === "flac" ? extractFlacEntry(head, meta.mdatStart) : void 0; const mime = codecMime(meta.codec); const ctor = this.mediaSourceConstructor(); if (!ctor.isTypeSupported(mime)) throw new Error(`浏览器不支持 MSE 音频格式: ${mime}`); const mediaSource = new ctor(); this.mediaSource = mediaSource; this.objectUrl = unsafeWindow.URL.createObjectURL(mediaSource); const opened = this.waitForSourceOpen(mediaSource, session); this.audio.src = this.objectUrl; await opened; this.assertSession(session); this.sourceBuffer = mediaSource.addSourceBuffer(mime); this.segmentSequence = 0; const init2 = initSegment(meta, sampleEntry); this.emitMessage(`追加 init 段 ${init2.byteLength} 字节 (${mime})`, "info"); await this.enqueueSourceBuffer(session, (sourceBuffer) => { sourceBuffer.appendBuffer(this.toPageBuffer(init2)); }); this.assertSession(session); const ticks = new Float64Array(meta.sampleCount + 1); for (let i2 = 0; i2 < meta.sampleCount; i2++) { ticks[i2 + 1] = ticks[i2] + meta.durations[i2]; } const durationSeconds = meta.totalTicks / meta.timescale; const samplesPerSecond = meta.sampleCount / durationSeconds; const segmentSeconds = meta.codec === "opus" ? Math.min(this.options.segmentSeconds, MAX_WEBM_SEGMENT_SECONDS) : this.options.segmentSeconds; this.context = { url, head, meta, key, counterStart, ticks, samplesPerSecond, samplesPerSegment: Math.max(1, Math.round(samplesPerSecond * segmentSeconds)) }; this.info = { codec: meta.codec, sampleCount: meta.sampleCount, durationSeconds, timescale: meta.timescale, channels: meta.channels, encryptedBytes: meta.offsets[meta.sampleCount] }; await initialStream.skip(meta.mdatStart); this.assertSession(session); this.emitState("ready"); this.startStream(0, initialStream); initialStream = null; return this.info; } catch (error) { void (initialStream == null ? void 0 : initialStream.cancel()); if (this.isCurrent(session) && !isAbort(error)) { this.emitMessage(asError(error).message, "error"); void this.reportAppendFailure(); this.emitState("error"); } throw error; } } /** load(..., true) 的便捷形式。 */ start(source) { return this.load(source, true); } play() { this.ensureAlive(); return this.audio.play().then(() => void 0); } pause() { this.audio.pause(); } /** 停止当前流并释放 MSE URL,但保留播放器实例以便再次 load。 */ stop() { if (this.state === "destroyed") return; this.beginSession(); this.emitState("idle"); } destroy() { if (this.state === "destroyed") return; this.beginSession(); this.audio.removeEventListener("seeking", this.onSeeking); this.audio.removeEventListener("play", this.onPlay); this.audio.removeEventListener("pause", this.onPause); this.audio.removeEventListener("ended", this.onEnded); this.emitState("destroyed"); } ensureAlive() { if (this.state === "destroyed") throw new Error("播放器已销毁"); } beginSession() { var _a, _b; this.session++; this.streamToken++; this.seekToken++; (_a = this.sessionController) == null ? void 0 : _a.abort(); (_b = this.streamController) == null ? void 0 : _b.abort(); this.sessionController = new AbortController(); this.streamController = null; this.context = null; this.info = null; this.autoplay = false; this.sourceBuffer = null; this.mediaSource = null; this.sourceBufferChain = Promise.resolve(); this.audio.pause(); if (this.audio.src) { this.audio.removeAttribute("src"); this.audio.load(); } if (this.objectUrl) { unsafeWindow.URL.revokeObjectURL(this.objectUrl); this.objectUrl = null; } return this.session; } validateUrl(raw) { const url = raw.trim(); const parsed = new URL(url); if (parsed.protocol !== "https:" && parsed.protocol !== "http:") { throw new Error("音频 URL 必须是 HTTP(S) 地址"); } return parsed.toString(); } mediaSourceConstructor() { const win2 = unsafeWindow; const ctor = win2.MediaSource ?? globalThis.MediaSource; if (!ctor) throw new Error("当前浏览器没有 MediaSource 支持"); return ctor; } /** * 把字节搬到页面 realm,再交给 appendBuffer。 * * MediaSource 取自 unsafeWindow(页面 realm),而我们构造的 Uint8Array * 属于用户脚本沙箱 realm。跨 realm 的 ArrayBuffer 传进 appendBuffer 会被拒, * 表现是 SourceBuffer 立刻抛 error 事件、而