// ==UserScript== // @name 【改写】m3u8-downloader // @namespace https://github.com/jackhai9/userscripts // @icon data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E // @icon64 data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E // @version 0.10.35 // @description m3u8 下载增强脚本,仅在白名单视频站启用,避免误伤交易页等重前端应用 // @author jackhai9 // @include https://18jav.tv/* // @include https://*.18jav.tv/* // @include https://njav.com/* // @include https://*.njav.com/* // @include https://www.brookstradingcourse.com/* // @include https://brookstradingcourse.com/* // @include https://iframe.mediadelivery.net/* // @downloadURL https://raw.githubusercontent.com/jackhai9/userscripts/main/scripts/m3u8-downloader.user.js // @updateURL https://raw.githubusercontent.com/jackhai9/userscripts/main/scripts/m3u8-downloader.user.js // @grant none // @run-at document-start // ==/UserScript== (() => { var __getOwnPropNames = Object.getOwnPropertyNames; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __commonJS = (cb, mod) => function __require() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; // src/m3u8-downloader/constants.js var M3U8_MESSAGE_TYPE, BROOKS_MEDIA_INDEX_MESSAGE_TYPE, BROOKS_MEDIA_INDEX_STATE_KEY, BROOKS_MEDIA_EXPORT_SCHEMA_VERSION, BROOKS_MEDIA_EXPORT_TIMEOUT_MS, BROOKS_MEDIA_EXPORT_STEP_DELAY_MS, BROOKS_MEDIA_EXPORT_STATUS_INTERVAL_MS, EXTERNAL_DOWNLOADER_BLOCKED_HOST_SUFFIXES; var init_constants = __esm({ "src/m3u8-downloader/constants.js"() { M3U8_MESSAGE_TYPE = "jh-userscripts:m3u8-detected"; BROOKS_MEDIA_INDEX_MESSAGE_TYPE = "jh-userscripts:brooks-media-index-record"; BROOKS_MEDIA_INDEX_STATE_KEY = "jh-userscripts:brooks-media-index-export"; BROOKS_MEDIA_EXPORT_SCHEMA_VERSION = 2; BROOKS_MEDIA_EXPORT_TIMEOUT_MS = 45e3; BROOKS_MEDIA_EXPORT_STEP_DELAY_MS = 500; BROOKS_MEDIA_EXPORT_STATUS_INTERVAL_MS = 1e3; EXTERNAL_DOWNLOADER_BLOCKED_HOST_SUFFIXES = [ ".b-cdn.net", ".hshdkshd.com" ]; } }); // src/m3u8-downloader/media-url.js function buildExternalDownloaderUrl(sourceUrl) { return "https://blog.luckly-mjw.cn/tool-show/m3u8-downloader/index.html?source=" + sourceUrl; } function isExternalDownloaderBlocked(url) { try { const hostname = new URL(url).hostname; return EXTERNAL_DOWNLOADER_BLOCKED_HOST_SUFFIXES.some((suffix) => hostname.endsWith(suffix)); } catch (error) { return false; } } function shellQuote(value) { return "'" + value.replace(/'/g, "'\\''") + "'"; } function buildCaptionUrlFromM3u82(url, captionFile) { const sourceUrl = new URL(url); sourceUrl.searchParams.delete("title"); const pathParts = sourceUrl.pathname.split("/").filter(Boolean); const videoIndex = pathParts.findIndex((part) => part === "video.m3u8" || part === "playlist.m3u8"); if (videoIndex <= 0) { throw new Error("Unable to infer caption path from m3u8 URL"); } const baseIndex = pathParts[videoIndex] === "video.m3u8" && videoIndex > 0 && /^\d+x\d+$/.test(pathParts[videoIndex - 1]) ? videoIndex - 1 : videoIndex; sourceUrl.pathname = "/" + pathParts.slice(0, baseIndex).concat(["captions", captionFile]).join("/"); sourceUrl.hash = ""; return sourceUrl.href; } function getCleanMediaUrl(url) { const sourceUrl = new URL(url); sourceUrl.searchParams.delete("title"); return sourceUrl.href; } function getBrooksVideoIdFromM3u8(url) { const sourceUrl = new URL(url); const pathParts = sourceUrl.pathname.split("/").filter(Boolean); const videoIndex = pathParts.findIndex((part) => part === "video.m3u8" || part === "playlist.m3u8"); if (videoIndex <= 0) { return ""; } const baseIndex = pathParts[videoIndex] === "video.m3u8" && videoIndex > 0 && /^\d+x\d+$/.test(pathParts[videoIndex - 1]) ? videoIndex - 1 : videoIndex; return pathParts[baseIndex - 1] || ""; } function getYtDlpOutputName(title) { return title.replace(/\s*\|\s*Brooks Trading Course\s*$/i, "").replace(/[/:*?"<>|]/g, "_").trim() + ".%(ext)s"; } var init_media_url = __esm({ "src/m3u8-downloader/media-url.js"() { init_constants(); } }); // src/m3u8-downloader/brooks-pages.js function normalizeBrooksTitle(title) { return (title || "").replace(/\s*\|\s*Brooks Trading Course\s*$/i, "").replace(/^BTC PAF/i, "Video").trim(); } function isBrooksHost(hostname) { return hostname === "brookstradingcourse.com" || hostname.endsWith(".brookstradingcourse.com"); } function isBrooksCourseIndexPage(locationObj = location) { return isBrooksHost(locationObj.hostname) && locationObj.pathname.replace(/\/+$/, "") === "/main-course-videos"; } function isBrooksMediaPageUrl(url) { const path = url.pathname; return /\/video-\d+[a-z]?-[^/]+\/?$/i.test(path) || /^\/bonus-videos\/[^/]+\/?$/i.test(path); } function getBrooksCourseVideoLinks(root) { const seen = /* @__PURE__ */ new Set(); const baseHref = root.defaultView?.location?.href || location.href; return Array.from(root.querySelectorAll("a[href]")).map((link) => { try { const url = new URL(link.getAttribute("href"), baseHref); url.hash = ""; return url; } catch (error) { return null; } }).filter((url) => url && isBrooksHost(url.hostname) && isBrooksMediaPageUrl(url)).map((url) => url.href).filter((href) => { if (seen.has(href)) { return false; } seen.add(href); return true; }); } function extractBrooksMediaExportPageInfo(root, pageUrl) { const embed = Array.from(root.querySelectorAll('iframe[src*="iframe.mediadelivery.net/embed/"]'))[0]; const embedSrc = embed && embed.getAttribute("src"); if (!embedSrc) { throw new Error("Bunny embed iframe not found"); } const metaTitle = root.querySelector('meta[property="og:title"]'); const title = normalizeBrooksTitle(metaTitle && metaTitle.getAttribute("content") || root.title || ""); return { pageUrl, title, embedSrc: new URL(embedSrc, pageUrl).href }; } function buildBrooksMediaExportEmbedUrl(info) { const embedUrl = new URL(info.embedSrc); embedUrl.searchParams.set("jhBrooksPageUrl", info.pageUrl); embedUrl.searchParams.set("jhBrooksTitle", info.title || ""); return embedUrl.href; } function isSameBrooksVideoPage(left, right) { try { const leftUrl = new URL(left); const rightUrl = new URL(right); return leftUrl.origin === rightUrl.origin && leftUrl.pathname.replace(/\/+$/, "") === rightUrl.pathname.replace(/\/+$/, ""); } catch (error) { return false; } } var init_brooks_pages = __esm({ "src/m3u8-downloader/brooks-pages.js"() { } }); // src/m3u8-downloader/brooks-record.js function buildBrooksMediaIndexRecord(options) { const sourceUrl = new URL(options.m3u8Url); const mediaTitle = sourceUrl.searchParams.get("title") || options.title || ""; return { ok: true, url: options.pageUrl, title: options.title || mediaTitle, mediaTitle, pageUrl: options.pageUrl, output: getYtDlpOutputName(mediaTitle), referer: options.referer || "", m3u8: getCleanMediaUrl(options.m3u8Url), videoId: getBrooksVideoIdFromM3u8(options.m3u8Url), cn: buildCaptionUrlFromM3u82(options.m3u8Url, "CN.vtt"), en: buildCaptionUrlFromM3u82(options.m3u8Url, "EN.vtt"), index: options.index }; } var init_brooks_record = __esm({ "src/m3u8-downloader/brooks-record.js"() { init_media_url(); } }); // src/m3u8-downloader/brooks-status.js function getBrooksMediaExportPageLabel(url) { try { const parts = new URL(url).pathname.split("/").filter(Boolean); return truncateBrooksMediaExportText(parts[parts.length - 1] || url, 40); } catch (error) { return truncateBrooksMediaExportText(url || "", 40); } } function truncateBrooksMediaExportText(value, maxLength) { if (!value || value.length <= maxLength) { return value || ""; } return value.slice(0, Math.max(0, maxLength - 1)) + "…"; } function parseBrooksMediaExportTime(value) { if (typeof value === "number" && Number.isFinite(value)) { return value; } if (!value) { return null; } const timestamp = Date.parse(value); return Number.isFinite(timestamp) ? timestamp : null; } function getBrooksMediaExportElapsedMs(state, now) { if (!state) { return null; } const baseElapsedMs = typeof state.activeElapsedMs === "number" && Number.isFinite(state.activeElapsedMs) ? Math.max(0, state.activeElapsedMs) : null; const activeRunStartedAt = parseBrooksMediaExportTime(state.activeRunStartedAt); if (baseElapsedMs === null && activeRunStartedAt === null) { return null; } const fallbackNow = typeof now === "number" ? now : Date.now(); const activeRunMs = state.running && activeRunStartedAt !== null ? Math.max(0, fallbackNow - activeRunStartedAt) : 0; return (baseElapsedMs || 0) + activeRunMs; } function markBrooksMediaExportRunStarted(state, now) { if (!state) { return; } if (typeof state.activeElapsedMs !== "number" || !Number.isFinite(state.activeElapsedMs)) { state.activeElapsedMs = 0; } if (parseBrooksMediaExportTime(state.activeRunStartedAt) === null) { state.activeRunStartedAt = new Date(typeof now === "number" ? now : Date.now()).toISOString(); } } function stopBrooksMediaExportRunTimer(state, now) { if (!state) { return null; } const activeRunStartedAt = parseBrooksMediaExportTime(state.activeRunStartedAt); if (typeof state.activeElapsedMs !== "number" || !Number.isFinite(state.activeElapsedMs)) { state.activeElapsedMs = 0; } if (activeRunStartedAt !== null) { const endedAt = typeof now === "number" ? now : Date.now(); state.activeElapsedMs += Math.max(0, endedAt - activeRunStartedAt); delete state.activeRunStartedAt; } return state.activeElapsedMs; } function formatBrooksMediaExportDuration(milliseconds) { if (typeof milliseconds !== "number" || !Number.isFinite(milliseconds)) { return ""; } const totalSeconds = Math.max(0, Math.floor(milliseconds / 1e3)); const seconds = totalSeconds % 60; const totalMinutes = Math.floor(totalSeconds / 60); const minutes = totalMinutes % 60; const hours = Math.floor(totalMinutes / 60); if (hours) { return `${hours}h${String(minutes).padStart(2, "0")}m${String(seconds).padStart(2, "0")}s`; } if (minutes) { return `${minutes}m${String(seconds).padStart(2, "0")}s`; } return `${seconds}s`; } function formatBrooksMediaExportStatus(options) { const state = options && options.state; if (!state) { return ""; } const links = state.links || []; const records = state.records || []; const failures = state.failures || []; const total = links.length; const done = records.length + failures.length; const stateText = state.running ? "采集中" : state.stopped ? "已暂停" : "已完成"; const summaryParts = [ `${stateText} ${done}/${total}`, `成功 ${records.length}`, `失败 ${failures.length}` ]; const lines = [summaryParts.join(" | ")]; const elapsedText = formatBrooksMediaExportDuration(getBrooksMediaExportElapsedMs(state, options && options.now)); if (elapsedText) { lines.push(`耗时: ${elapsedText}`); } const pending = options && options.pending; if (pending && pending.url) { const currentIndex = typeof pending.index === "number" ? pending.index + 1 : (state.index || 0) + 1; const currentParts = [`当前 ${currentIndex}/${total} ${getBrooksMediaExportPageLabel(pending.url)}`]; if (pending.startedAt) { const elapsedSeconds = Math.max(0, Math.floor(((options.now || Date.now()) - pending.startedAt) / 1e3)); currentParts.push(`等待 ${elapsedSeconds}s`); } lines.push(currentParts.join(" | ")); } const lastFailure = failures[failures.length - 1]; if (lastFailure && lastFailure.error) { lines.push(`最近失败: ${lastFailure.error}`); if (!state.running && failures.length) { lines.push("请点“重试失败”;仍失败再导出清单 JSON"); } } return lines.join("\n"); } function getBrooksMediaExportPrimaryLabel(state) { if (state && state.running && !state.stopped) { return "暂停"; } if (state && state.stopped) { return "继续"; } return "开始"; } function isBrooksMediaExportComplete(state) { if (!state || !state.links || !state.links.length) { return false; } const records = state.records || []; const failures = state.failures || []; return records.length + failures.length >= state.links.length; } function canRetryFailedBrooksMediaExport(state) { return !!(state && !state.running && isBrooksMediaExportComplete(state) && state.failures && state.failures.length); } function shouldShowBrooksMediaExportReset(state) { if (!state || state.running) { return false; } if (state.stopped) { return true; } const links = Array.isArray(state.links) ? state.links : []; const records = Array.isArray(state.records) ? state.records : []; const failures = Array.isArray(state.failures) ? state.failures : []; if (failures.length) { return true; } if (!links.length) { return false; } return records.length + failures.length < links.length; } function buildBrooksMediaExportPayload(state, exportedAt) { const links = state && state.links ? state.links : []; const records = state && state.records ? state.records : []; const failures = state && state.failures ? state.failures : []; const elapsedMs = getBrooksMediaExportElapsedMs(state, parseBrooksMediaExportTime(exportedAt) || Date.now()); const completedIndexes = new Set(records.concat(failures).map((item) => item.index)); const missingIndexes = links.map((url, index) => index).filter((index) => !completedIndexes.has(index)); const done = records.length + failures.length; return { exportedAt, startedAt: state && state.startedAt ? state.startedAt : null, updatedAt: state && state.updatedAt ? state.updatedAt : null, elapsedMs, elapsedSeconds: elapsedMs === null ? null : Math.floor(elapsedMs / 1e3), elapsedText: formatBrooksMediaExportDuration(elapsedMs), total: links.length, done, completed: links.length > 0 && done >= links.length, nextIndex: state && typeof state.index === "number" ? state.index : 0, running: !!(state && state.running), stopped: !!(state && state.stopped), missingIndexes, records, failures }; } var init_brooks_status = __esm({ "src/m3u8-downloader/brooks-status.js"() { } }); // src/m3u8-downloader/brooks-exporter.js function buildBrooksMediaIndexExportFilename(exportedAt) { const date = new Date(exportedAt); if (Number.isNaN(date.getTime())) { throw new Error(`Invalid Brooks media export timestamp: ${exportedAt}`); } return `brooks-media-index-${date.toISOString().replace(/\.\d{3}Z$/, "Z").replace(/:/g, "")}.json`; } function createBrooksMediaExporter({ originXHR: originXHR2, downloadWithA: downloadWithA2, getTitle: getTitle2 }) { var brooksMediaExportState = null; var brooksMediaExportFrame = null; var brooksMediaExportPending = null; function notifyBrooksMediaIndexDetected(url, referer) { if (!isBrooksHost(location.hostname)) { return; } try { const record = buildBrooksMediaIndexRecord({ pageUrl: location.href, title: getTitle2(), referer, m3u8Url: url }); window.top.postMessage({ type: BROOKS_MEDIA_INDEX_MESSAGE_TYPE, record }, location.origin); } catch (error) { console.error("Unable to build Brooks media index record:", error); } } function recordBrooksMediaExportSuccess(record) { if (!brooksMediaExportState || !brooksMediaExportPending) { return; } if (!isSameBrooksVideoPage(record.pageUrl, brooksMediaExportPending.url)) { return; } brooksMediaExportState.records.push({ ...record, index: brooksMediaExportPending.index, url: brooksMediaExportPending.url, pageUrl: record.pageUrl }); advanceBrooksMediaExportQueue(brooksMediaExportPending.index); saveBrooksMediaExportState(); clearBrooksMediaExportFrame(); updateBrooksMediaExportStatus(); setTimeout(processNextBrooksMediaExport, BROOKS_MEDIA_EXPORT_STEP_DELAY_MS); } function isBrooksMediaExportFrameMessage(event, data) { return !!(brooksMediaExportPending && brooksMediaExportFrame && event.source === brooksMediaExportFrame.contentWindow && data && data.brooksExport && data.brooksExport.pageUrl && isSameBrooksVideoPage(data.brooksExport.pageUrl, brooksMediaExportPending.url)); } function handleBrooksDirectM3u8Message(event, data) { if (!isBrooksMediaExportFrameMessage(event, data)) { return false; } const record = buildBrooksMediaIndexRecord({ pageUrl: data.brooksExport.pageUrl, title: data.brooksExport.title || "", referer: data.referer || "", m3u8Url: data.url }); recordBrooksMediaExportSuccess(record); return true; } function saveBrooksMediaExportState() { if (!brooksMediaExportState) { return; } brooksMediaExportState.updatedAt = (/* @__PURE__ */ new Date()).toISOString(); localStorage.setItem(BROOKS_MEDIA_INDEX_STATE_KEY, JSON.stringify(brooksMediaExportState)); } function loadBrooksMediaExportState() { try { const raw = localStorage.getItem(BROOKS_MEDIA_INDEX_STATE_KEY); const state = raw ? JSON.parse(raw) : null; return state && state.schemaVersion === BROOKS_MEDIA_EXPORT_SCHEMA_VERSION ? state : null; } catch (error) { console.error("Unable to load Brooks media export state:", error); return null; } } function updateBrooksMediaExportControls(state) { const primaryButton = document.getElementById("brooks-media-export-primary"); if (primaryButton) { primaryButton.textContent = getBrooksMediaExportPrimaryLabel(state); } const retryFailedButton = document.getElementById("brooks-media-export-retry-failed"); if (retryFailedButton) { const canRetryFailures = canRetryFailedBrooksMediaExport(state); retryFailedButton.style.display = canRetryFailures ? "" : "none"; } const downloadButton = document.getElementById("brooks-media-export-download"); if (downloadButton) { downloadButton.style.display = state && state.running && !state.stopped ? "none" : ""; } const resetButton = document.getElementById("brooks-media-export-reset"); const resetHelp = document.getElementById("brooks-media-export-reset-help"); const showReset = shouldShowBrooksMediaExportReset(state); if (resetButton) { resetButton.style.display = showReset ? "" : "none"; resetButton.title = "清空当前进度和结果,不会自动开始"; } if (resetHelp) { resetHelp.style.display = showReset ? "" : "none"; } } function updateBrooksMediaExportStatus() { const statusEl = document.getElementById("brooks-media-export-status"); if (!statusEl) { return; } const state = brooksMediaExportState || loadBrooksMediaExportState(); if (!state) { statusEl.textContent = `发现 ${getBrooksCourseVideoLinks(document).length} 个课程视频`; updateBrooksMediaExportControls(null); return; } statusEl.textContent = formatBrooksMediaExportStatus({ state, pending: brooksMediaExportPending, now: Date.now() }); updateBrooksMediaExportControls(state); } function clearBrooksMediaExportFrame() { if (brooksMediaExportPending && brooksMediaExportPending.timeoutId) { clearTimeout(brooksMediaExportPending.timeoutId); } if (brooksMediaExportPending && brooksMediaExportPending.statusIntervalId) { clearInterval(brooksMediaExportPending.statusIntervalId); } brooksMediaExportPending = null; if (brooksMediaExportFrame && brooksMediaExportFrame.parentNode) { brooksMediaExportFrame.remove(); } brooksMediaExportFrame = null; } function recordBrooksMediaExportFailure(index, url, error) { if (!brooksMediaExportState) { return; } brooksMediaExportState.failures.push({ ok: false, index, url, error }); advanceBrooksMediaExportQueue(index); saveBrooksMediaExportState(); updateBrooksMediaExportStatus(); } function getNextBrooksMediaExportIndex(state) { if (state && state.retryQueue && state.retryQueue.length) { return state.retryQueue[0]; } return state && typeof state.index === "number" ? state.index : 0; } function advanceBrooksMediaExportQueue(index) { if (!brooksMediaExportState) { return; } if (brooksMediaExportState.retryQueue && brooksMediaExportState.retryQueue.length) { brooksMediaExportState.retryQueue = brooksMediaExportState.retryQueue.filter((itemIndex) => itemIndex !== index); if (!brooksMediaExportState.retryQueue.length) { delete brooksMediaExportState.retryQueue; } return; } brooksMediaExportState.index = index + 1; } function isCurrentBrooksMediaExportPending(index, url) { return !!(brooksMediaExportState && brooksMediaExportState.running && !brooksMediaExportState.stopped && brooksMediaExportPending && brooksMediaExportPending.index === index && isSameBrooksVideoPage(brooksMediaExportPending.url, url)); } function fetchBrooksMediaExportPageInfo(url, onSuccess, onFailure) { const xhr = new originXHR2(); xhr.open("GET", url, true); xhr.onload = function() { if (xhr.status < 200 || xhr.status >= 300) { onFailure(`page fetch failed: ${xhr.status}`); return; } try { const parser = new DOMParser(); const doc = parser.parseFromString(xhr.responseText || xhr.response || "", "text/html"); onSuccess(extractBrooksMediaExportPageInfo(doc, url)); } catch (error) { onFailure(error && error.message ? error.message : "page parse failed"); } }; xhr.onerror = function() { onFailure("page fetch network error"); }; xhr.send(); } function createBrooksMediaExportFrame(src) { brooksMediaExportFrame = document.createElement("iframe"); brooksMediaExportFrame.style.cssText = "position:fixed;right:20px;top:20px;width:640px;height:360px;opacity:.01;pointer-events:none;border:0;z-index:9998;background:white;"; brooksMediaExportFrame.setAttribute("aria-hidden", "true"); brooksMediaExportFrame.src = src; document.body.appendChild(brooksMediaExportFrame); } function processNextBrooksMediaExport() { if (!brooksMediaExportState || !brooksMediaExportState.running || brooksMediaExportState.stopped) { updateBrooksMediaExportStatus(); return; } const index = getNextBrooksMediaExportIndex(brooksMediaExportState); const url = brooksMediaExportState.links[index]; if (!url) { stopBrooksMediaExportRunTimer(brooksMediaExportState); brooksMediaExportState.running = false; saveBrooksMediaExportState(); clearBrooksMediaExportFrame(); updateBrooksMediaExportStatus(); return; } clearBrooksMediaExportFrame(); brooksMediaExportPending = { index, url, startedAt: Date.now(), timeoutId: setTimeout(() => { recordBrooksMediaExportFailure(index, url, "m3u8 detection timeout"); processNextBrooksMediaExport(); }, BROOKS_MEDIA_EXPORT_TIMEOUT_MS) }; brooksMediaExportPending.statusIntervalId = setInterval(updateBrooksMediaExportStatus, BROOKS_MEDIA_EXPORT_STATUS_INTERVAL_MS); updateBrooksMediaExportStatus(); fetchBrooksMediaExportPageInfo(url, (info) => { if (!isCurrentBrooksMediaExportPending(index, url)) { return; } createBrooksMediaExportFrame(buildBrooksMediaExportEmbedUrl(info)); }, (error) => { if (!isCurrentBrooksMediaExportPending(index, url)) { return; } recordBrooksMediaExportFailure(index, url, error); processNextBrooksMediaExport(); }); } function startBrooksMediaExport() { const links = getBrooksCourseVideoLinks(document); const now = Date.now(); const nowIso = new Date(now).toISOString(); brooksMediaExportState = { running: true, stopped: false, schemaVersion: BROOKS_MEDIA_EXPORT_SCHEMA_VERSION, links, index: 0, records: [], failures: [], startedAt: nowIso, updatedAt: nowIso, activeElapsedMs: 0, activeRunStartedAt: nowIso }; saveBrooksMediaExportState(); processNextBrooksMediaExport(); } function resumeBrooksMediaExport() { brooksMediaExportState = loadBrooksMediaExportState(); if (!brooksMediaExportState || !brooksMediaExportState.links || !brooksMediaExportState.links.length) { startBrooksMediaExport(); return; } brooksMediaExportState.running = true; brooksMediaExportState.stopped = false; markBrooksMediaExportRunStarted(brooksMediaExportState); saveBrooksMediaExportState(); processNextBrooksMediaExport(); } function pauseBrooksMediaExport() { if (!brooksMediaExportState) { brooksMediaExportState = loadBrooksMediaExportState(); } if (brooksMediaExportState) { stopBrooksMediaExportRunTimer(brooksMediaExportState); brooksMediaExportState.running = false; brooksMediaExportState.stopped = true; saveBrooksMediaExportState(); } clearBrooksMediaExportFrame(); updateBrooksMediaExportStatus(); } function toggleBrooksMediaExportPrimaryAction() { if (!brooksMediaExportState) { brooksMediaExportState = loadBrooksMediaExportState(); } if (brooksMediaExportState && brooksMediaExportState.running && !brooksMediaExportState.stopped) { pauseBrooksMediaExport(); return; } if (brooksMediaExportState && brooksMediaExportState.stopped) { resumeBrooksMediaExport(); return; } startBrooksMediaExport(); } function resetBrooksMediaExport() { clearBrooksMediaExportFrame(); brooksMediaExportState = null; localStorage.removeItem(BROOKS_MEDIA_INDEX_STATE_KEY); updateBrooksMediaExportStatus(); } function retryFailedBrooksMediaExport() { if (!brooksMediaExportState) { brooksMediaExportState = loadBrooksMediaExportState(); } if (!canRetryFailedBrooksMediaExport(brooksMediaExportState)) { return; } const retryIndexes = (brooksMediaExportState.failures || []).map((failure) => failure.index).filter((index) => typeof index === "number" && brooksMediaExportState.links[index]); if (!retryIndexes.length) { return; } clearBrooksMediaExportFrame(); brooksMediaExportState.retryQueue = [...new Set(retryIndexes)]; brooksMediaExportState.failures = []; brooksMediaExportState.running = true; brooksMediaExportState.stopped = false; markBrooksMediaExportRunStarted(brooksMediaExportState); saveBrooksMediaExportState(); processNextBrooksMediaExport(); } function exportBrooksMediaIndex() { const state = brooksMediaExportState || loadBrooksMediaExportState(); if (!state) { alert("没有可导出的 Brooks 视频与字幕清单"); return; } const exportedAt = (/* @__PURE__ */ new Date()).toISOString(); const payload = buildBrooksMediaExportPayload(state, exportedAt); const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); downloadWithA2(url, buildBrooksMediaIndexExportFilename(exportedAt)); URL.revokeObjectURL(url); } function handleBrooksMediaIndexMessage(event) { const data = event.data || {}; if (event.origin !== location.origin || data.type !== BROOKS_MEDIA_INDEX_MESSAGE_TYPE || !data.record) { return; } if (!brooksMediaExportState || !brooksMediaExportPending) { return; } if (!isSameBrooksVideoPage(data.record.pageUrl, brooksMediaExportPending.url)) { return; } const record = { ...data.record, index: brooksMediaExportPending.index, url: brooksMediaExportPending.url, pageUrl: data.record.pageUrl }; brooksMediaExportState.records.push(record); advanceBrooksMediaExportQueue(brooksMediaExportPending.index); saveBrooksMediaExportState(); clearBrooksMediaExportFrame(); updateBrooksMediaExportStatus(); setTimeout(processNextBrooksMediaExport, BROOKS_MEDIA_EXPORT_STEP_DELAY_MS); } function appendBrooksMediaExporterDom() { if (!isBrooksCourseIndexPage() || document.getElementById("brooks-media-export-dom") || !document.body) { return; } const links = getBrooksCourseVideoLinks(document); const section = document.createElement("section"); section.id = "brooks-media-export-dom"; section.style.cssText = "position:fixed;right:20px;bottom:88px;z-index:9999;width:440px;max-width:calc(100vw - 40px);box-sizing:border-box;padding:10px 12px;background:#1f2937;color:white;border:1px solid #d1d5db;border-radius:4px;font-size:13px;line-height:1.35;box-shadow:0 4px 12px rgba(0,0,0,.18);"; section.innerHTML = `