[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ncustom: [https://paypal.me/o2bmm]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug.yaml",
    "content": "name: Bug 报告 Bug Report\ndescription: 创建一个bug报告. File a bug report\nbody:\n  - type: input\n    id: version\n    attributes:\n      label: 扩展版本号 extension version\n      placeholder: e.g. vX.Y.Z\n  - type: dropdown\n    id: browser\n    attributes:\n      label: 浏览器 browser\n      options:\n        - Google Chrome\n        - Microsoft Edge\n        - Microsoft Edge (Android)\n        - Firefox\n        - Firefox (Android)\n        - Chromium\n        - 360浏览器\n        - 其他基于 Chromium 的浏览器 / Other Chromium-based browsers\n    validations:\n      required: true\n  - type: input\n    id: browserVersion\n    attributes:\n      label: 浏览器版本号 browser version\n      placeholder: e.g. vX.Y.Z\n  - type: input\n    id: url\n    attributes:\n      label: 涉及网址 related URL\n      placeholder: e.g. https://example.com\n      description: 请提供发生问题的网址 需要授权登陆才能播放的请通过邮箱提交bug\n  - type: checkboxes\n    id: checklist\n    attributes:\n      label: Checklist\n      options:\n        - label: 我已在 [issues](https://github.com/xifangczy/cat-catch/issues) 通过搜索, 未找到解决办法。 The issue observed is not already reported by searching on Github under [issues](https://github.com/xifangczy/cat-catch/issues)\n          required: true\n        - label: 我已查看 [FAQ](https://github.com/xifangczy/cat-catch/wiki/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98-FAQ) 未找到解决办法。 I've checked the [FAQ](https://github.com/xifangczy/cat-catch/wiki/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98-FAQ) but couldn't find a solution.\n          required: true\n  - type: textarea\n    id: description\n    attributes:\n      label: 请详细描述问题 What actually happened?\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/功能-添加-修改-增强-请求.md",
    "content": "---\nname: 功能 添加/修改/增强 请求\nabout: 请求一个功能修改或添加\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n** 详细描述想要添 加/修改/增强 的功能 **\n"
  },
  {
    "path": ".gitignore",
    "content": "# 构建产物\nbuild/\ndist/\n*.crx\n*.zip\n*.pem"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## 更新说明\n\n### 2.6.6\n\n[Updated] 感谢 @Oleada1 完善翻译\n\n[Updated] 增强 深度搜索 现在可以找到更多资源\n\n[Fixed] 修复 firefox无法视频录制问题\n\n[Fixed] 修复 录制视频 无法弹出下载问题\n\n[Fixed] 修复 深度搜索 导致网页无法正常使用问题\n\n[Fixed] 修复 屏蔽列表 失效问题\n\n[Fixed] 修复 edge Brave等浏览器 使用在线ffmpeg 文件为空问题\n\n### 2.6.5\n\n[Added] 部分网站不希望被本扩展抓取 添加 全局强制屏蔽 [屏蔽列表](https://o2bmm.gitbook.io/cat-catch/blockedsite)\n\n[Added] 增加土耳其语 感谢 @ilker-binzet\n\n[Added] 增加西班牙语 感谢 @Oleada1\n\n[Fixed] 部分浏览器 侧边栏无法使用问题\n\n[Fixed] 发送到 Aria2 User-Agent 传递错误\n\n[Fixed] 模板标签替换 双引号处理错误\n\n[Fixed] 导入配置时部分设置丢失问题\n\n[Fixed] 深度搜索导致部分网站无法正常使用问题\n\n### 2.6.4\n\n[Updated] webrtc 录制脚本 更新\n\n[Updated] 深度搜索脚本 更新\n\n[Updated] 更新日语 感谢@hmaoraze\n\n[Added] 支持 MQTT 协议 感谢@jetsung\n\n[Added] 筛选 删除重复文件名\n\n[Added] 始终打开 深度搜索 选项 (慎用)\n\n[Added] 弹出模式 可选择页面\n\n[Added] 筛选页面 支持时长排序\n\n### 2.6.3\n\n[Fixed] Chromium 114 版本以下缺少 `sidePanel` 功能，导致扩展无法使用\n\n### 2.6.2\n\n[Added] m3u8 解析器 录制失败重试功能 (测试)\n\n[Added] m3u8 解析器 尝试估算文件大小\n\n[Added] 增加 其他设置 `使用侧边栏` 选项。从 popup 模式改为浏览器侧边栏打开扩展 (不支持 firefox)\n\n[Updated] m3u8 预览现在支持 hevc/h265 编码\n\n[Updated] 深度搜索 支持解析 vimeo playlist.json\n\n[Changed] 重构 缓存捕捉 脚本 减少头部数据缺失问题\n\n[Changed] 重构 排除重复的资源 减少资源占用\n\n[Fixed] 缓存捕捉脚本导致视频无法播放问题\n\n[Deleted] m3u8 解析器 删除了旧版本下载器\n\n[Deleted] 启用新弹出页 删除旧弹出页\n\n### 2.6.1\n\n[Changed] 对手机浏览器进行一些适配\n\n### 2.6.0\n\n[Added] 全新的弹出页面(`弹出`按钮) 文件预览/筛选帮助你下载需要的文件 (设置`feat newPopup`关闭新版)\n\n[Changed] 增强数据发送功能，现在能自定义发送数据 感谢 @helson-lin 的支持\n\n[Changed] 正则匹配 现在能获取到请求头\n\n[Changed] 支持夸克浏览器 (部分功能不可用)\n\n[Updated] 深度搜索脚本 找到更多资源\n\n[Fixed] Fifefox 导入功能 bug 导致扩展不可用\n\n[Fixed] 偶尔会弹出多个 ffmpeg 页面的 bug\n\n[Fixed] 下载器 打开`边下边存` 无法自动关闭的 bug\n\n### 2.5.9\n\n[Added] 增加屏蔽网址功能 添加不希望开启扩展的网站 (可设为白名单, 只允许添加网址开启扩展)\n\n[Fixed] 新版下载器 下载大文件时 出错 #610\n\n[Changed] 限制每页面最大储存 9999 条资源\n\n[Changed] 设置增加导航栏\n\n[Changed] 自动下载 允许自定义保存文件名\n\n### 2.5.8\n\n[Changed] 如果资源 url 不存在文件名 尝试使用页面标题作为文件名\n\n### 2.5.7\n\n[Fixed] 自定义保存文件名使用 `/` 无法创建目录\n\n[Changed] firefox 升级 manifest v3\n\n[Changed] firefox 128 以上版本 支持使用深度搜索 缓存录制 等脚本功能\n\n[Fixed] firefox 无法发送到在线 ffmpeg 问题\n\n[Added] 重构 猫抓下载器 如需旧版本请在设置 关闭 `Test version` 选项\n\n[Added] `URL Protocol m3u8dl` `调用程序` 增加下载前确认参数设置\n\n[Added] m3u8 为疑似密钥增加验证密钥功能\n\n[Changed] 增强 深度搜索 现在能找到更多疑似密钥\n\n### 2.5.6\n\n[Fixed] m3u8 解析器 自动关闭 bug #531\n\n[Fixed] chrome 130 自定义 url 新规范导致 `m3u8dl://` 调用失败 #528\n\n[Fixed] m3u8 解析器 文件不正确无法解析 造成死循环占用 CPU 问题\n\n[Changed] 猫抓下载器 添加更多请求头 增加下载成功率\n\n### 2.5.5\n\n[Fixed] 修复一个严重 bug #483\n\n[Added] 在线 ffmpeg 提供服务器选择\n\n[Fixed] m3u8 解析器 文件名存在`|`字符 无法下载问题\n\n[Changed] 发送数据 提供完整请求头\n\n### 2.5.4\n\n[Added] m3u8DL 增加切换 RE 版本 (RE 版 需[URLProtocol](https://github.com/xifangczy/URLProtocol))\n\n[Added] 录制相关脚本 增加码率设置\n\n[Fixed] 深度搜索 脚本错误导致无法使用\n\n[Fixed] m3u8 解析器录制直播 录制时间显示错误\n\n### 2.5.3\n\n[Added] 增加`弹出`模式 (以新窗口打开资源列表页面)\n\n[Added] 增加`调用本地程序`设置, 程序没有调用协议, 可以使用[URLProtocol](https://github.com/xifangczy/URLProtocol)帮助程序注册调用协议。具体使用方法查看 [调用本地协议](https://o2bmm.gitbook.io/cat-catch/docs/invoke)\n\n[Added] 下载器 增加`边下边存`选项 可以用来下载一些直播视频链接\n\n[Added] 现在使用`深度搜索` 或其他脚本得到的疑似密钥, 直接显示在 popup 页面 `疑似密钥` 标签内。\n\n[Added] 增加 葡萄牙语\n\n[Changed] 重写 `录制webRTC` 脚本\n\n[Changed] `m3u8解析器` `下载器`页面内更改设置不会被储存。所有设置更改统一到扩展设置页面。\n\n[Changed] storage.local 更改为 storage.session 以减少 IO 错误导致扩展无法使用.(要求 chrome 104 以上)\n\n[Changed] 优化与 ffmpeg 网页端的通信, 避免多任务时的数据错乱。\n(请提前打开 [在线 ffmpeg](https://ffmpeg.bmmmd.com/) ctrl+f5 刷新页面 避免页面缓存造成的问题)\n\n[Changed] 稍微增大一些按钮图标 不再训练大家的鼠标精准度 🙄...如果你不喜欢想还原 设置-自定义 css 填入 `body{font-size:12px;width:550px;}.icon,.favicon{width:18px;height:18px;}.DownCheck{width:15px;height:15px;}`\n\n### 2.5.2\n\n[Added] 添加测试功能 数据发送 嗅探数据和密钥发送到指定地址\n\n[Added] 替换标签 增加 `${origin}`\n\n[Added] 显示 图标数字角标 开关\n\n[Fixed] 猫抓下载器 小部分网站需要指定 range\n\n[Fixed] 修复 标题作为文件名 文件名含有非法字符问题 #339\n\n### 2.5.1\n\n[Added] 多语言 增加繁体中文\n\n[Fixed] 修复 深度搜索 死循环 bug\n\n[Fixed] 兼容低版本 chromium 缺少 API 导致扩展无法使用\n\n[Changed] popup 页面 现在能合并两个 m3u8 文件\n\n### 2.5.0\n\n[Added] 多语言支持\n\n[Changed] m3u8 解析 新下载器 性能优化\n\n[Fixed] 视频捕捉 不使用`从头捕获`也会丢掉头部数据的问题\n\n[Changed] 深度搜索 现在能找到更多密钥\n\n### 2.4.9\n\n[Fixed] `$url$` 标签 修复(自动更新成`${url}`) #281\n\n[Fixed] 修复 加密 m3u8 存在 EXT-X-MAP 标签，解密会失败的 bug\n\n[Added] 设置页面 添加自动合并 m3u8 选项 #286 (测试)\n\n[Added] 增加录制 webRTC 流脚本 更多功能-录制 webRTC (测试)\n\n### 2.4.8\n\n[Fixed] 修复 m3u8 新下载器 ${referer} 标签问题 #272\n\n[Fixed] 修复 m3u8 新下载器 全部重新下载 bug #274\n\n[Fixed] 修复 m3u8 新下载器 下载失败丢失线程 #276\n\n[Fixed] 修复 m3u8 新下载器 勾选 ffmpeg 转码 下载超过 2G 大小 不会强制下载\n\n[Changed] 完善 Aria2 Rpc 协议 增加密钥 和 cookie 支持\n\n[Added] 增加${cookie}标签 如果资源存在 cookie\n\n### 2.4.7\n\n[Fixed] 缓存捕获 延迟获取标题 #241\n\n[Fixed] 特殊字符造成无法下载的问题 #253\n\n[Fixed] m3u8 解析器 没有解析出全部嵌套 m3u8 的 bug #265\n\n[Added] firefox 增加 privacy 协议页面 第一次安装显示\n\n[Added] 增加 Aria2 Rpc 协议下载 感谢 @aar0u\n\n[Changed] 重写录制脚本\n\n[Changed] 增强深度搜索\n\n[Changed] m3u8 解析器 现在可以自定义头属性\n\n[Changed] m3u8 解析器 最大下载线程调整为 6\n\n[Changed] m3u8 解析器 默认开启新下载器\n\n### 2.4.6\n\n[Fixed] 缓存捕获 多个视频问题 #239\n\n[Changed] 更新 mux m3u8-decrypt mpd-parser 版本\n\n[Changed] 设置 刷新跳转清空当前标签抓取的数据 现在可以调节模式\n\n[Changed] firefox 版本要求 113+\n\n[test] m3u8 解析器 增加测试项 `重构的下载器`\n\n### 2.4.5\n\n[Changed] 增强 深度搜索 解决\"一次性\"m3u8\n\n[Changed] m3u8 解析器 下载范围允许填写时间格式 HH:MM:SS\n\n[Added] 增加 缓存捕获 从头捕获、正则提取文件名、手动填写文件名\n\n[Added] 增加 设置 正则匹配 屏蔽资源功能\n\n[Added] 增加 下载器 后台打开页面设置\n\n[deleted] 删除 \"幽灵资源\" 设定 不确定来源的资源归于当前标签\n\n[Fixed] 修复 缓存捕获 清理缓存\n\n[Fixed] 修复 正则匹配 有时会匹配失效(lastIndex 没有复位)\n\n[Fixed] 修复 媒体控制 有时检测不到媒体\n\n[Fixed] 修复 重置所有设置 丢失配置\n\n[Fixed] 修复 firefox 兼容问题\n\n### 2.4.4\n\n[Changed] 增强 深度搜索\n\n[Fixed] m3u8 解析器 无限触发错误的 bug\n\n### 2.4.3\n\n[Fixed] 修复 缓存捕获 获取文件名为空\n\n[Changed] 增强 深度搜索 可以搜到更多密钥\n\n[Changed] 增强 注入脚本 现在会注入到所有 iframe\n\n[Changed] 删除 youtube 支持 可以使用缓存捕捉\n\n### 2.4.2\n\n[Added] 设置页面增加 排除重复的资源 选项\n\n[Added] popup 增加暂停抓取按钮\n\n[Changed] 超过 500 条资源 popup 可以中断加载\n\n[Changed] 调整默认配置 默认不启用 ts 文件 删除多余正则\n\n[Changed] 正则匹配的性能优化\n\n[Fixed] 修复 m3u8 解析器录制功能 直播结束导致自动刷新页面丢失已下载数据的问题\n\n[Fixed] 修复 m3u8 解析器边下边存和 mp4 转码一起使用 编码不正确的 bug\n\n[Fixed] 修复 扩展重新启动后 造成的死循环\n\n### 2.4.1\n\n[Added] 捕获脚本 现在可以通过表达式获取文件名\n\n[Changed] 删除 打开自动下载的烦人提示\n\n[Changed] 优化 firefox 下 资源严重占用问题\n\n[Fixed] 猫抓下载器 不再限制 2G 文件大小 #179\n\n### 2.4.0\n\n[Added] 加入自定义 css\n\n[Added] 音频 视频 一键合并\n\n[Added] popup 页面正则筛选\n\n[Added] 自定义快捷键支持\n\n[Added] popup 页面支持正则筛选\n\n[Added] m3u8 碎片文件自定义参数\n\n[Changed] 筛选 现在能隐藏不要的数据 而不是取消勾选\n\n[Changed] 重写优化 popup 大部分代码\n\n[Changed] 重写初始化部分代码\n\n[Changed] m3u8 解析器 默认设置改为 ffmpeg 转码 而不是 mp4 转码\n\n[Changed] 删除 调试模式\n\n[Fixed] 深度搜索 深度判断的 bug\n\n[Fixed] 很多 bug\n\n### 2.3.3\n\n[Changed] 解析器 m3u8DL 默认不载入设置参数 #149\n\n[Changed] 可以同时打开多个捕获脚本\n\n[Changed] popup 页面 css 细节调整 #156\n\n[Fixed] 清空不会删除角标的 bug\n\n[Fixed] 替换标签中 参数内包含 \"|\" 字符处理不正确的 bug\n\n### 2.3.2\n\n[Changed] 设置 增加自定义文件名 删除标题正则提取\n\n[Added] 支持深色模式 #134\n\n[Added] popup 增加筛选\n\n[Fixed] 修复非加密的 m3u8 无法自定义密钥下载\n\n[Fixed] mp4 转码删除 创建媒体日期 属性 #142\n\n### 2.3.1\n\n[Added] 新的替换标签\n\n[Changed] 边下边存 支持 mp4 转码\n\n[Fixed] 修复 BUG #123 #117 #114 #124\n\n### 2.3.0\n\n[Added] m3u8 解析器 边下边存\n\n[Added] m3u8 解析器 在线 ffmpeg 转码\n\n[Fixed] 特殊文件名 下载所选无法下载\n\n[Fixed] m3u8 解析器 某些情况无法下载文件\n\n[Fixed] Header 属性提取失败\n\n[Fixed] 添加抓取类型出错 #109\n\n[Changed] 修改 标题修剪 默认配置\n\n### 2.2.9\n\n[Fixed] 修复 m3u8DL 调用命令范围参数 --downloadRange 不正确\n\n[Added] 正则修剪标题 [#90](https://github.com/xifangczy/cat-catch/issues/94)\n\n[Added] 下载前选择保存目录 选项\n\n[Fixed] m3u8 解析器 部分情况无法下载 ts 文件\n\n[Changed] `复制所选`按钮 现在能被 `复制选项`设置影响\n\n### 2.2.8\n\n[Changed] m3u8 解析器现在会记忆你设定的参数\n\n[Changed] 幽灵数据 更改为 其他页面(幽灵数据同样归类其他页面)\n\n[Changed] popup 页面的性能优化\n\n[Changed] 增加 始终不启用下载器 选项\n\n[Fixed] 修复 使用第三方下载器猫抓下载器也会被调用\n\n### 2.2.7\n\n[Fixed] 修正 文件大小显示不正确\n\n[Changed] 性能优化\n\n[Fixed] 修复 没有正确清理冗余数据 导致 CPU 占用问题\n\n### 2.2.6\n\n[Added] 深度搜索 尝试收集 m3u8 文件的密钥 具体使用查看 [用户文档](https://o2bmm.gitbook.io/cat-catch/docs/m3u8parse#maybekey)\n\n[Added] popup 资源详情增加二维码按钮\n\n[Added] m3u8 解析器 自定义文件名 只要音频 另存为 m3u8DL 命令完善 部分代码来自 [#80](https://github.com/xifangczy/cat-catch/pull/80)\n\n[Added] 非 Chrome 扩展商店版本 现在支持 Youtube\n\n[Added] Firefox 版 现在支持 m3u8 视频预览\n\n[Fixed] m3u8 解析器 超长名字无法保存文件 [#80](https://github.com/xifangczy/cat-catch/pull/80)\n\n[Fixed] 修正 媒体控制 某些情况检测不到视频\n\n### 2.2.5\n\n[Fixed] 修复 mpd 解析器丢失音轨 [#70](https://github.com/xifangczy/cat-catch/issues/70)\n\n[Changed] 优化在网络状况不佳下的直播 m3u8 录制\n\n[Changed] 更新 深度搜索 search.js 进一步增加分析能力\n\n[Changed] 减少 mp4 转码时内存占用\n\n[Changed] 自定义调用本地播放器的协议\n\n### 2.2.4\n\n[Changed] 更新 hls.js\n\n[Changed] m3u8 文件现在能显示更多媒体信息\n\n[Added] 增加 Dash mpd 文件解析\n\n[Added] 增加 深度搜索 脚本\n\n[Fixed] 修复 捕获按钮偶尔失效\n\n### 2.2.3\n\n[Added] m3u8 解析器增加录制直播\n\n[Added] m3u8 解析器增加处理 EXT-X-MAP 标签\n\n[Added] 新增捕获脚本 recorder2.js 需要 Chromium 104 以上版本\n\n[Added] 增加选项 刷新、跳转到新页面 清空当前标签抓取的数据\n\n[Fixed] 修正 m3u8 解析器使用 mp4 转码生成的文件，媒体时长信息不正确\n\n### 2.2.2\n\n[Changed] m3u8 解析器使用 hls.js 替代，多项改进，自定义功能添加\n\n[Changed] 分离下载器和 m3u8 解析器\n\n[Fixed] 修复 m3u8 解析器`调用N_m3u8DL-CLI下载`按钮失效\n\n[Fixed] 修复幽灵数据随机丢失问题\n\n[Fixed] 修复 m3u8 解析器 key 下载器在某些时候无法下载的问题\n\n### 2.2.1\n\n[Fixed] 修复浏览器字体过大，按钮遮挡资源列表的问题。\n\n[Fixed] 调整关键词替换\n\n[Fixed] 修复 Firefox download API 无法下载 data URL 问题\n\n[Changed] m3u8 解析器多个 KEY 显示问题\n\n[Changed] 视频控制现在可以控制其他页面的视频\n\n[Changed] 视频控制现在可以对视频截图\n\n[Changed] 自定义复制选项增加 其他文件 选项\n\n[Added] m3u8 解析器现在可以转换成 mp4 格式\n\n### 2.2.0\n\n[Fixed] 修复文件名出现 \"~\" 符号 导致 chrome API 无法下载\n\n[Fixed] 修复 Firefox 中 popup 页面下载按钮被滚动条遮挡\n\n[Fixed] 储存路劲有中文时 m3u8dl 协议调用错误\n\n[Changed] 增加/删除一些默认配置\n\n[Added] 增加操控当前网页视频功能\n\n[Added] 增加自定义复制选项\n\n### 2.1.2\n\n[Changed] 细节调整\n\n### 2.1.1\n\n[Changed] 调整正则匹配 现在能提取多个网址\n\n[Fixed] 修复选择脚本在 m3u8 解析器里不起作用 并提高安全性\n\n[Fixed] m3u8 解析器在 Firefox 中不能正常播放 m3u8 视频\n\n[Fixed] 修复 Firefox 中手机端模拟无法还原的问题\n\n[Fixed] 修复初始化错误 BUG 导致扩展失效\n\n### 2.1.0\n\n[Changed] 新增 referer 获取 不存在再使用 initiator 或者直接使用 url\n\n[Changed] 重新支持 Firefox 需要 93 版本以上\n\n[Changed] chromium 内核的浏览器最低要求降为 93 小部分功能需要 102 版本以上，低版本会隐藏功能按钮\n\n[Fixed] 部分 m3u8 key 文件解析错误问题\n\n[Fixed] 修复 保存文件名使用网页标题 选项在 m3u8 解析器里不起作用\n\n### 2.0.0\n\n[Changed] 模拟手机端，现在会修改 navigator.userAgent 变量\n\n[Added] 视频捕获功能，解决被动嗅探无法下载视频的问题\n\n[Added] 视频录制功能，解决被动嗅探无法下载视频的问题\n\n[Added] 支持 N_m3u8DL-CLI 的 m3u8dl://协议\n\n[Added] m3u8 解析器增强，现在能在线合并下载 m3u8 文件\n\n[Added] popup 页面无法下载的视频，会交给 m3u8 解析器修改 Referer 下载\n\n[Added] popup 页面和 m3u8 页面可以在线预览 m3u8\n\n[Added] json 查看工具，和 m3u8 解析器一样在 popup 页面显示图标进入\n\n[Fixed] 无数 BUG\n\n[Fixed] 解决 1.0.17 以来会丢失数据的问题\n\n[Fixed] 该死的 Service Worker... 现在后台被杀死能立刻唤醒自己... 继续用肮脏的手段对抗 Manifest V3\n\n### 1.0.26\n\n[Fixed] 解决关闭网页不能正确删除当前页面储存的数据问题\n\n### 1.0.25\n\n[Changed] 正则匹配增强\n\n[Changed] Heart Beat\n\n[Added] 手机端模拟，手机环境下有更多资源可以被下载。\n\n[Added] 自动下载\n\n### 1.0.24\n\n[Added] 导入/导出配置\n\n[Added] Heart Beat 解决 Service Worker 休眠问题\n\n[Added] firefox.js 兼容层 并上架 Firefox\n\n### 1.0.23\n\n[Added] 正则匹配\n\n### 1.0.22\n\n[Fixed] 一个严重 BUG，导致 Service Worker 无法使用 \\*\n\n### 1.0.21\n\n[Added] 自定义抓取类型\n\n[Refactor] 设置页面新界面\n\n### 1.0.20\n\n[Added] 抓取 image/\\*类型文件选项\n\n### 1.0.19\n\n[Fixed] 重构导致的许多 BUG \\*\n\n### 1.0.18\n\n[Added] 抓取 application/octet-stream 选项\n\n[Refactor] 重构剩余代码\n\n### 1.0.17\n\n[Refactor] Manifest 更新到 V3 部分代码\n\n[Added] 使用 PotPlayer 预览媒体\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\"> [中文] | [<a href=\"README_en.md\">English</a>] | [<a href=\"README_es.md\">Español</a>]</p>\n\n# 📑简介\n猫抓(cat-catch) 资源嗅探扩展，能够帮你筛选列出当前页面的资源。\n\n# 📖安装地址\n## 🐴Chrome\nhttps://chrome.google.com/webstore/detail/jfedfbgedapdagkghmgibemcoggfppbb\n## 🦄Edge\nhttps://microsoftedge.microsoft.com/addons/detail/oohmdefbjalncfplafanlagojlakmjci\n## 🦊Firefox\nhttps://addons.mozilla.org/addon/cat-catch/ 😂需非国区IP访问\n## 📱Edge Android\n<img src=\"https://raw.githubusercontent.com/xifangczy/cat-catch/master/README/edgeqrcode.png\" width=\"20%\" />\n\n💔猫抓是开源的，任何人都可以下载修改上架到应用商店，已经有不少加上广告代码后上架的伪猫抓，请注意自己的数据安全。所有安装地址以github和用户文档为准。\n\n# 📒用户文档\nhttps://o2bmm.gitbook.io/cat-catch/\n\n# 🌏翻译\n[![gitlocalized ](https://gitlocalize.com/repo/9392/whole_project/badge.svg)](https://gitlocalize.com/repo/9392?utm_source=badge)\n\n# 📘安装方法\n## 应用商店安装\n通过安装地址的链接到官方扩展商店即可安装。\n## 源码安装\n1. Git Clone 代码。\n2. 扩展管理页面 打开 \"开发者模式\"。\n3. 点击 \"加载已解压的扩展程序\" 选中扩展文件夹即可。\n## crx安装\n1. [Releases](https://github.com/xifangczy/cat-catch/releases) **右键另存为**下载crx文件。\n2. 扩展管理页面 打开 \"开发者模式\"。\n3. 将crx文件拖入扩展程序页面即可。\n\n# 📚兼容性说明\n1.0.17版本之后需要Chromium内核版本93以上。\n低于93请使用1.0.16版本。\n要体验完整功能，请使用104版本以上。\n\n# 🔍界面\n![popup界面](https://raw.githubusercontent.com/xifangczy/cat-catch/master/README/popup.png)\n![m3u8解析器界面](https://raw.githubusercontent.com/xifangczy/cat-catch/master/README/m3u8.png)\n\n# 🤚🏻免责\n本扩展仅供下载用户拥有版权或已获授权的视频，禁止用于下载受版权保护且未经授权的内容。用户需自行承担使用本工具的全部法律责任，开发者不对用户的任何行为负责。本工具按“原样”提供，开发者不承担任何直接或间接责任。\n\n# 🚫版权保护与拒绝抓取声明\n我们尊重所有网站的内容版权和运营方的合法权益。\n如果您不允许本工具运行在您的网站上，请遵循以下流程向我们提交请求，我们将会把您的域名加入本项目的“避免抓取列表”中。\n- 在本仓库创建一个新的 Issue\n- Issue 标题请使用格式： `[Opt-Out Request] 您的网站域名`\n\n在 Issue 正文中，请提供以下信息以便我们核实：\n- 网站域名：（例如：`example.com`）\n- 联系人邮箱：（用于必要时核实身份）\n\n我们承诺在收到有效请求后，将在后续版本更新中尊重您的意愿。请注意，本项目是一个开源项目，更新和发布需要一定的周期。感谢您的理解与合作。\n\n# 🔒隐私政策\n本扩展收集所有信息都在本地储存处理，不会发送到远程服务器，不包含任何跟踪器。\n\n# 💖鸣谢\n- [hls.js](https://github.com/video-dev/hls.js)\n- [jQuery](https://github.com/jquery/jquery)\n- [mux.js](https://github.com/videojs/mux.js)\n- [js-base64](https://github.com/dankogai/js-base64)\n- [jquery.json-viewer](https://github.com/abodelot/jquery.json-viewer)\n- [Momo707577045](https://github.com/Momo707577045)\n- [mpd-parser](https://github.com/videojs/mpd-parser)\n- [StreamSaver.js](https://github.com/jimmywarting/StreamSaver.js)\n\n# 📜License\nGPL-3.0 license\n\n1.0版 使用 MIT许可\n\n2.0版 更改为GPL v3许可\n\n为了资源嗅探扩展有良好发展，希望使用猫抓源码的扩展仍然保持开源。\n"
  },
  {
    "path": "README_en.md",
    "content": "<p align=\"center\"> [<a href=\"README.md\">中文</a>] | [English] | [<a href=\"README_es.md\">Español</a>]</p>\n\n# 📑Introduction\nCat-Catch is a resource sniffing extension that can help you filter and list the resources of the current page.\n\n# 📖Installation\n## 🐴Chrome\nhttps://chrome.google.com/webstore/detail/jfedfbgedapdagkghmgibemcoggfppbb\n## 🦄Edge\nhttps://microsoftedge.microsoft.com/addons/detail/oohmdefbjalncfplafanlagojlakmjci\n## 🦊Firefox\nhttps://addons.mozilla.org/addon/cat-catch/ 😂Non-China IP required for access\n## 📱Edge Android\n<img src=\"https://raw.githubusercontent.com/xifangczy/cat-catch/master/README/edgeqrcode.png\" width=\"20%\" />\n\n💔Cat-Catch is open source, anyone can download, modify, and list it in the app store. There are already quite a few fake Cat-Catch extensions listed with added ad codes, please pay attention to your data security. All installation URLs are subject to github and user documentation.\n\n# 📒Documentation\nhttps://o2bmm.gitbook.io/cat-catch/\n\n# 🌏Translations\n[![gitlocalized ](https://gitlocalize.com/repo/9392/whole_project/badge.svg)](https://gitlocalize.com/repo/9392?utm_source=badge)\n\n# 📘 Installation Methods\n## App Store Installation\nInstall directly from the official extension store using the link provided.\n## Source Code Installation\n1. Git clone the repository.\n2. Open the extensions management page and enable \"Developer Mode.\"\n3. Click \"Load unpacked\" and select the extension folder.\n## CRX Installation\n1. **Right-click** and save the CRX file from [Releases](https://github.com/xifangczy/cat-catch/releases).\n2. Open the extensions management page and enable \"Developer Mode.\"\n3. Drag the CRX file into the extensions page.\n\n# 📚Compatibility\nAfter version 1.0.17, Chromium kernel version 93 or above is required.\nUse version 1.0.16 if below 93.\nFor full functionality, use version 104 or above.\n\n# 🔍Screenshot\n![popup Screenshot](https://raw.githubusercontent.com/xifangczy/cat-catch/master/README/popup.png)\n![m3u8 parser Screenshot](https://raw.githubusercontent.com/xifangczy/cat-catch/master/README/m3u8.png)\n\n# 🤚🏻Disclaimer\nThis extension is intended for downloading videos that you own or have authorized access to. It is prohibited to use this Tool for downloading copyrighted content without permission. Users are solely responsible for their actions, and the developer is not liable for any user behavior. This Tool is provided \"as-is,\" and the developer assumes no direct or indirect liability.\n\n# 🚫 Copyright Protection and Opt-Out Policy\nWe respect the intellectual property rights and legitimate interests of all websites and their operators.\nIf you do not permit this tool to operate on your website, please follow the procedure below to submit a request. We will add your domain to the project's \"Opt-Out List.\"\n- Create a new **Issue** in this repository.\n- **Please use the following title format:** `[Opt-Out Request] YourDomain.com`\n\nIn the Issue description, please provide the following information for verification:\n- **Website Domain:** (e.g., `example.com`)\n- **Contact Email:** (For identity verification when necessary).\n\nWe commit to honoring your request and will add verified domains to the Opt-Out list in subsequent version updates.\nPlease note that this is an open-source project, and updates and releases require a certain cycle. Thank you for your understanding and cooperation.\n\n\n\n# 🔒Privacy Policy\nThe extension collects and processes all information locally without sending it to remote servers and does not include any trackers.\n\n# 💖Acknowledgements\n- [hls.js](https://github.com/video-dev/hls.js)\n- [jQuery](https://github.com/jquery/jquery)\n- [mux.js](https://github.com/videojs/mux.js)\n- [js-base64](https://github.com/dankogai/js-base64)\n- [jquery.json-viewer](https://github.com/abodelot/jquery.json-viewer)\n- [Momo707577045](https://github.com/Momo707577045)\n- [mpd-parser](https://github.com/videojs/mpd-parser)\n- [StreamSaver.js](https://github.com/jimmywarting/StreamSaver.js)\n\n# 📜License\nGPL-3.0 license\n\nVersion 1.0 uses the MIT license.\n\nVersion 2.0 has changed to the GPL v3 license.\n\nIn order for the resource sniffing extension to develop well, it is hoped that extensions using the Cat-Catch source code will continue to be open source.\n"
  },
  {
    "path": "README_es.md",
    "content": "<p align=\"center\"> [<a href=\"README.md\">中文</a>] | [<a href=\"README_en.md\">English</a>] | [Español]</p>\n\n# 📑Introducción\nCat-Catch es una extensión de rastreo de recursos que puede ayudarlo a filtrar y enumerar los recursos de la página actual.\n\n# 📖Instalación\n## 🐴Chrome\nhttps://chrome.google.com/webstore/detail/jfedfbgedapdagkghmgibemcoggfppbb\n## 🦄Edge\nhttps://microsoftedge.microsoft.com/addons/detail/oohmdefbjalncfplafanlagojlakmjci\n## 🦊Firefox\nhttps://addons.mozilla.org/addon/cat-catch/ 😂Se requiere IP que no sea china para acceder\n## 📱Edge Android\n<img src=\"https://raw.githubusercontent.com/xifangczy/cat-catch/master/README/edgeqrcode.png\" width=\"20%\" />\n\n💔Cat-Catch es de código abierto, cualquiera puede descargarlo, modificarlo y incluirlo en la tienda de aplicaciones. Ya hay bastantes extensiones falsas de Cat-Catch listadas con códigos publicitarios agregados; preste atención a la seguridad de sus datos. Todas las URL de instalación están sujetas a github y a la documentación del usuario.\n\n# 📒Documentación\nhttps://o2bmm.gitbook.io/cat-catch/\n\n# 🌏Traducciones\n[![gitlocalized ](https://gitlocalize.com/repo/9392/whole_project/badge.svg)](https://gitlocalize.com/repo/9392?utm_source=badge)\n\n# 📘 Métodos de instalación\n## Instalación de la tienda de aplicaciones\nInstálelo directamente desde la tienda de extensiones oficial utilizando el enlace proporcionado.\n## Instalación código fuente\n1. Clonar el repositorio Git.\n2. Abra la página de gestión de extensiones y active \"Developer Mode.\"\n3. Clic en \"Load unpacked\" y seleccione la carpeta de extensión.\n## Instalación CRX\n1. **Clic derecho** y guarde el archivo CRX desde [Releases](https://github.com/xifangczy/cat-catch/releases).\n2. Abra la página de gestión de extensiones y active \"Developer Mode.\"\n3. Arrastre el archivo CRX a la página de extensiones.\n\n# 📚Compatibilidad\nDespués de la versión 1.0.17, se requiere el kernel Chromium versión 93 o superior.\nUtilice la versión 1.0.16 si es inferior a 93.\nPara una funcionalidad completa, utilice la versión 104 o superior.\n\n# 🔍Captura de pantalla\n![popup Screenshot](https://raw.githubusercontent.com/xifangczy/cat-catch/master/README/popup_es.png)\n![m3u8 parser Screenshot](https://raw.githubusercontent.com/xifangczy/cat-catch/master/README/m3u8_es.png)\n\n# 🤚🏻Descargo de responsabilidad\nEsta extensión está pensada para descargar vídeos de tu propiedad o a los que tengas acceso autorizado. Está prohibido utilizar esta herramienta para descargar contenidos protegidos por derechos de autor sin autorización. Los usuarios son los únicos responsables de sus acciones, y el desarrollador no es responsable de ningún comportamiento de los usuarios. Esta herramienta se proporciona \"tal cual\" y el promotor no asume ninguna responsabilidad directa o indirecta.\n\n# 🚫 Protección de los derechos de autor y política de exclusión voluntaria\nRespetamos los derechos de propiedad intelectual y los intereses legítimos de todos los sitios web y sus operadores.\nSi no permite que esta herramienta funcione en su sitio web, siga el procedimiento que se indica a continuación para presentar una solicitud. Añadiremos su dominio al proyecto de \"Opt-Out List.\"\n- Crear un nuevo **Issue** en este repositorio.\n- **Utilice el siguiente formato de título:** `[Opt-Out Request] SuDominio.com`\n\nEn la descripción de la incidencia, facilite la siguiente información para su verificación:\n- **Dominio del sitio web:** (ej., `ejemplo.com`)\n- **Contacto Email:** (Para verificar la identidad cuando sea necesario).\n\nNos comprometemos a atender su solicitud y añadiremos los dominios verificados al \"Opt-Out list\" en posteriores actualizaciones de la versión.\nTenga en cuenta que se trata de un proyecto de código abierto, y las actualizaciones y versiones requieren un cierto ciclo. Gracias por su comprensión y cooperación.\n\n\n\n# 🔒Política de privacidad\nLa extensión recopila y procesa toda la información localmente sin enviarla a servidores remotos y no incluye ningún rastreador.\n\n# 💖Agradecimientos\n- [hls.js](https://github.com/video-dev/hls.js)\n- [jQuery](https://github.com/jquery/jquery)\n- [mux.js](https://github.com/videojs/mux.js)\n- [js-base64](https://github.com/dankogai/js-base64)\n- [jquery.json-viewer](https://github.com/abodelot/jquery.json-viewer)\n- [Momo707577045](https://github.com/Momo707577045)\n- [mpd-parser](https://github.com/videojs/mpd-parser)\n- [StreamSaver.js](https://github.com/jimmywarting/StreamSaver.js)\n\n# 📜Licencia\nLicencia GPL-3.0\n\nLa versión 1.0 utiliza la licencia MIT.\n\nLa versión 2.0 ha cambiado a la licencia GPL v3.\n\nPara que la extensión de rastreo de recursos se desarrolle bien, se espera que las extensiones que utilicen el código fuente de Cat-Catch sigan siendo de código abierto.\n"
  },
  {
    "path": "_locales/en/messages.json",
    "content": "{\n  \"catCatch\": {\n    \"message\": \"cat-catch\"\n  },\n  \"description\": {\n    \"message\": \"Web media sniffing tool\"\n  },\n  \"confirm\": {\n    \"message\": \"Confirm\"\n  },\n  \"currentPage\": {\n    \"message\": \"Current Page\"\n  },\n  \"otherPage\": {\n    \"message\": \"Other Page\"\n  },\n  \"otherFeatures\": {\n    \"message\": \"Other Features\"\n  },\n  \"mediaControl\": {\n    \"message\": \"Media Control\"\n  },\n  \"loadingData\": {\n    \"message\": \"Loading Data...\"\n  },\n  \"selectWebpage\": {\n    \"message\": \"Webpage:\"\n  },\n  \"selectMedia\": {\n    \"message\": \"Media:\"\n  },\n  \"noMediaDetected\": {\n    \"message\": \"No Media Detected on Webpage\"\n  },\n  \"noControllableMediaDetected\": {\n    \"message\": \"No Controllable Media Detected\"\n  },\n  \"multiplier\": {\n    \"message\": \"Multiplier:\"\n  },\n  \"speedPlayback\": {\n    \"message\": \"Speed Playback\"\n  },\n  \"play\": {\n    \"message\": \"Play\"\n  },\n  \"normalPlay\": {\n    \"message\": \"Normal Playback\"\n  },\n  \"pictureInPicture\": {\n    \"message\": \"Picture in Picture\"\n  },\n  \"fullscreen\": {\n    \"message\": \"Fullscreen\"\n  },\n  \"screenshot\": {\n    \"message\": \"Screenshot\"\n  },\n  \"loop\": {\n    \"message\": \"Loop\"\n  },\n  \"mute\": {\n    \"message\": \"Mute\"\n  },\n  \"volume\": {\n    \"message\": \"Volume\"\n  },\n  \"functionEntry\": {\n    \"message\": \"Function Entry\"\n  },\n  \"downloader\": {\n    \"message\": \"Downloader\"\n  },\n  \"parser\": {\n    \"message\": \"Parser\"\n  },\n  \"m3u8Parser\": {\n    \"message\": \"M3U8 Parser\"\n  },\n  \"mpdParser\": {\n    \"message\": \"MPD Parser\"\n  },\n  \"jsonFormatter\": {\n    \"message\": \"JSON Formatter\"\n  },\n  \"expandAll\": {\n    \"message\": \"Expand All\"\n  },\n  \"expandPlayable\": {\n    \"message\": \"Expand Playable\"\n  },\n  \"expandSelected\": {\n    \"message\": \"Expand Selected\"\n  },\n  \"collapseAll\": {\n    \"message\": \"Collapse All\"\n  },\n  \"videoRecording\": {\n    \"message\": \"Video Recording\"\n  },\n  \"closeRecording\": {\n    \"message\": \"Close Recording\"\n  },\n  \"recordWebRTC\": {\n    \"message\": \"Record WebRTC\"\n  },\n  \"screenCapture\": {\n    \"message\": \"Screen Capture\"\n  },\n  \"simulateMobile\": {\n    \"message\": \"Simulate Mobile\"\n  },\n  \"autoDownload\": {\n    \"message\": \"Auto Download\"\n  },\n  \"onlineMerge\": {\n    \"message\": \"Merge\"\n  },\n  \"download\": {\n    \"message\": \"Download\"\n  },\n  \"copy\": {\n    \"message\": \"Copy\"\n  },\n  \"selectAll\": {\n    \"message\": \"Select All\"\n  },\n  \"invertSelection\": {\n    \"message\": \"Toggle\"\n  },\n  \"filter\": {\n    \"message\": \"Filter\"\n  },\n  \"clear\": {\n    \"message\": \"Clear\"\n  },\n  \"deepSearch\": {\n    \"message\": \"Search\"\n  },\n  \"closeSearch\": {\n    \"message\": \"Close Search\"\n  },\n  \"cacheCapture\": {\n    \"message\": \"Capture\"\n  },\n  \"closeCapture\": {\n    \"message\": \"Close Capture\"\n  },\n  \"moreFeatures\": {\n    \"message\": \"More\"\n  },\n  \"pause\": {\n    \"message\": \"Pause\"\n  },\n  \"settings\": {\n    \"message\": \"Settings\"\n  },\n  \"closeSimulation\": {\n    \"message\": \"Close Simulation\"\n  },\n  \"closeDownload\": {\n    \"message\": \"Close Download\"\n  },\n  \"enable\": {\n    \"message\": \"Enable\"\n  },\n  \"disable\": {\n    \"message\": \"Disable\"\n  },\n  \"noData\": {\n    \"message\": \"No Fish\"\n  },\n  \"regularFilterPlaceholder\": {\n    \"message\": \"Regular expression filter, match resource URL, press Enter to confirm\"\n  },\n  \"option\": {\n    \"message\": \"Option\"\n  },\n  \"titleOption\": {\n    \"message\": \"cat-catch Option\"\n  },\n  \"titleDownload\": {\n    \"message\": \"cat-catch Download\"\n  },\n  \"titleM3U8\": {\n    \"message\": \"cat-catch m3u8 Parser\"\n  },\n  \"titleJson\": {\n    \"message\": \"cat-catch json formatter\"\n  },\n  \"titledash\": {\n    \"message\": \"cat-catch Dash Parser\"\n  },\n  \"suffix\": {\n    \"message\": \"Suffix\"\n  },\n  \"suffixTip\": {\n    \"message\": \"Fill in the suffix that does not contain '.', if no size filtering is needed, fill in 0.\"\n  },\n  \"extensionName\": {\n    \"message\": \"Extension Name\"\n  },\n  \"filterSize\": {\n    \"message\": \"Filter Size\"\n  },\n  \"delete\": {\n    \"message\": \"Delete\"\n  },\n  \"addSuffix\": {\n    \"message\": \"Add Suffix\"\n  },\n  \"extension\": {\n    \"message\": \"Extension\"\n  },\n  \"disableAll\": {\n    \"message\": \"Disable All\"\n  },\n  \"enableAll\": {\n    \"message\": \"Enable All\"\n  },\n  \"type\": {\n    \"message\": \"Type\"\n  },\n  \"addType\": {\n    \"message\": \"Add Type\"\n  },\n  \"typeTip\": {\n    \"message\": \"Enter the correct content-type, if no size filtering is needed, fill in 0.\"\n  },\n  \"addTypeError\": {\n    \"message\": \"The format of the capture type is incorrect, please check\"\n  },\n  \"regexMatch\": {\n    \"message\": \"Regex Match\"\n  },\n  \"blockResource\": {\n    \"message\": \"Block Resource\"\n  },\n  \"alert\": {\n    \"message\": \"Alert\"\n  },\n  \"regexExpression\": {\n    \"message\": \"Regex Expression\"\n  },\n  \"addRegex\": {\n    \"message\": \"Add Regex\"\n  },\n  \"regexTest\": {\n    \"message\": \"Regex test\"\n  },\n  \"regex\": {\n    \"message\": \"regex\"\n  },\n  \"flag\": {\n    \"message\": \"Flag\"\n  },\n  \"result\": {\n    \"message\": \"Result\"\n  },\n  \"match\": {\n    \"message\": \"Match\"\n  },\n  \"noMatch\": {\n    \"message\": \"No Match\"\n  },\n  \"blockResourceTip\": {\n    \"message\": \"Block the resources you do not want to appear\"\n  },\n  \"flagTip\": {\n    \"message\": \"i: ignore case, g: global search. It can also be left blank\"\n  },\n  \"regexSuffixTip\": {\n    \"message\": \"Assign a suffix to the obtained URL. It can be left blank, and the suffix will be automatically truncated (many files do not have a suffix)\"\n  },\n  \"regexTip\": {\n    \"message\": \"Regular expressions consume a lot of resources, use them carefully if not necessary\"\n  },\n  \"copyTip\": {\n    \"message\": \"For the convenience of using third-party applications, customize the content written to the clipboard by the copy button\"\n  },\n  \"replaceKeywordList\": {\n    \"message\": \"Replace the Keyword List\"\n  },\n  \"otherFiles\": {\n    \"message\": \"Other Files\"\n  },\n  \"resetCopySettings\": {\n    \"message\": \"Reset Copy Settings\"\n  },\n  \"autoSetRefererCookieParams\": {\n    \"message\": \"Automatically Set Referer and Cookie Parameters\"\n  },\n  \"secretKey\": {\n    \"message\": \"Secret Key\"\n  },\n  \"address\": {\n    \"message\": \"Address\"\n  },\n  \"documentation\": {\n    \"message\": \"Documentation\"\n  },\n  \"aria2Tip\": {\n    \"message\": \"An excellent download tool, see how to use\"\n  },\n  \"m3u8DLTips\": {\n    \"message\": \"An excellent third-party m3u8 and mpd download tool, see how to use\"\n  },\n  \"invoke\": {\n    \"message\": \"Invoke\"\n  },\n  \"parameter\": {\n    \"message\": \"Parameter\"\n  },\n  \"parameterSetting\": {\n    \"message\": \"Parameter Setting\"\n  },\n  \"test\": {\n    \"message\": \"Test\"\n  },\n  \"replaceTags\": {\n    \"message\": \"Replace Tags\"\n  },\n  \"customSaveFileName\": {\n    \"message\": \"Custom Save File Name\"\n  },\n  \"userAgentTip\": {\n    \"message\": \"Default to the current browser's User Agent\"\n  },\n  \"alwaysDisableCatCatcher\": {\n    \"message\": \"Always Disable Cat-Catch Downloader\"\n  },\n  \"autoClosePageAfterDownload\": {\n    \"message\": \"Automatically Close Page After Download\"\n  },\n  \"openDownloaderPageInBackground\": {\n    \"message\": \"Open Downloader Page in Background\"\n  },\n  \"downloaderTip\": {\n    \"message\": \"If the resource download fails, automatically enable the downloader to try again.\"\n  },\n  \"autoDownM3u8Tip\": {\n    \"message\": \"Click the download button and use the m3u8 parser to start merging and downloading immediately\"\n  },\n  \"otherSettings\": {\n    \"message\": \"Other Settings\"\n  },\n  \"resetOtherSettings\": {\n    \"message\": \"Reset Other Settings\"\n  },\n  \"previewMode\": {\n    \"message\": \"Use the local player's call protocol to open video previews\"\n  },\n  \"previewModePlaceholder\": {\n    \"message\": \"Leave it blank to disable it. The default is to use the popup page to preview the video\"\n  },\n  \"preview\": {\n    \"message\": \"Preview\"\n  },\n  \"customFilenameOption\": {\n    \"message\": \"Use a custom filename to save the file (the default is the webpage title)\"\n  },\n  \"saveAsOption\": {\n    \"message\": \"Choose the save directory after downloading\"\n  },\n  \"iconOption\": {\n    \"message\": \"Display the website icon\"\n  },\n  \"clearOption\": {\n    \"message\": \"Refresh, navigate to a new page, and clear the data captured by the current tab\"\n  },\n  \"doNotClear\": {\n    \"message\": \"Do Not Clear\"\n  },\n  \"normalClear\": {\n    \"message\": \"Normal Clear\"\n  },\n  \"moreFrequent\": {\n    \"message\": \"More Frequent\"\n  },\n  \"dopreview\": {\n    \"message\": \"preview\"\n  },\n  \"dopopup\": {\n    \"message\": \"popup\"\n  },\n  \"winpreview\": {\n    \"message\": \"window preview\"\n  },\n  \"winpopup\": {\n    \"message\": \"window popup\"\n  },\n  \"excludeDuplicateResources\": {\n    \"message\": \"Exclude duplicate resources (too many resources will consume a lot of CPU)\"\n  },\n  \"customCSS\": {\n    \"message\": \"Custom CSS\"\n  },\n  \"MQTT\": {\n    \"message\": \"MQTT\"\n  },\n  \"mqttBroker\": {\n    \"message\": \"Broker address\"\n  },\n  \"mqttPath\": {\n    \"message\": \"Path\"\n  },\n  \"mqttProtocol\": {\n    \"message\": \"Protocol\"\n  },\n  \"mqttClientId\": {\n    \"message\": \"Client ID\"\n  },\n  \"mqttTitleLength\": {\n    \"message\": \"Title Max Length\"\n  },\n  \"mqttUsername\": {\n    \"message\": \"Username\"\n  },\n  \"mqttPassword\": {\n    \"message\": \"Password\"\n  },\n  \"mqttTopic\": {\n    \"message\": \"Topic\"\n  },\n  \"mqttQos\": {\n    \"message\": \"QoS Level\"\n  },\n  \"mqttQos0\": {\n    \"message\": \"(At most once)\"\n  },\n  \"mqttQos1\": {\n    \"message\": \"(At least once)\"\n  },\n  \"mqttQos2\": {\n    \"message\": \"(Exactly once)\"\n  },\n  \"mqttDataFormat\": {\n    \"message\": \"Data Format\"\n  },\n  \"mqttDataFormatHelp\": {\n    \"message\": \"{\\\"url\\\": \\\"$${url}\\\", \\\"title\\\": \\\"$${title}\\\", \\\"type\\\": \\\"$${type}\\\", \\\"ext\\\": \\\"$${ext}\\\", \\\"timestamp\\\": \\\"$${timestamp}\\\"}\"\n  },\n  \"mqttDataFormatVars\": {\n    \"message\": \"Available variables\"\n  },\n  \"mqttDataFormatDefault\": {\n    \"message\": \"Leave blank to use default JSON format\"\n  },\n  \"mqttProtocolWss\": {\n    \"message\": \"WSS (Secure)\"\n  },\n  \"mqttProtocolWs\": {\n    \"message\": \"WS (Insecure)\"\n  },\n  \"mqttTitleLengthHelp\": {\n    \"message\": \"Maximum length of the title to send in MQTT messages\"\n  },\n  \"mqttBrokerHelp\": {\n    \"message\": \"Hostname or IP address of the MQTT broker\"\n  },\n  \"mqttPathHelp\": {\n    \"message\": \"WebSocket path (usually /mqtt or /ws)\"\n  },\n  \"mqttClientIdHelp\": {\n    \"message\": \"Unique client identifier for this connection\"\n  },\n  \"mqttTopicHelp\": {\n    \"message\": \"MQTT topic to publish messages to\"\n  },\n  \"mqttQosHelp\": {\n    \"message\": \"Quality of Service level (0=At most once, 1=At least once, 2=Exactly once)\"\n  },\n  \"mqttCredentialsHelp\": {\n    \"message\": \"Leave username/password empty if not required\"\n  },\n  \"operation\": {\n    \"message\": \"Operation\"\n  },\n  \"exportSettings\": {\n    \"message\": \"Export Settings\"\n  },\n  \"importConfiguration\": {\n    \"message\": \"Import Configuration\"\n  },\n  \"clearCapturedData\": {\n    \"message\": \"Clear Captured Data\"\n  },\n  \"resetSettings\": {\n    \"message\": \"Reset Settings\"\n  },\n  \"resetAllSettings\": {\n    \"message\": \"Reset All Settings\"\n  },\n  \"restartExtension\": {\n    \"message\": \"Restart Extension\"\n  },\n  \"about\": {\n    \"message\": \"About\"\n  },\n  \"confirmReset\": {\n    \"message\": \"Are you sure you want to reset?\"\n  },\n  \"invokeProtocolTemplate\": {\n    \"message\": \"Invoke Protocol Template\"\n  },\n  \"customVLCProtocol\": {\n    \"message\": \"Custom VLC Protocol\"\n  },\n  \"systemShare\": {\n    \"message\": \"System Share\"\n  },\n  \"default\": {\n    \"message\": \"Default\"\n  },\n  \"goBack\": {\n    \"message\": \"Go Back\"\n  },\n  \"openDir\": {\n    \"message\": \"Open Directory\"\n  },\n  \"downloadDir\": {\n    \"message\": \"Download Directory\"\n  },\n  \"sendFfmpeg\": {\n    \"message\": \"Send to Online ffmpeg\"\n  },\n  \"autoCloserDownload\": {\n    \"message\": \"Automatically Close Page After Download\"\n  },\n  \"openInBgDownload\": {\n    \"message\": \"Open Downloader Page in Background\"\n  },\n  \"m3u8Placeholder\": {\n    \"message\": \"Please enter m3u8 link / m3u8 content / segment list / $${range} tag\"\n  },\n  \"m3u8Url\": {\n    \"message\": \"m3u8 URL\"\n  },\n  \"nextLevel\": {\n    \"message\": \"Next Level\"\n  },\n  \"nextLevelTip\": {\n    \"message\": \"This M3U8 file nests multiple M3U8 files.\"\n  },\n  \"multipleAudios\": {\n    \"message\": \"Multiple Audios\"\n  },\n  \"multipleAudiosTip\": {\n    \"message\": \"This M3U8 file nests multiple audios\"\n  },\n  \"multipleSubtitles\": {\n    \"message\": \"Multiple Subtitles\"\n  },\n  \"multipleSubtitlesTip\": {\n    \"message\": \"This M3U8 file nests multiple subtitles.\"\n  },\n  \"possibleKey\": {\n    \"message\": \"Found possible keys\"\n  },\n  \"loading\": {\n    \"message\": \"Loading...\"\n  },\n  \"waitDownload\": {\n    \"message\": \"Waiting for download...\"\n  },\n  \"downloadSegmentList\": {\n    \"message\": \"Download list\"\n  },\n  \"originalM3u8\": {\n    \"message\": \"Original M3U8\"\n  },\n  \"localM3u8\": {\n    \"message\": \"Local M3U8\"\n  },\n  \"segmentList\": {\n    \"message\": \"Segment\"\n  },\n  \"downloadProgress\": {\n    \"message\": \"Download Progress\"\n  },\n  \"getParameters\": {\n    \"message\": \"GET Parameters\"\n  },\n  \"restoreGetParameters\": {\n    \"message\": \"Restore GET Parameters\"\n  },\n  \"requestHeaders\": {\n    \"message\": \"Request Headers\"\n  },\n  \"setRequestHeaders\": {\n    \"message\": \"Set request headers.\"\n  },\n  \"invokeM3u8DL\": {\n    \"message\": \"Invoke M3U8DL\"\n  },\n  \"copyCommand\": {\n    \"message\": \"Copy Command\"\n  },\n  \"previewCommand\": {\n    \"message\": \"Preview Command\"\n  },\n  \"addSettingParameters\": {\n    \"message\": \"Add Setting Parameters\"\n  },\n  \"customKeyPlaceholder\": {\n    \"message\": \"Customize the key in hexadecimal or base64, or the key address\"\n  },\n  \"uploadKey\": {\n    \"message\": \"Upload key\"\n  },\n  \"downloadThreads\": {\n    \"message\": \"Threads\"\n  },\n  \"ffmpegTranscoding\": {\n    \"message\": \"FFmpeg transcod\"\n  },\n  \"mp4Format\": {\n    \"message\": \"MP4\"\n  },\n  \"downloadWhileSaving\": {\n    \"message\": \"Stream download\"\n  },\n  \"audioOnly\": {\n    \"message\": \"Audio Only\"\n  },\n  \"saveAs\": {\n    \"message\": \"Save As\"\n  },\n  \"skipDecryption\": {\n    \"message\": \"Skip Decryption\"\n  },\n  \"newDownloader\": {\n    \"message\": \"New Downloader\"\n  },\n  \"downloadRange\": {\n    \"message\": \"Download Range\"\n  },\n  \"recordLive\": {\n    \"message\": \"Record\"\n  },\n  \"mergeDownloads\": {\n    \"message\": \"Merge Downloads\"\n  },\n  \"redownloadFailedItems\": {\n    \"message\": \"Redownload Failed Items\"\n  },\n  \"downloadExistingData\": {\n    \"message\": \"Download Existing Data\"\n  },\n  \"stopDownload\": {\n    \"message\": \"Stop Download\"\n  },\n  \"start\": {\n    \"message\": \"Start\"\n  },\n  \"end\": {\n    \"message\": \"End\"\n  },\n  \"resolution\": {\n    \"message\": \"Resolution\"\n  },\n  \"duration\": {\n    \"message\": \"Duration\"\n  },\n  \"bitrate\": {\n    \"message\": \"Bitrate\"\n  },\n  \"ADTSerror\": {\n    \"message\": \"Cannot find the ADTS header. It may be an AES-128-ECB encrypted resource, which is not currently supported for decryption. Please use third-party merging software.\"\n  },\n  \"m3u8Error\": {\n    \"message\": \"There are errors in parsing or playing the M3U8 file, check the console for detailed error information\"\n  },\n  \"noAudio\": {\n    \"message\": \"No Audio\"\n  },\n  \"noVideo\": {\n    \"message\": \"No Video\"\n  },\n  \"hevcTip\": {\n    \"message\": \"HEVC/H.265 encoded fragment files are only supported for online ffmpeg transcoding\"\n  },\n  \"hevcPreviewTip\": {\n    \"message\": \"HEVC/H.265 encoded fragment files are not supported for preview.\"\n  },\n  \"m3u8Info\": {\n    \"message\": \"A total of $num$ file, with a total duration of $time$.\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      },\n      \"time\": {\n        \"content\": \"$2\"\n      }\n    }\n  },\n  \"encryptedHLS\": {\n    \"message\": \"Encrypted HLS\"\n  },\n  \"encryptedSAMPLE\": {\n    \"message\": \"Resources encrypted with SAMPLE-AES-CTR cannot be handled at the moment.\"\n  },\n  \"liveHLS\": {\n    \"message\": \"Live HLS\"\n  },\n  \"keyAddress\": {\n    \"message\": \"Key Address\"\n  },\n  \"key\": {\n    \"message\": \"Key\"\n  },\n  \"encryptionAlgorithm\": {\n    \"message\": \"Method\"\n  },\n  \"keyDownloadFailed\": {\n    \"message\": \"Key Download Failed\"\n  },\n  \"savePrompt\": {\n    \"message\": \"Saved to disk, please check the downloaded content in the browser.\"\n  },\n  \"close\": {\n    \"message\": \"Close\"\n  },\n  \"blobM3u8DLError\": {\n    \"message\": \"Blob URLs cannot invoke M3U8DL for download\"\n  },\n  \"M3U8DLparameterLong\": {\n    \"message\": \"The M3U8DL parameter is too long.\"\n  },\n  \"runningCannotChangeSettings\": {\n    \"message\": \"Running, Cannot Change Settings\"\n  },\n  \"streamSaverTip\": {\n    \"message\": \"The function of 'download while saving' does not support ffmpeg online format conversion, does not support re-downloading erroneous slices, and does not support 'save as'.\"\n  },\n  \"stopRecording\": {\n    \"message\": \"Stop Recording\"\n  },\n  \"waitingForLiveData\": {\n    \"message\": \"Waiting for Live Data\"\n  },\n  \"sNumError\": {\n    \"message\": \"Serial Number Error\"\n  },\n  \"startGTend\": {\n    \"message\": \"Start Number Cannot Be Greater Than End Number\"\n  },\n  \"sNumMax\": {\n    \"message\": \"Serial Number Cannot Exceed $num$\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"incorrectKey\": {\n    \"message\": \"Incorrect Key\"\n  },\n  \"addParameters\": {\n    \"message\": \"Add Parameters\"\n  },\n  \"decryptionError\": {\n    \"message\": \"Decryption Error\"\n  },\n  \"downloadFailed\": {\n    \"message\": \"Download Failed\"\n  },\n  \"retryDownload\": {\n    \"message\": \"Retry Download\"\n  },\n  \"recordingDuration\": {\n    \"message\": \"Recording Duration\"\n  },\n  \"downloaded\": {\n    \"message\": \"Downloaded\"\n  },\n  \"downloadedVideoLength\": {\n    \"message\": \"Downloaded Video Length\"\n  },\n  \"downloadComplete\": {\n    \"message\": \"Download Complete\"\n  },\n  \"retryingDownload\": {\n    \"message\": \"Retrying Download\"\n  },\n  \"merging\": {\n    \"message\": \"Merging\"\n  },\n  \"fileTooLarge\": {\n    \"message\": \"File Too Large, File larger than $size$\",\n    \"placeholders\": {\n      \"size\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"fileTooLargeStream\": {\n    \"message\": \"File larger than $size$, enable stream download?\",\n    \"placeholders\": {\n      \"size\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"formatConversionError\": {\n    \"message\": \"Format Conversion Error\"\n  },\n  \"streamOnbeforeunload\": {\n    \"message\": \"Streaming is in progress, the download will stop after closing\"\n  },\n  \"fileLoading\": {\n    \"message\": \"File Loading\"\n  },\n  \"expandAllNodes\": {\n    \"message\": \"Expand all JSON nodes\"\n  },\n  \"collapseAllNodes\": {\n    \"message\": \"Collapse all JSON nodes\"\n  },\n  \"fileRetrievalFailed\": {\n    \"message\": \"File Retrieval Failed\"\n  },\n  \"selectVideo\": {\n    \"message\": \"Select Video\"\n  },\n  \"extractSlices\": {\n    \"message\": \"Extract Slices\"\n  },\n  \"convertToM3U8\": {\n    \"message\": \"Convert to M3U8 Parsing\"\n  },\n  \"selectAudio\": {\n    \"message\": \"Select Audio\"\n  },\n  \"audio\": {\n    \"message\": \"Audio\"\n  },\n  \"video\": {\n    \"message\": \"Video\"\n  },\n  \"DRMerror\": {\n    \"message\": \"The media has DRM protection, please use third-party tools for download\"\n  },\n  \"regexTitle\": {\n    \"message\": \"Regular expression match or from deep search\"\n  },\n  \"downloadWithRequestHeader\": {\n    \"message\": \"Download with request header parameters.\"\n  },\n  \"m3u8Playlist\": {\n    \"message\": \"M3U8 Playlist\"\n  },\n  \"copiedToClipboard\": {\n    \"message\": \"Copied to Clipboard\"\n  },\n  \"hasSent\": {\n    \"message\": \"Sent\"\n  },\n  \"sendFailed\": {\n    \"message\": \"Send Failed\"\n  },\n  \"confirmDownload\": {\n    \"message\": \"$num$ files in total, confirm download?\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"confirmLoading\": {\n    \"message\": \"There are $num$ resources in total, do you want to cancel the loading?\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"waitingForMedia\": {\n    \"message\": \"Waiting to receive media files... Please do not close this page.\"\n  },\n  \"exit\": {\n    \"message\": \"Exit\"\n  },\n  \"unknownSize\": {\n    \"message\": \"Unknown size\"\n  },\n  \"saving\": {\n    \"message\": \"Saving\"\n  },\n  \"saveFailed\": {\n    \"message\": \"Save failed\"\n  },\n  \"badgeNumber\": {\n    \"message\": \"Show icon badge prompt\"\n  },\n  \"viewSlices\": {\n    \"message\": \"View all slices and download progress\"\n  },\n  \"send2local\": {\n    \"message\": \"Data transmission\"\n  },\n  \"send2MQTT\": {\n    \"message\": \"Send to MQTT\"\n  },\n  \"sendingToMQTT\": {\n    \"message\": \"Sending to MQTT server...\"\n  },\n  \"connectingToMQTT\": {\n    \"message\": \"Connecting to MQTT server...\"\n  },\n  \"sendingMessageToMQTT\": {\n    \"message\": \"Sending message to MQTT server...\"\n  },\n  \"messageSentToMQTT\": {\n    \"message\": \"Message sent to MQTT server\"\n  },\n  \"popup\": {\n    \"message\": \"Popup\"\n  },\n  \"defaultPopup\": {\n    \"message\": \"Default Popup Mode\"\n  },\n  \"invokeApp\": {\n    \"message\": \"Invoke application\"\n  },\n  \"onlineServiceAddress\": {\n    \"message\": \"Online Service Address\"\n  },\n  \"withinChina\": {\n    \"message\": \"Within China\"\n  },\n  \"dataFetchFailed\": {\n    \"message\": \"Data fetch failed\"\n  },\n  \"confirmParameters\": {\n    \"message\": \"Confirm Parameters\"\n  },\n  \"searchingForRealKey\": {\n    \"message\": \"Searching for real key\"\n  },\n  \"verifying\": {\n    \"message\": \"Verifying\"\n  },\n  \"realKeyNotFound\": {\n    \"message\": \"Real key not found\"\n  },\n  \"blockUrl\": {\n    \"message\": \"Block URL\"\n  },\n  \"addUrl\": {\n    \"message\": \"Add URL\"\n  },\n  \"wildcards\": {\n    \"message\": \"wildcards\"\n  },\n  \"blockUrlTips\": {\n    \"message\": \"Support wildcards * and ?\"\n  },\n  \"setWhiteList\": {\n    \"message\": \"Set to whitelist\"\n  },\n  \"autoSend\": {\n    \"message\": \"Automatic data transmission\"\n  },\n  \"manualSend\": {\n    \"message\": \"Manual data transmission\"\n  },\n  \"requestMethod\": {\n    \"message\": \"Request Method\"\n  },\n  \"requestBody\": {\n    \"message\": \"Request Body\"\n  },\n  \"sort\": {\n    \"message\": \"Sort\"\n  },\n  \"asc\": {\n    \"message\": \"Ascending\"\n  },\n  \"desc\": {\n    \"message\": \"Descending\"\n  },\n  \"getTime\": {\n    \"message\": \"Retrieval Time\"\n  },\n  \"fileSize\": {\n    \"message\": \"File Size\"\n  },\n  \"title\": {\n    \"message\": \"Title\"\n  },\n  \"noKeyIsRequired\": {\n    \"message\": \"No key is required\"\n  },\n  \"estimateSize\": {\n    \"message\": \"Estimated size\"\n  },\n  \"retryCount\": {\n    \"message\": \"Retry count\"\n  },\n  \"useSidePanel\": {\n    \"message\": \"Use side panel\"\n  },\n  \"Script\": {\n    \"message\": \"Script\"\n  },\n  \"alwaysSearch\": {\n    \"message\": \"Always enable deep search\"\n  },\n  \"sideurlprotocol\": {\n    \"message\": \"Protocolo URL m3u8dl\"\n  },\n  \"deleteDuplicateFilenames\": {\n    \"message\": \"Delete duplicate filenames\"\n  },\n  \"alertimport\": {\n    \"message\": \"Import completed\"\n  },\n  \"isBlockedSite\": {\n    \"message\": \"This site requires blocking the operation of this extension\"\n  }\n}"
  },
  {
    "path": "_locales/es/messages.json",
    "content": "{\n  \"catCatch\": {\n    \"message\": \"cat-catch\"\n  },\n  \"description\": {\n    \"message\": \"Herramienta de rastreo de medios web\"\n  },\n  \"confirm\": {\n    \"message\": \"Confirmar\"\n  },\n  \"currentPage\": {\n    \"message\": \"Página actual\"\n  },\n  \"otherPage\": {\n    \"message\": \"Otra página\"\n  },\n  \"otherFeatures\": {\n    \"message\": \"Otras funciones\"\n  },\n  \"mediaControl\": {\n    \"message\": \"Control medios\"\n  },\n  \"loadingData\": {\n    \"message\": \"Cargando datos...\"\n  },\n  \"selectWebpage\": {\n    \"message\": \"Sitio:\"\n  },\n  \"selectMedia\": {\n    \"message\": \"Medios:\"\n  },\n  \"noMediaDetected\": {\n    \"message\": \"No se detectan medios en la página web\"\n  },\n  \"noControllableMediaDetected\": {\n    \"message\": \"No se detectan medios controlables\"\n  },\n  \"multiplier\": {\n    \"message\": \"Multiplicador:\"\n  },\n  \"speedPlayback\": {\n    \"message\": \"Velocidad\"\n  },\n  \"play\": {\n    \"message\": \"Reproducir\"\n  },\n  \"normalPlay\": {\n    \"message\": \"Normal\"\n  },\n  \"pictureInPicture\": {\n    \"message\": \"Imagen en imagen\"\n  },\n  \"fullscreen\": {\n    \"message\": \"Pantalla completa\"\n  },\n  \"screenshot\": {\n    \"message\": \"Captura\"\n  },\n  \"loop\": {\n    \"message\": \"Bucle\"\n  },\n  \"mute\": {\n    \"message\": \"Silencio\"\n  },\n  \"volume\": {\n    \"message\": \"Volumen\"\n  },\n  \"functionEntry\": {\n    \"message\": \"Funciones entrada\"\n  },\n  \"downloader\": {\n    \"message\": \"Descargador\"\n  },\n  \"parser\": {\n    \"message\": \"Analizar\"\n  },\n  \"m3u8Parser\": {\n    \"message\": \"Analizar M3U8\"\n  },\n  \"mpdParser\": {\n    \"message\": \"Analizar MPD\"\n  },\n  \"jsonFormatter\": {\n    \"message\": \"Formateador JSON\"\n  },\n  \"expandAll\": {\n    \"message\": \"Expandir todo\"\n  },\n  \"expandPlayable\": {\n    \"message\": \"Expandir reproducible\"\n  },\n  \"expandSelected\": {\n    \"message\": \"Expandir seleccionado\"\n  },\n  \"collapseAll\": {\n    \"message\": \"Contraer todo\"\n  },\n  \"videoRecording\": {\n    \"message\": \"Grabar vídeo\"\n  },\n  \"closeRecording\": {\n    \"message\": \"Cerrar grabar\"\n  },\n  \"recordWebRTC\": {\n    \"message\": \"Grabar WebRTC\"\n  },\n  \"screenCapture\": {\n    \"message\": \"Capturar pantalla\"\n  },\n  \"simulateMobile\": {\n    \"message\": \"Simular modo móvil\"\n  },\n  \"autoDownload\": {\n    \"message\": \"Descarga automática\"\n  },\n  \"onlineMerge\": {\n    \"message\": \"Unir\"\n  },\n  \"download\": {\n    \"message\": \"Descargar\"\n  },\n  \"copy\": {\n    \"message\": \"Copiar\"\n  },\n  \"selectAll\": {\n    \"message\": \"Seleccionar todo\"\n  },\n  \"invertSelection\": {\n    \"message\": \"Alternar\"\n  },\n  \"filter\": {\n    \"message\": \"Filtro\"\n  },\n  \"clear\": {\n    \"message\": \"Limpiar\"\n  },\n  \"deepSearch\": {\n    \"message\": \"Buscar\"\n  },\n  \"closeSearch\": {\n    \"message\": \"Cerrar buscar\"\n  },\n  \"cacheCapture\": {\n    \"message\": \"Capturar\"\n  },\n  \"closeCapture\": {\n    \"message\": \"Cerrar capturar\"\n  },\n  \"moreFeatures\": {\n    \"message\": \"Más\"\n  },\n  \"pause\": {\n    \"message\": \"Pausa\"\n  },\n  \"settings\": {\n    \"message\": \"Ajustes\"\n  },\n  \"closeSimulation\": {\n    \"message\": \"Cerrar simulación\"\n  },\n  \"closeDownload\": {\n    \"message\": \"Cerrar descarga\"\n  },\n  \"enable\": {\n    \"message\": \"Activar\"\n  },\n  \"disable\": {\n    \"message\": \"Desactivar\"\n  },\n  \"noData\": {\n    \"message\": \"Sin pesca\"\n  },\n  \"regularFilterPlaceholder\": {\n    \"message\": \"Filtro de expresión regular, URL del recurso coincidente, pulse Intro para confirmar\"\n  },\n  \"option\": {\n    \"message\": \"Opciones\"\n  },\n  \"titleOption\": {\n    \"message\": \"Opciones cat-catch\"\n  },\n  \"titleDownload\": {\n    \"message\": \"Descargar con cat-catch\"\n  },\n  \"titleM3U8\": {\n    \"message\": \"Analizar m3u8 con cat-catch\"\n  },\n  \"titleJson\": {\n    \"message\": \"Formateador json con cat-catch\"\n  },\n  \"titledash\": {\n    \"message\": \"Analizar dash con cat-catch\"\n  },\n  \"suffix\": {\n    \"message\": \"Sufijo\"\n  },\n  \"suffixTip\": {\n    \"message\": \"Colocar sufijo que no contenga un punto(.), si no es necesario el filtro del tamaño, colocar 0.\"\n  },\n  \"extensionName\": {\n    \"message\": \"Nombre extensión\"\n  },\n  \"filterSize\": {\n    \"message\": \"Filtro tamaño\"\n  },\n  \"delete\": {\n    \"message\": \"Borrar\"\n  },\n  \"addSuffix\": {\n    \"message\": \"Añadir sufijo\"\n  },\n  \"extension\": {\n    \"message\": \"Extensión\"\n  },\n  \"disableAll\": {\n    \"message\": \"Desactivar todo\"\n  },\n  \"enableAll\": {\n    \"message\": \"Activar todo\"\n  },\n  \"type\": {\n    \"message\": \"Tipo\"\n  },\n  \"addType\": {\n    \"message\": \"Añadir tipo\"\n  },\n  \"typeTip\": {\n    \"message\": \"Introducir el tipo de contenido correcto, si no es necesario el filtro del tamaño, colocar 0.\"\n  },\n  \"addTypeError\": {\n    \"message\": \"El formato del tipo de captura es incorrecto, por favor compruebe\"\n  },\n  \"regexMatch\": {\n    \"message\": \"Coincidencia Regex\"\n  },\n  \"blockResource\": {\n    \"message\": \"Bloquear recurso\"\n  },\n  \"alert\": {\n    \"message\": \"Alerta\"\n  },\n  \"regexExpression\": {\n    \"message\": \"Expresión Regex\"\n  },\n  \"addRegex\": {\n    \"message\": \"Añadir Regex\"\n  },\n  \"regexTest\": {\n    \"message\": \"Prueba Regex\"\n  },\n  \"regex\": {\n    \"message\": \"regex\"\n  },\n  \"flag\": {\n    \"message\": \"Indicador\"\n  },\n  \"result\": {\n    \"message\": \"Resultado\"\n  },\n  \"match\": {\n    \"message\": \"Igual\"\n  },\n  \"noMatch\": {\n    \"message\": \"No igual\"\n  },\n  \"blockResourceTip\": {\n    \"message\": \"Bloquea los recursos que no quieres que aparezcan\"\n  },\n  \"flagTip\": {\n    \"message\": \"i: ignorar el caso, g: búsqueda global. También puede dejarse vacío\"\n  },\n  \"regexSuffixTip\": {\n    \"message\": \"Asignar un sufijo a la URL obtenida. Puede dejarse vacío, y el sufijo se truncará automáticamente (muchos archivos no tienen sufijo)\"\n  },\n  \"regexTip\": {\n    \"message\": \"Las expresiones regulares consumen muchos recursos, úselas con cuidado si no es necesario\"\n  },\n  \"copyTip\": {\n    \"message\": \"Para facilitar el uso de aplicaciones de terceros, personalice el contenido escrito en el portapapeles mediante el botón Copiar\"\n  },\n  \"replaceKeywordList\": {\n    \"message\": \"Sustituir la lista de palabras clave\"\n  },\n  \"otherFiles\": {\n    \"message\": \"Otros archivos\"\n  },\n  \"resetCopySettings\": {\n    \"message\": \"Restaurar ajustes copia\"\n  },\n  \"autoSetRefererCookieParams\": {\n    \"message\": \"Colocar automáticamente los parámetros Referer y Cookies\"\n  },\n  \"secretKey\": {\n    \"message\": \"Clave secreta\"\n  },\n  \"address\": {\n    \"message\": \"Dirección\"\n  },\n  \"documentation\": {\n    \"message\": \"Documentación\"\n  },\n  \"aria2Tip\": {\n    \"message\": \"Una excelente herramienta de descarga, vea cómo usarla, en\"\n  },\n  \"m3u8DLTips\": {\n    \"message\": \"Una excelente herramienta de descarga de m3u8 y mpd de terceros, vea cómo usarla, en\"\n  },\n  \"invoke\": {\n    \"message\": \"Invocar\"\n  },\n  \"parameter\": {\n    \"message\": \"Parámetros\"\n  },\n  \"parameterSetting\": {\n    \"message\": \"Ajuste parámetros\"\n  },\n  \"test\": {\n    \"message\": \"Prueba\"\n  },\n  \"replaceTags\": {\n    \"message\": \"Sustituir etiquetas\"\n  },\n  \"customSaveFileName\": {\n    \"message\": \"Guardar nombre en archivo personal\"\n  },\n  \"userAgentTip\": {\n    \"message\": \"Predeterminado, agente usuario del navegador actual\"\n  },\n  \"alwaysDisableCatCatcher\": {\n    \"message\": \"Desactivar siempre descargador Cat-Catch\"\n  },\n  \"autoClosePageAfterDownload\": {\n    \"message\": \"Cerrar la página después de descargar\"\n  },\n  \"openDownloaderPageInBackground\": {\n    \"message\": \"Abrir la página del descargador en segundo plano\"\n  },\n  \"downloaderTip\": {\n    \"message\": \"Si falla la descarga de recursos, activar el descargador y vuelve a intentarlo.\"\n  },\n  \"autoDownM3u8Tip\": {\n    \"message\": \"Clic en Descargar y el analizador m3u8 empezará a fusionar y a descargar.\"\n  },\n  \"otherSettings\": {\n    \"message\": \"Otros ajustes\"\n  },\n  \"resetOtherSettings\": {\n    \"message\": \"Restaurar otros ajustes\"\n  },\n  \"previewMode\": {\n    \"message\": \"Usar el protocolo de llamada del reproductor local para abrir previsualizaciones de vídeo\"\n  },\n  \"previewModePlaceholder\": {\n    \"message\": \"Dejar vacío para desactivarlo. Se utiliza la ventana emergente para visualizar el vídeo\"\n  },\n  \"preview\": {\n    \"message\": \"Previo\"\n  },\n  \"customFilenameOption\": {\n    \"message\": \"Nombre para guardar el archivo (predeterminado: título de la página web)\"\n  },\n  \"saveAsOption\": {\n    \"message\": \"Seleccionar la carpeta de almacenamiento después de la descarga\"\n  },\n  \"iconOption\": {\n    \"message\": \"Mostrar el icono del sitio web\"\n  },\n  \"clearOption\": {\n    \"message\": \"Actualizar, navegar a una página y borrar datos capturados de la pestaña actual.\"\n  },\n  \"doNotClear\": {\n    \"message\": \"No borrar\"\n  },\n  \"normalClear\": {\n    \"message\": \"Borrar normal\"\n  },\n  \"moreFrequent\": {\n    \"message\": \"Más frecuentes\"\n  },\n  \"dopreview\": {\n    \"message\": \"previo\"\n  },\n  \"dopopup\": {\n    \"message\": \"ventana emergente\"\n  },\n  \"winpreview\": {\n    \"message\": \"ventana previo\"\n  },\n  \"winpopup\": {\n    \"message\": \"ventana\"\n  },\n  \"excludeDuplicateResources\": {\n    \"message\": \"Excluir los recursos duplicados (demasiados recursos consumen mucha CPU)\"\n  },\n  \"customCSS\": {\n    \"message\": \"CSS personal\"\n  },\n  \"MQTT\": {\n    \"message\": \"MQTT\"\n  },\n  \"mqttBroker\": {\n    \"message\": \"Dirección agente\"\n  },\n  \"mqttPath\": {\n    \"message\": \"Ruta\"\n  },\n  \"mqttProtocol\": {\n    \"message\": \"Protocolo\"\n  },\n  \"mqttClientId\": {\n    \"message\": \"ID cliente\"\n  },\n  \"mqttTitleLength\": {\n    \"message\": \"Longitud máxima título\"\n  },\n  \"mqttUsername\": {\n    \"message\": \"Usuario\"\n  },\n  \"mqttPassword\": {\n    \"message\": \"Contraseña\"\n  },\n  \"mqttTopic\": {\n    \"message\": \"Tema\"\n  },\n  \"mqttQos\": {\n    \"message\": \"Nivel QoS\"\n  },\n  \"mqttQos0\": {\n    \"message\": \"(Como máximo una vez)\"\n  },\n  \"mqttQos1\": {\n    \"message\": \"(Al menos una vez)\"\n  },\n  \"mqttQos2\": {\n    \"message\": \"(Exactamente una vez)\"\n  },\n  \"mqttDataFormat\": {\n    \"message\": \"Formato datos\"\n  },\n  \"mqttDataFormatHelp\": {\n    \"message\": \"{\\\"url\\\": \\\"$${url}\\\", \\\"title\\\": \\\"$${title}\\\", \\\"type\\\": \\\"$${type}\\\", \\\"ext\\\": \\\"$${ext}\\\", \\\"timestamp\\\": \\\"$${timestamp}\\\"}\"\n  },\n  \"mqttDataFormatVars\": {\n    \"message\": \"Variables disponibles\"\n  },\n  \"mqttDataFormatDefault\": {\n    \"message\": \"Dejar vacío para usar el formato JSON predeterminado\"\n  },\n  \"mqttProtocolWss\": {\n    \"message\": \"WSS (Seguro)\"\n  },\n  \"mqttProtocolWs\": {\n    \"message\": \"WS (No seguro)\"\n  },\n  \"mqttTitleLengthHelp\": {\n    \"message\": \"Longitud máxima del título para enviar mensajes MQTT\"\n  },\n  \"mqttBrokerHelp\": {\n    \"message\": \"Nombre host o dirección IP agente MQTT\"\n  },\n  \"mqttPathHelp\": {\n    \"message\": \"Ruta de websocket (generalmente /mqtt o /ws)\"\n  },\n  \"mqttClientIdHelp\": {\n    \"message\": \"Identificador de cliente único para esta conexión\"\n  },\n  \"mqttTopicHelp\": {\n    \"message\": \"Tema MQTT para publicar mensajes\"\n  },\n  \"mqttQosHelp\": {\n    \"message\": \"Calidad del nivel de servicio (0 = como máximo una vez, 1 = al menos una vez, 2 = exactamente una vez)\"\n  },\n  \"mqttCredentialsHelp\": {\n    \"message\": \"Deje el nombre de usuario/contraseña vacío si no es necesario\"\n  },\n  \"operation\": {\n    \"message\": \"Operación\"\n  },\n  \"exportSettings\": {\n    \"message\": \"Exportar ajustes\"\n  },\n  \"importConfiguration\": {\n    \"message\": \"Importar ajustes\"\n  },\n  \"clearCapturedData\": {\n    \"message\": \"Borrar datos capturados\"\n  },\n  \"resetSettings\": {\n    \"message\": \"Restaurar ajustes\"\n  },\n  \"resetAllSettings\": {\n    \"message\": \"Restaurar todos los ajustes\"\n  },\n  \"restartExtension\": {\n    \"message\": \"Reiniciar extensión\"\n  },\n  \"about\": {\n    \"message\": \"Acerca de\"\n  },\n  \"confirmReset\": {\n    \"message\": \"¿Seguro que quieres restaurar?\"\n  },\n  \"invokeProtocolTemplate\": {\n    \"message\": \"Plantilla protocolo invocar\"\n  },\n  \"customVLCProtocol\": {\n    \"message\": \"Protocolo VLC personal\"\n  },\n  \"systemShare\": {\n    \"message\": \"Compartir sistema\"\n  },\n  \"default\": {\n    \"message\": \"Default\"\n  },\n  \"goBack\": {\n    \"message\": \"Atrás\"\n  },\n  \"openDir\": {\n    \"message\": \"Abrir carpeta\"\n  },\n  \"downloadDir\": {\n    \"message\": \"Carpeta descarga\"\n  },\n  \"sendFfmpeg\": {\n    \"message\": \"Enviar a ffmpeg en línea\"\n  },\n  \"autoCloserDownload\": {\n    \"message\": \"Cerrar automáticamente la página tras la descarga\"\n  },\n  \"openInBgDownload\": {\n    \"message\": \"Abrir la página del descargador en segundo plano\"\n  },\n  \"m3u8Placeholder\": {\n    \"message\": \"Introducir el contenido m3u8 o la lista de fragmentos ts.\"\n  },\n  \"m3u8Url\": {\n    \"message\": \"URL m3u8\"\n  },\n  \"nextLevel\": {\n    \"message\": \"Nivel siguiente\"\n  },\n  \"nextLevelTip\": {\n    \"message\": \"Este archivo M3U8 anida varios archivos M3U8.\"\n  },\n  \"multipleAudios\": {\n    \"message\": \"Múltiples audios\"\n  },\n  \"multipleAudiosTip\": {\n    \"message\": \"Este archivo M3U8 anida múltiples audios\"\n  },\n  \"multipleSubtitles\": {\n    \"message\": \"Múltiples subtítulos\"\n  },\n  \"multipleSubtitlesTip\": {\n    \"message\": \"Este archivo M3U8 anida múltiples subtítulos.\"\n  },\n  \"possibleKey\": {\n    \"message\": \"Posibles claves encontradas\"\n  },\n  \"loading\": {\n    \"message\": \"Cargando...\"\n  },\n  \"waitDownload\": {\n    \"message\": \"Esperando descarga...\"\n  },\n  \"downloadSegmentList\": {\n    \"message\": \"Descargar lista\"\n  },\n  \"originalM3u8\": {\n    \"message\": \"M3U8 original\"\n  },\n  \"localM3u8\": {\n    \"message\": \"M3U8 local\"\n  },\n  \"segmentList\": {\n    \"message\": \"Segmento\"\n  },\n  \"downloadProgress\": {\n    \"message\": \"Progreso descarga\"\n  },\n  \"getParameters\": {\n    \"message\": \"Parámetros GET\"\n  },\n  \"restoreGetParameters\": {\n    \"message\": \"Restaurar parámetros GET\"\n  },\n  \"requestHeaders\": {\n    \"message\": \"Solicitud cabecera\"\n  },\n  \"setRequestHeaders\": {\n    \"message\": \"Cabeceras de solicitud.\"\n  },\n  \"invokeM3u8DL\": {\n    \"message\": \"Invocar M3U8DL\"\n  },\n  \"copyCommand\": {\n    \"message\": \"Copiar comando\"\n  },\n  \"previewCommand\": {\n    \"message\": \"Previo comando\"\n  },\n  \"addSettingParameters\": {\n    \"message\": \"Añadir ajustes parámetros\"\n  },\n  \"customKeyPlaceholder\": {\n    \"message\": \"Personaliza la clave en hexadecimal o base64, o la dirección de la clave\"\n  },\n  \"uploadKey\": {\n    \"message\": \"Cargar clave\"\n  },\n  \"downloadThreads\": {\n    \"message\": \"Tareas\"\n  },\n  \"ffmpegTranscoding\": {\n    \"message\": \"Transcodificar FFmpeg\"\n  },\n  \"mp4Format\": {\n    \"message\": \"MP4\"\n  },\n  \"downloadWhileSaving\": {\n    \"message\": \"Descargar stream\"\n  },\n  \"audioOnly\": {\n    \"message\": \"Sólo audio\"\n  },\n  \"saveAs\": {\n    \"message\": \"Guardar como\"\n  },\n  \"skipDecryption\": {\n    \"message\": \"Omitir desencriptar\"\n  },\n  \"newDownloader\": {\n    \"message\": \"Nuevo descargador\"\n  },\n  \"downloadRange\": {\n    \"message\": \"Rango descarga\"\n  },\n  \"recordLive\": {\n    \"message\": \"Grabar\"\n  },\n  \"mergeDownloads\": {\n    \"message\": \"Unir descargas\"\n  },\n  \"redownloadFailedItems\": {\n    \"message\": \"Descargar fallidos\"\n  },\n  \"downloadExistingData\": {\n    \"message\": \"Descargar datos existentes\"\n  },\n  \"stopDownload\": {\n    \"message\": \"Detener descarga\"\n  },\n  \"start\": {\n    \"message\": \"Iniciar\"\n  },\n  \"end\": {\n    \"message\": \"Finalizar\"\n  },\n  \"resolution\": {\n    \"message\": \"Resolución\"\n  },\n  \"duration\": {\n    \"message\": \"Duración\"\n  },\n  \"bitrate\": {\n    \"message\": \"Bitrate\"\n  },\n  \"ADTSerror\": {\n    \"message\": \"No se encuentra la cabecera ADTS. Puede ser un recurso encriptado AES-128-ECB, que actualmente no sea compatible. Usar software de fusión de terceros.\"\n  },\n  \"m3u8Error\": {\n    \"message\": \"Hay errores al analizar o reproducir el archivo M3U8, compruebe la consola para obtener información detallada de los errores\"\n  },\n  \"noAudio\": {\n    \"message\": \"Sin audio\"\n  },\n  \"noVideo\": {\n    \"message\": \"Sin vídeo\"\n  },\n  \"hevcTip\": {\n    \"message\": \"Los archivos de fragmentos codificados en HEVC/H.265 sólo son compatibles con la transcodificación ffmpeg en línea\"\n  },\n  \"hevcPreviewTip\": {\n    \"message\": \"Los archivos de fragmentos codificados en HEVC/H.265 no son compatibles con la previsualización.\"\n  },\n  \"m3u8Info\": {\n    \"message\": \"Un total de $num$ archivo(s), con una duración total de $time$.\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      },\n      \"time\": {\n        \"content\": \"$2\"\n      }\n    }\n  },\n  \"encryptedHLS\": {\n    \"message\": \"HLS encriptado\"\n  },\n  \"encryptedSAMPLE\": {\n    \"message\": \"Los recursos encriptados con SAMPLE-AES-CTR no pueden manejarse por el momento.\"\n  },\n  \"liveHLS\": {\n    \"message\": \"HLS en directo\"\n  },\n  \"keyAddress\": {\n    \"message\": \"Dirección clave\"\n  },\n  \"key\": {\n    \"message\": \"Clave\"\n  },\n  \"encryptionAlgorithm\": {\n    \"message\": \"Método\"\n  },\n  \"keyDownloadFailed\": {\n    \"message\": \"Error en la descarga de claves\"\n  },\n  \"savePrompt\": {\n    \"message\": \"Guardado en disco, compruebe el contenido descargado en el navegador.\"\n  },\n  \"close\": {\n    \"message\": \"Cerrar\"\n  },\n  \"blobM3u8DLError\": {\n    \"message\": \"Las URL Blob no pueden invocar M3U8DL para la descarga\"\n  },\n  \"M3U8DLparameterLong\": {\n    \"message\": \"El parámetro M3U8DL es demasiado largo.\"\n  },\n  \"runningCannotChangeSettings\": {\n    \"message\": \"En ejecución, no se puede cambiar la configuración\"\n  },\n  \"streamSaverTip\": {\n    \"message\": \"La función de 'guardar mientras se descarga' no admite la conversión de formato en línea de ffmpeg, no permite volver a descargar los cortes erróneos y no admite 'guardar como'..\"\n  },\n  \"stopRecording\": {\n    \"message\": \"Detener grabación\"\n  },\n  \"waitingForLiveData\": {\n    \"message\": \"A la espera de datos en directo\"\n  },\n  \"sNumError\": {\n    \"message\": \"Error de número de serie\"\n  },\n  \"startGTend\": {\n    \"message\": \"El número inicial no puede ser mayor que el final\"\n  },\n  \"sNumMax\": {\n    \"message\": \"El número de serie no puede ser superior a $num$\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"incorrectKey\": {\n    \"message\": \"Clave incorrecta\"\n  },\n  \"addParameters\": {\n    \"message\": \"Añadir parámetros\"\n  },\n  \"decryptionError\": {\n    \"message\": \"Error al desencriptar\"\n  },\n  \"downloadFailed\": {\n    \"message\": \"Error en descarga\"\n  },\n  \"retryDownload\": {\n    \"message\": \"Reintentar descarga\"\n  },\n  \"recordingDuration\": {\n    \"message\": \"Duración grabación\"\n  },\n  \"downloaded\": {\n    \"message\": \"Descargado\"\n  },\n  \"downloadedVideoLength\": {\n    \"message\": \"Duración vídeo descargado\"\n  },\n  \"downloadComplete\": {\n    \"message\": \"Descarga completa\"\n  },\n  \"retryingDownload\": {\n    \"message\": \"Reintentar descarga\"\n  },\n  \"merging\": {\n    \"message\": \"Uniendo\"\n  },\n  \"fileTooLarge\": {\n    \"message\": \"Archivo demasiado grande, mayor de $size$\",\n    \"placeholders\": {\n      \"size\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"fileTooLargeStream\": {\n    \"message\": \"¿Archivo mayor de $size$, activar descarga en stream?\",\n    \"placeholders\": {\n      \"size\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"formatConversionError\": {\n    \"message\": \"Error de conversión de formato\"\n  },\n  \"streamOnbeforeunload\": {\n    \"message\": \"La transmisión está en curso, la descarga se detendrá tras el cierre\"\n  },\n  \"fileLoading\": {\n    \"message\": \"Cargando archivo\"\n  },\n  \"expandAllNodes\": {\n    \"message\": \"Expandir nodos JSON\"\n  },\n  \"collapseAllNodes\": {\n    \"message\": \"Contraer nodos JSON\"\n  },\n  \"fileRetrievalFailed\": {\n    \"message\": \"Fallo en la recuperación de archivos\"\n  },\n  \"selectVideo\": {\n    \"message\": \"Seleccionar vídeo\"\n  },\n  \"extractSlices\": {\n    \"message\": \"Extraer partes\"\n  },\n  \"convertToM3U8\": {\n    \"message\": \"Convertir a analizador M3U8\"\n  },\n  \"selectAudio\": {\n    \"message\": \"Seleccionar audio\"\n  },\n  \"audio\": {\n    \"message\": \"Audio\"\n  },\n  \"video\": {\n    \"message\": \"Vídeo\"\n  },\n  \"DRMerror\": {\n    \"message\": \"Los medios tienen protección DRM, usar herramientas de terceros para descargar\"\n  },\n  \"regexTitle\": {\n    \"message\": \"Coincidencia de expresión regular o desde una búsqueda profunda\"\n  },\n  \"downloadWithRequestHeader\": {\n    \"message\": \"Descargar con los parámetros del encabezado de la solicitud.\"\n  },\n  \"m3u8Playlist\": {\n    \"message\": \"Lista de reproducción M3U8\"\n  },\n  \"copiedToClipboard\": {\n    \"message\": \"Copiado al portapapeles\"\n  },\n  \"hasSent\": {\n    \"message\": \"Enviado\"\n  },\n  \"sendFailed\": {\n    \"message\": \"Enviar fallido\"\n  },\n  \"confirmDownload\": {\n    \"message\": \"$num$ archivos en total, ¿confirmar descarga?\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"confirmLoading\": {\n    \"message\": \"Hay $num$ recursos en total, ¿desea cancelar la carga?\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"waitingForMedia\": {\n    \"message\": \"Esperando recibir archivos multimedia... No cierre esta página.\"\n  },\n  \"exit\": {\n    \"message\": \"Salir\"\n  },\n  \"unknownSize\": {\n    \"message\": \"Tamaño desconocido\"\n  },\n  \"saving\": {\n    \"message\": \"Guardando\"\n  },\n  \"saveFailed\": {\n    \"message\": \"Error al guardar\"\n  },\n  \"badgeNumber\": {\n    \"message\": \"Mostrar icono distintivo\"\n  },\n  \"viewSlices\": {\n    \"message\": \"Ver todas las partes y progreso descarga\"\n  },\n  \"send2local\": {\n    \"message\": \"Transmisión de datos\"\n  },\n  \"send2MQTT\": {\n    \"message\": \"Enviar a MQTT\"\n  },\n  \"sendingToMQTT\": {\n    \"message\": \"Enviando al sercidor MQTT...\"\n  },\n  \"connectingToMQTT\": {\n    \"message\": \"Conectando al servidor MQTT...\"\n  },\n  \"sendingMessageToMQTT\": {\n    \"message\": \"Enviando mensaje al servidor MQTT...\"\n  },\n  \"messageSentToMQTT\": {\n    \"message\": \"Mensaje enviado al servidor MQTT\"\n  },\n  \"popup\": {\n    \"message\": \"Ventana emergente\"\n  },\n  \"defaultPopup\": {\n    \"message\": \"Ventana predeterminada\"\n  },\n  \"invokeApp\": {\n    \"message\": \"Invocar aplicación\"\n  },\n  \"onlineServiceAddress\": {\n    \"message\": \"Dirección del servicio en línea\"\n  },\n  \"withinChina\": {\n    \"message\": \"En China\"\n  },\n  \"dataFetchFailed\": {\n    \"message\": \"Error al obtener datos\"\n  },\n  \"confirmParameters\": {\n    \"message\": \"Confirmar parámetros\"\n  },\n  \"searchingForRealKey\": {\n    \"message\": \"Buscando la clave real\"\n  },\n  \"verifying\": {\n    \"message\": \"Verificando\"\n  },\n  \"realKeyNotFound\": {\n    \"message\": \"Clave real no encontrada\"\n  },\n  \"blockUrl\": {\n    \"message\": \"Bloquear URL\"\n  },\n  \"addUrl\": {\n    \"message\": \"Añadir URL\"\n  },\n  \"wildcards\": {\n    \"message\": \"comodines\"\n  },\n  \"blockUrlTips\": {\n    \"message\": \"Admite los comodines * y ?\"\n  },\n  \"setWhiteList\": {\n    \"message\": \"Colocar en la lista blanca\"\n  },\n  \"autoSend\": {\n    \"message\": \"Transmisión manual de datos\"\n  },\n  \"manualSend\": {\n    \"message\": \"Transmisión automática de datos\"\n  },\n  \"requestMethod\": {\n    \"message\": \"Método de solicitud\"\n  },\n  \"requestBody\": {\n    \"message\": \"Cuerpo de la solicitud\"\n  },\n  \"sort\": {\n    \"message\": \"Ordenar\"\n  },\n  \"asc\": {\n    \"message\": \"Ascendente\"\n  },\n  \"desc\": {\n    \"message\": \"Descendente\"\n  },\n  \"getTime\": {\n    \"message\": \"Tiempo recuperación\"\n  },\n  \"fileSize\": {\n    \"message\": \"Tamaño archivo\"\n  },\n  \"title\": {\n    \"message\": \"Título\"\n  },\n  \"noKeyIsRequired\": {\n    \"message\": \"No se necesita clave\"\n  },\n  \"estimateSize\": {\n    \"message\": \"Tamaño estimado\"\n  },\n  \"retryCount\": {\n    \"message\": \"Reintentar contar\"\n  },\n  \"useSidePanel\": {\n    \"message\": \"Usar panel lateral\"\n  },\n  \"Script\": {\n    \"message\": \"Script\"\n  },\n  \"alwaysSearch\": {\n    \"message\": \"Activar una búsqueda profunda\"\n  },\n  \"alertimport\": {\n  \t\"message\": \"importación completada\"\n  },\n  \"sideurlprotocol\": {\n    \"message\": \"Protocolo URL m3u8dl\"\n  }, \n  \"deleteDuplicateFilenames\": {\n    \"message\": \"Eliminar nombres de archivo duplicados\"\n  }\n}\n"
  },
  {
    "path": "_locales/ja/messages.json",
    "content": "{\n  \"catCatch\": {\n    \"message\": \"cat-catch\"\n  },\n  \"description\": {\n    \"message\": \"ウェブメディア探す TOOL\"\n  },\n  \"confirm\": {\n    \"message\": \"確認\"\n  },\n  \"currentPage\": {\n    \"message\": \"現在のページ\"\n  },\n  \"otherPage\": {\n    \"message\": \"他のページ\"\n  },\n  \"otherFeatures\": {\n    \"message\": \"その他の機能\"\n  },\n  \"mediaControl\": {\n    \"message\": \"メディアコントロール\"\n  },\n  \"loadingData\": {\n    \"message\": \"データを読み込み中…\"\n  },\n  \"selectWebpage\": {\n    \"message\": \"ページを選択:\"\n  },\n  \"selectMedia\": {\n    \"message\": \"メディアを選択:\"\n  },\n  \"noMediaDetected\": {\n    \"message\": \"メディア検出されず\"\n  },\n  \"noControllableMediaDetected\": {\n    \"message\": \"操作可能なメディア検出されず\"\n  },\n  \"multiplier\": {\n    \"message\": \"倍率:\"\n  },\n  \"speedPlayback\": {\n    \"message\": \"倍速再生\"\n  },\n  \"play\": {\n    \"message\": \"再生\"\n  },\n  \"normalPlay\": {\n    \"message\": \"通常再生\"\n  },\n  \"pictureInPicture\": {\n    \"message\": \"ピクチャインピクチャ\"\n  },\n  \"fullscreen\": {\n    \"message\": \"フルスクリーン\"\n  },\n  \"screenshot\": {\n    \"message\": \"スクリーンショット\"\n  },\n  \"loop\": {\n    \"message\": \"ループ\"\n  },\n  \"mute\": {\n    \"message\": \"ミュート\"\n  },\n  \"volume\": {\n    \"message\": \"音量\"\n  },\n  \"functionEntry\": {\n    \"message\": \"機能入口\"\n  },\n  \"downloader\": {\n    \"message\": \"ダウンローダー\"\n  },\n  \"parser\": {\n    \"message\": \"パーサー\"\n  },\n  \"m3u8Parser\": {\n    \"message\": \"M3U8パーサー\"\n  },\n  \"mpdParser\": {\n    \"message\": \"MPDパーサー\"\n  },\n  \"jsonFormatter\": {\n    \"message\": \"JSONフォーマット\"\n  },\n  \"expandAll\": {\n    \"message\": \"すべて展開\"\n  },\n  \"expandPlayable\": {\n    \"message\": \"再生可能展開\"\n  },\n  \"expandSelected\": {\n    \"message\": \"選択展開\"\n  },\n  \"collapseAll\": {\n    \"message\": \"すべて折りたたみ\"\n  },\n  \"videoRecording\": {\n    \"message\": \"ビデオ録画\"\n  },\n  \"closeRecording\": {\n    \"message\": \"録画終了\"\n  },\n  \"recordWebRTC\": {\n    \"message\": \"webRTC録画\"\n  },\n  \"screenCapture\": {\n    \"message\": \"画面キャプチャー\"\n  },\n  \"simulateMobile\": {\n    \"message\": \"モバイルシミュレーション\"\n  },\n  \"autoDownload\": {\n    \"message\": \"自動ダウンロード\"\n  },\n  \"onlineMerge\": {\n    \"message\": \"オンラインマージ\"\n  },\n  \"download\": {\n    \"message\": \"ダウンロード\"\n  },\n  \"copy\": {\n    \"message\": \"コピー\"\n  },\n  \"selectAll\": {\n    \"message\": \"全選択\"\n  },\n  \"invertSelection\": {\n    \"message\": \"選択反転\"\n  },\n  \"filter\": {\n    \"message\": \"フィルター\"\n  },\n  \"clear\": {\n    \"message\": \"クリア\"\n  },\n  \"deepSearch\": {\n    \"message\": \"深層検索\"\n  },\n  \"closeSearch\": {\n    \"message\": \"検索終了\"\n  },\n  \"cacheCapture\": {\n    \"message\": \"キャッシュキャプチャー\"\n  },\n  \"closeCapture\": {\n    \"message\": \"キャプチャー終了\"\n  },\n  \"moreFeatures\": {\n    \"message\": \"その他の機能\"\n  },\n  \"pause\": {\n    \"message\": \"一時停止\"\n  },\n  \"settings\": {\n    \"message\": \"設定\"\n  },\n  \"closeSimulation\": {\n    \"message\": \"シミュレーション終了\"\n  },\n  \"closeDownload\": {\n    \"message\": \"ダウンロード終了\"\n  },\n  \"enable\": {\n    \"message\": \"有効\"\n  },\n  \"disable\": {\n    \"message\": \"無効\"\n  },\n  \"noData\": {\n    \"message\": \"まだ検出されていません~\"\n  },\n  \"regularFilterPlaceholder\": {\n    \"message\": \"正規表現フィルター リソースURLをマッチング エンターキーで確認\"\n  },\n  \"option\": {\n    \"message\": \"オプション\"\n  },\n  \"titleOption\": {\n    \"message\": \"cat-catch 設定\"\n  },\n  \"titleDownload\": {\n    \"message\": \"cat-catch ダウンローダー\"\n  },\n  \"titleM3U8\": {\n    \"message\": \"cat-catch M3U8パーサー\"\n  },\n  \"titleJson\": {\n    \"message\": \"cat-catch JSONフォーマット\"\n  },\n  \"titledash\": {\n    \"message\": \"cat-catch DASHパーサー\"\n  },\n  \"suffix\": {\n    \"message\": \"サフィックス\"\n  },\n  \"suffixTip\": {\n    \"message\": \"'. 'を除くサフィックスを入力 0でサイズフィルターなし\"\n  },\n  \"extensionName\": {\n    \"message\": \"拡張子\"\n  },\n  \"filterSize\": {\n    \"message\": \"サイズフィルター\"\n  },\n  \"delete\": {\n    \"message\": \"削除\"\n  },\n  \"addSuffix\": {\n    \"message\": \"サフィックス追加\"\n  },\n  \"extension\": {\n    \"message\": \"拡張\"\n  },\n  \"disableAll\": {\n    \"message\": \"すべて無効\"\n  },\n  \"enableAll\": {\n    \"message\": \"すべて有効\"\n  },\n  \"type\": {\n    \"message\": \"タイプ\"\n  },\n  \"addType\": {\n    \"message\": \"タイプ追加\"\n  },\n  \"typeTip\": {\n    \"message\": \"正しいcontent-typeを入力 0でサイズフィルターなし\"\n  },\n  \"addTypeError\": {\n    \"message\": \"キャプチャータイプ形式エラー チェックしてください\"\n  },\n  \"regexMatch\": {\n    \"message\": \"正規表現マッチ\"\n  },\n  \"blockResource\": {\n    \"message\": \"リソースブロック\"\n  },\n  \"alert\": {\n    \"message\": \"通知\"\n  },\n  \"regexExpression\": {\n    \"message\": \"正規表現\"\n  },\n  \"addRegex\": {\n    \"message\": \"正規表現追加\"\n  },\n  \"regexTest\": {\n    \"message\": \"正規表現テスト\"\n  },\n  \"regex\": {\n    \"message\": \"正規表現\"\n  },\n  \"flag\": {\n    \"message\": \"フラグ\"\n  },\n  \"result\": {\n    \"message\": \"結果\"\n  },\n  \"match\": {\n    \"message\": \"マッチ\"\n  },\n  \"noMatch\": {\n    \"message\": \"ノーマッチ\"\n  },\n  \"blockResourceTip\": {\n    \"message\": \"表示させたくないリソースをブロック\"\n  },\n  \"flagTip\": {\n    \"message\": \"i: 大小無視, g: グローバル検索 空欄可\"\n  },\n  \"regexSuffixTip\": {\n    \"message\": \"取得URLにサフィックス指定 可変自動取得\"\n  },\n  \"regexTip\": {\n    \"message\": \"正規表現はリソース高負荷のため必要時のみ使用\"\n  },\n  \"copyTip\": {\n    \"message\": \"外部アプリ利用のためカスタムコピー内容設定\"\n  },\n  \"replaceKeywordList\": {\n    \"message\": \"置換キーワードリスト\"\n  },\n  \"otherFiles\": {\n    \"message\": \"その他のファイル\"\n  },\n  \"resetCopySettings\": {\n    \"message\": \"コピー設定リセット\"\n  },\n  \"autoSetRefererCookieParams\": {\n    \"message\": \"リファラーコokies自動設定\"\n  },\n  \"secretKey\": {\n    \"message\": \"シークレットキー\"\n  },\n  \"address\": {\n    \"message\": \"アドレス\"\n  },\n  \"documentation\": {\n    \"message\": \"ドキュメント\"\n  },\n  \"aria2Tip\": {\n    \"message\": \"優れたダウンロードツール 使用方法は\"\n  },\n  \"m3u8DLTips\": {\n    \"message\": \"優れたm3u8/mpdダウンロードツール 使用方法は\"\n  },\n  \"invoke\": {\n    \"message\": \"呼び出し\"\n  },\n  \"parameter\": {\n    \"message\": \"パラメーター\"\n  },\n  \"parameterSetting\": {\n    \"message\": \"パラメーターセット\"\n  },\n  \"test\": {\n    \"message\": \"テスト\"\n  },\n  \"replaceTags\": {\n    \"message\": \"タグ置換\"\n  },\n  \"customSaveFileName\": {\n    \"message\": \"保存ファイル名カスタム\"\n  },\n  \"userAgentTip\": {\n    \"message\": \"空欄でブラウザデフォルトUser Agent使用\"\n  },\n  \"alwaysDisableCatCatcher\": {\n    \"message\": \"常にcat-catchダウンローダー無効\"\n  },\n  \"autoClosePageAfterDownload\": {\n    \"message\": \"ダウンロード後自動クローズ\"\n  },\n  \"openDownloaderPageInBackground\": {\n    \"message\": \"バックグラウンドでダウンローダー開く\"\n  },\n  \"downloaderTip\": {\n    \"message\": \"ダウンロード失敗時に自動ダウンローダー起動\"\n  },\n  \"autoDownM3u8Tip\": {\n    \"message\": \"「ダウンロード」でm3u8DL即時開始\"\n  },\n  \"otherSettings\": {\n    \"message\": \"その他の設定\"\n  },\n  \"resetOtherSettings\": {\n    \"message\": \"他の設定リセット\"\n  },\n  \"previewMode\": {\n    \"message\": \"ローカルプレイヤーでビデオプレビュー\"\n  },\n  \"previewModePlaceholder\": {\n    \"message\": \"空欄でデフォルトpopupプレビュー使用\"\n  },\n  \"preview\": {\n    \"message\": \"プレビュー\"\n  },\n  \"customFilenameOption\": {\n    \"message\": \"カスタムファイル名保存(デフォルトはページタイトル)\"\n  },\n  \"saveAsOption\": {\n    \"message\": \"ダウンロード後保存先選択\"\n  },\n  \"iconOption\": {\n    \"message\": \"サイトアイコン表示\"\n  },\n  \"clearOption\": {\n    \"message\": \"リロード/遷移でタグデータクリア\"\n  },\n  \"doNotClear\": {\n    \"message\": \"クリアしない\"\n  },\n  \"normalClear\": {\n    \"message\": \"通常クリア\"\n  },\n  \"moreFrequent\": {\n    \"message\": \"より頻繁\"\n  },\n  \"dopreview\": {\n    \"message\": \"preview\"\n  },\n  \"dopopup\": {\n    \"message\": \"popup\"\n  },\n  \"winpreview\": {\n    \"message\": \"window preview\"\n  },\n  \"winpopup\": {\n    \"message\": \"window popup\"\n  },\n  \"excludeDuplicateResources\": {\n    \"message\": \"重複リソース除外(CPU高負荷注意)\"\n  },\n  \"customCSS\": {\n    \"message\": \"カスタムCSS\"\n  },\n  \"MQTT\": {\n    \"message\": \"MQTT\"\n  },\n  \"mqttBroker\": {\n    \"message\": \"MQTTブローカーアドレス\"\n  },\n  \"mqttPath\": {\n    \"message\": \"パス\"\n  },\n  \"mqttProtocol\": {\n    \"message\": \"プロトコル\"\n  },\n  \"mqttClientId\": {\n    \"message\": \"クライアントID\"\n  },\n  \"mqttTitleLength\": {\n    \"message\": \"タイトルの最大長\"\n  },\n  \"mqttUsername\": {\n    \"message\": \"ユーザー名\"\n  },\n  \"mqttPassword\": {\n    \"message\": \"パスワード\"\n  },\n  \"mqttTopic\": {\n    \"message\": \"トピック\"\n  },\n  \"mqttQos\": {\n    \"message\": \"QoSレベル\"\n  },\n  \"mqttQos0\": {\n    \"message\": \"(最大1回)\"\n  },\n  \"mqttQos1\": {\n    \"message\": \"(少なくとも1回)\"\n  },\n  \"mqttQos2\": {\n    \"message\": \"(1回のみ確実に配信)\"\n  },\n  \"mqttDataFormat\": {\n    \"message\": \"データ形式\"\n  },\n  \"mqttDataFormatHelp\": {\n    \"message\": \"{\\\"url\\\": \\\"$${url}\\\", \\\"title\\\": \\\"$${title}\\\", \\\"type\\\": \\\"$${type}\\\", \\\"ext\\\": \\\"$${ext}\\\", \\\"timestamp\\\": \\\"$${timestamp}\\\"}\"\n  },\n  \"mqttDataFormatVars\": {\n    \"message\": \"使用可能な変数\"\n  },\n  \"mqttDataFormatDefault\": {\n    \"message\": \"空の場合はデフォルトのJSON形式を使用\"\n  },\n  \"mqttProtocolWss\": {\n    \"message\": \"WSS (セキュア)\"\n  },\n  \"mqttProtocolWs\": {\n    \"message\": \"WS (非セキュア)\"\n  },\n  \"mqttTitleLengthHelp\": {\n    \"message\": \"MQTTメッセージのタイトルの最大長\"\n  },\n  \"mqttBrokerHelp\": {\n    \"message\": \"MQTTブローカーのホスト名またはIPアドレス\"\n  },\n  \"mqttPathHelp\": {\n    \"message\": \"WebSocketパス (通常は/mqttまたは/ws)\"\n  },\n  \"mqttClientIdHelp\": {\n    \"message\": \"この接続の一意のクライアント識別子\"\n  },\n  \"mqttTopicHelp\": {\n    \"message\": \"メッセージを公開するMQTTトピック\"\n  },\n  \"mqttQosHelp\": {\n    \"message\": \"サービス品質レベル (0=最大1回, 1=少なくとも1回, 2=1回のみ確実に配信)\"\n  },\n  \"mqttCredentialsHelp\": {\n    \"message\": \"認証が不要な場合は、ユーザー名とパスワードを空のままにしてください\"\n  },\n  \"operation\": {\n    \"message\": \"操作\"\n  },\n  \"exportSettings\": {\n    \"message\": \"設定エクスポート\"\n  },\n  \"importConfiguration\": {\n    \"message\": \"設定インポート\"\n  },\n  \"clearCapturedData\": {\n    \"message\": \"キャプチャーデータクリア\"\n  },\n  \"resetSettings\": {\n    \"message\": \"設定リセット\"\n  },\n  \"resetAllSettings\": {\n    \"message\": \"すべての設定リセット\"\n  },\n  \"restartExtension\": {\n    \"message\": \"拡張再起動\"\n  },\n  \"about\": {\n    \"message\": \"について\"\n  },\n  \"confirmReset\": {\n    \"message\": \"本当にリセットしますか？\"\n  },\n  \"invokeProtocolTemplate\": {\n    \"message\": \"プロトコルテンプレート呼び出し\"\n  },\n  \"customVLCProtocol\": {\n    \"message\": \"VLCプロトコルカスタム\"\n  },\n  \"systemShare\": {\n    \"message\": \"システム共有\"\n  },\n  \"default\": {\n    \"message\": \"デフォルト\"\n  },\n  \"goBack\": {\n    \"message\": \"前のページへ\"\n  },\n  \"openDir\": {\n    \"message\": \"ダウンロードフォルダを開く\"\n  },\n  \"downloadDir\": {\n    \"message\": \"ダウンロードフォルダ\"\n  },\n  \"sendFfmpeg\": {\n    \"message\": \"オンラインffmpeg送信\"\n  },\n  \"autoCloserDownload\": {\n    \"message\": \"ダウンロード後自動クローズ\"\n  },\n  \"openInBgDownload\": {\n    \"message\": \"バックグラウンドでダウンローダー開く\"\n  },\n  \"m3u8Placeholder\": {\n    \"message\": \"m3u8内容またはtsリストを入力\"\n  },\n  \"m3u8Url\": {\n    \"message\": \"m3u8アドレス\"\n  },\n  \"nextLevel\": {\n    \"message\": \"次の階層ファイル\"\n  },\n  \"nextLevelTip\": {\n    \"message\": \"このm3u8に複数のm3u8がネスト\"\n  },\n  \"multipleAudios\": {\n    \"message\": \"複数の音声\"\n  },\n  \"multipleAudiosTip\": {\n    \"message\": \"このm3u8に複数の音声がネスト\"\n  },\n  \"multipleSubtitles\": {\n    \"message\": \"複数の字幕\"\n  },\n  \"multipleSubtitlesTip\": {\n    \"message\": \"このm3u8に複数の字幕がネスト\"\n  },\n  \"possibleKey\": {\n    \"message\": \"キー候補を検出\"\n  },\n  \"loading\": {\n    \"message\": \"読み込み中…\"\n  },\n  \"waitDownload\": {\n    \"message\": \"ダウンロード待機…\"\n  },\n  \"downloadSegmentList\": {\n    \"message\": \"セグメントリストダウンロード\"\n  },\n  \"originalM3u8\": {\n    \"message\": \"オリジナルm3u8\"\n  },\n  \"localM3u8\": {\n    \"message\": \"ローカルm3u8\"\n  },\n  \"segmentList\": {\n    \"message\": \"セグメントリスト\"\n  },\n  \"downloadProgress\": {\n    \"message\": \"ダウンロード進捗\"\n  },\n  \"getParameters\": {\n    \"message\": \"GETパラメーター\"\n  },\n  \"restoreGetParameters\": {\n    \"message\": \"GETパラメーター復元\"\n  },\n  \"requestHeaders\": {\n    \"message\": \"リクエストヘッダー\"\n  },\n  \"setRequestHeaders\": {\n    \"message\": \"ヘッダー設定\"\n  },\n  \"invokeM3u8DL\": {\n    \"message\": \"m3u8DL呼び出し\"\n  },\n  \"copyCommand\": {\n    \"message\": \"コマンドコピー\"\n  },\n  \"previewCommand\": {\n    \"message\": \"プレビューコマンド\"\n  },\n  \"addSettingParameters\": {\n    \"message\": \"設定パラメーター追加\"\n  },\n  \"customKeyPlaceholder\": {\n    \"message\": \"カスタムキー 16進数/base64/キーURL\"\n  },\n  \"uploadKey\": {\n    \"message\": \"キーをアップロード\"\n  },\n  \"downloadThreads\": {\n    \"message\": \"ダウンロードスレッド\"\n  },\n  \"ffmpegTranscoding\": {\n    \"message\": \"ffmpegトランスコード\"\n  },\n  \"mp4Format\": {\n    \"message\": \"mp4フォーマット\"\n  },\n  \"downloadWhileSaving\": {\n    \"message\": \"ダウンロードしながら保存\"\n  },\n  \"audioOnly\": {\n    \"message\": \"音声のみ\"\n  },\n  \"saveAs\": {\n    \"message\": \"名前を付けて保存\"\n  },\n  \"skipDecryption\": {\n    \"message\": \"復号化をスキップ\"\n  },\n  \"newDownloader\": {\n    \"message\": \"新ダウンローダー\"\n  },\n  \"downloadRange\": {\n    \"message\": \"ダウンロード範囲\"\n  },\n  \"recordLive\": {\n    \"message\": \"ライブ録画\"\n  },\n  \"mergeDownloads\": {\n    \"message\": \"ダウンロードマージ\"\n  },\n  \"redownloadFailedItems\": {\n    \"message\": \"失敗項目再ダウンロード\"\n  },\n  \"downloadExistingData\": {\n    \"message\": \"既存データダウンロード\"\n  },\n  \"stopDownload\": {\n    \"message\": \"ダウンロード停止\"\n  },\n  \"start\": {\n    \"message\": \"開始\"\n  },\n  \"end\": {\n    \"message\": \"終了\"\n  },\n  \"resolution\": {\n    \"message\": \"解像度\"\n  },\n  \"duration\": {\n    \"message\": \"再生時間\"\n  },\n  \"bitrate\": {\n    \"message\": \"ビットレート\"\n  },\n  \"ADTSerror\": {\n    \"message\": \"ADTSヘッダ未検出 AES-128-ECB暗号化リソース 取扱不可\"\n  },\n  \"m3u8Error\": {\n    \"message\": \"m3u8解析/再生エラー コンソールで詳細確認\"\n  },\n  \"noAudio\": {\n    \"message\": \"音声なし\"\n  },\n  \"noVideo\": {\n    \"message\": \"映像なし\"\n  },\n  \"hevcTip\": {\n    \"message\": \"HEVC/H.265セグメント オンラインffmpegのみ対応\"\n  },\n  \"hevcPreviewTip\": {\n    \"message\": \"HEVC/H.265セグメント プレビュー不可\"\n  },\n  \"m3u8Info\": {\n    \"message\": \"合計 $num$ 個のファイル、総再生時間 $time$\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      },\n      \"time\": {\n        \"content\": \"$2\"\n      }\n    }\n  },\n  \"encryptedHLS\": {\n    \"message\": \"暗号化HLS\"\n  },\n  \"encryptedSAMPLE\": {\n    \"message\": \"SAMPLE-AES-CTR暗号化リソース 現在処理不可\"\n  },\n  \"liveHLS\": {\n    \"message\": \"ライブHLS\"\n  },\n  \"keyAddress\": {\n    \"message\": \"キーURL\"\n  },\n  \"key\": {\n    \"message\": \"キー\"\n  },\n  \"encryptionAlgorithm\": {\n    \"message\": \"暗号化アルゴリズム\"\n  },\n  \"keyDownloadFailed\": {\n    \"message\": \"キーダウンロード失敗\"\n  },\n  \"savePrompt\": {\n    \"message\": \"ハードディスクに保存 ブラウザダウンロード履歴を確認\"\n  },\n  \"close\": {\n    \"message\": \"閉じる\"\n  },\n  \"blobM3u8DLError\": {\n    \"message\": \"blobアドレスはm3u8DL不可\"\n  },\n  \"M3U8DLparameterLong\": {\n    \"message\": \"m3u8dlパラメーター長すぎ m3u8DL手動実行してください\"\n  },\n  \"runningCannotChangeSettings\": {\n    \"message\": \"実行中 設定変更不可\"\n  },\n  \"streamSaverTip\": {\n    \"message\": \"ダウンロードしながら保存 ffmpeg変換不可 切片再ダウンロード不可 名前指定不可\"\n  },\n  \"stopRecording\": {\n    \"message\": \"録画停止\"\n  },\n  \"waitingForLiveData\": {\n    \"message\": \"ライブデータ待機中…ページを閉じないでください…\"\n  },\n  \"sNumError\": {\n    \"message\": \"番号エラー\"\n  },\n  \"startGTend\": {\n    \"message\": \"開始番号は終了番号より大きくできません\"\n  },\n  \"sNumMax\": {\n    \"message\": \"最大番号は $num$ を超えられません\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"incorrectKey\": {\n    \"message\": \"キーが間違っています\"\n  },\n  \"addParameters\": {\n    \"message\": \"パラメーター追加\"\n  },\n  \"decryptionError\": {\n    \"message\": \"復号化エラー\"\n  },\n  \"downloadFailed\": {\n    \"message\": \"ダウンロード失敗\"\n  },\n  \"retryDownload\": {\n    \"message\": \"再ダウンロード\"\n  },\n  \"recordingDuration\": {\n    \"message\": \"録画時間\"\n  },\n  \"downloaded\": {\n    \"message\": \"ダウンロード済\"\n  },\n  \"downloadedVideoLength\": {\n    \"message\": \"ダウンロード済映像長\"\n  },\n  \"downloadComplete\": {\n    \"message\": \"ダウンロード完了\"\n  },\n  \"retryingDownload\": {\n    \"message\": \"再ダウンロード中\"\n  },\n  \"merging\": {\n    \"message\": \"マージ中\"\n  },\n  \"fileTooLarge\": {\n    \"message\": \"$size$ を超えるためオンラインffmpeg不可 ダウンロードマージ中 大きいファイルは時間がかかります\",\n    \"placeholders\": {\n      \"size\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"fileTooLargeStream\": {\n    \"message\": \"$size$ 超えています ダウンロードしながら保存を有効にしますか？\",\n    \"placeholders\": {\n      \"size\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"formatConversionError\": {\n    \"message\": \"フォーマット変換エラー mp4変換をキャンセルして再ダウンロードしてください\"\n  },\n  \"streamOnbeforeunload\": {\n    \"message\": \"ストリーミング中 閉じるとダウンロード停止…\"\n  },\n  \"fileLoading\": {\n    \"message\": \"ファイル読み込み中\"\n  },\n  \"expandAllNodes\": {\n    \"message\": \"すべてのノード展開\"\n  },\n  \"collapseAllNodes\": {\n    \"message\": \"すべてのノード折りたたみ\"\n  },\n  \"fileRetrievalFailed\": {\n    \"message\": \"ファイル取得失敗\"\n  },\n  \"selectVideo\": {\n    \"message\": \"映像選択\"\n  },\n  \"extractSlices\": {\n    \"message\": \"スライス抽出\"\n  },\n  \"convertToM3U8\": {\n    \"message\": \"m3u8解析へ変換\"\n  },\n  \"selectAudio\": {\n    \"message\": \"音声選択\"\n  },\n  \"audio\": {\n    \"message\": \"音声\"\n  },\n  \"video\": {\n    \"message\": \"映像\"\n  },\n  \"DRMerror\": {\n    \"message\": \"DRM保護付き 他ツールでダウンロードしてください\"\n  },\n  \"regexTitle\": {\n    \"message\": \"正規表現マッチ 深層検索からの取得\"\n  },\n  \"downloadWithRequestHeader\": {\n    \"message\": \"リクエストヘッダー付きダウンロード\"\n  },\n  \"m3u8Playlist\": {\n    \"message\": \"m3u8プレイリスト\"\n  },\n  \"copiedToClipboard\": {\n    \"message\": \"クリップボードにコピー済\"\n  },\n  \"hasSent\": {\n    \"message\": \"送信済\"\n  },\n  \"sendFailed\": {\n    \"message\": \"送信失敗\"\n  },\n  \"confirmDownload\": {\n    \"message\": \"合計 $num$ 個のファイル ダウンロードしますか？\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"confirmLoading\": {\n    \"message\": \"$num$ 個のリソース ロードをキャンセルしますか？\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"waitingForMedia\": {\n    \"message\": \"メディア受信待機中…ページを閉じないでください…\"\n  },\n  \"exit\": {\n    \"message\": \"終了\"\n  },\n  \"unknownSize\": {\n    \"message\": \"サイズ不明\"\n  },\n  \"saving\": {\n    \"message\": \"保存中\"\n  },\n  \"saveFailed\": {\n    \"message\": \"保存失敗\"\n  },\n  \"badgeNumber\": {\n    \"message\": \"アイコンに数字バッジ表示\"\n  },\n  \"viewSlices\": {\n    \"message\": \"すべてのスライスと進捗を表示\"\n  },\n  \"send2local\": {\n    \"message\": \"データ送信\"\n  },\n  \"send2MQTT\": {\n    \"message\": \"MQTTに送信\"\n  },\n  \"sendingToMQTT\": {\n    \"message\": \"MQTTサーバーに送信中...\"\n  },\n  \"connectingToMQTT\": {\n    \"message\": \"MQTTサーバーに接続中...\"\n  },\n  \"sendingMessageToMQTT\": {\n    \"message\": \"MQTTサーバーにメッセージ送信中...\"\n  },\n  \"messageSentToMQTT\": {\n    \"message\": \"MQTTサーバーにメッセージ送信完了\"\n  },\n  \"popup\": {\n    \"message\": \"ポップアップ\"\n  },\n  \"defaultPopup\": {\n    \"message\": \"デフォルトポップアップ\"\n  },\n  \"invokeApp\": {\n    \"message\": \"アプリ呼び出し\"\n  },\n  \"onlineServiceAddress\": {\n    \"message\": \"オンラインサービスアドレス\"\n  },\n  \"withinChina\": {\n    \"message\": \"中国国内\"\n  },\n  \"dataFetchFailed\": {\n    \"message\": \"データ取得失敗\"\n  },\n  \"confirmParameters\": {\n    \"message\": \"パラメーター確認\"\n  },\n  \"searchingForRealKey\": {\n    \"message\": \"リアルキー探索中\"\n  },\n  \"verifying\": {\n    \"message\": \"検証中\"\n  },\n  \"realKeyNotFound\": {\n    \"message\": \"リアルキー未検出\"\n  },\n  \"blockUrl\": {\n    \"message\": \"ブロックURL\"\n  },\n  \"addUrl\": {\n    \"message\": \"URL追加\"\n  },\n  \"wildcards\": {\n    \"message\": \"ワイルドカード\"\n  },\n  \"blockUrlTips\": {\n    \"message\": \"*と?のワイルドカード使用可\"\n  },\n  \"setWhiteList\": {\n    \"message\": \"ホワイトリスト設定\"\n  },\n  \"autoSend\": {\n    \"message\": \"自動送信\"\n  },\n  \"manualSend\": {\n    \"message\": \"手動送信\"\n  },\n  \"requestMethod\": {\n    \"message\": \"リクエストメソッド\"\n  },\n  \"requestBody\": {\n    \"message\": \"リクエストボディ\"\n  },\n  \"sort\": {\n    \"message\": \"ソート\"\n  },\n  \"asc\": {\n    \"message\": \"昇順\"\n  },\n  \"desc\": {\n    \"message\": \"降順\"\n  },\n  \"getTime\": {\n    \"message\": \"取得時間\"\n  },\n  \"fileSize\": {\n    \"message\": \"ファイルサイズ\"\n  },\n  \"title\": {\n    \"message\": \"タイトル\"\n  },\n  \"noKeyIsRequired\": {\n    \"message\": \"キー不要\"\n  },\n  \"estimateSize\": {\n    \"message\": \"推定サイズ\"\n  },\n  \"retryCount\": {\n    \"message\": \"再試行回数\"\n  },\n  \"useSidePanel\": {\n    \"message\": \"サイドパネル使用\"\n  },\n  \"Script\": {\n    \"message\": \"スクリプト\"\n  },\n  \"alwaysSearch\": {\n    \"message\": \"常に深層検索を有効\"\n  },\n  \"deleteDuplicateFilenames\": {\n    \"message\": \"重複ファイル名を削除\"\n  }\n}"
  },
  {
    "path": "_locales/pt_BR/messages.json",
    "content": "{\n  \"catCatch\": {\n    \"message\": \"cat-catch\"\n  },\n  \"description\": {\n    \"message\": \"Ferramenta de detecção de mídia da web\"\n  },\n  \"confirm\": {\n    \"message\": \"Confirmar\"\n  },\n  \"currentPage\": {\n    \"message\": \"Página Atual\"\n  },\n  \"otherPage\": {\n    \"message\": \"Outra Página\"\n  },\n  \"otherFeatures\": {\n    \"message\": \"Outros Recursos\"\n  },\n  \"mediaControl\": {\n    \"message\": \"Controle de Mídia\"\n  },\n  \"loadingData\": {\n    \"message\": \"Carregando Dados...\"\n  },\n  \"selectWebpage\": {\n    \"message\": \"Página da Internet:\"\n  },\n  \"selectMedia\": {\n    \"message\": \"Mídia:\"\n  },\n  \"noMediaDetected\": {\n    \"message\": \"Nenhuma Mídia Detectada na Página da Web\"\n  },\n  \"noControllableMediaDetected\": {\n    \"message\": \"Nenhuma Mídia Controlável Detectada\"\n  },\n  \"multiplier\": {\n    \"message\": \"Multiplicador:\"\n  },\n  \"speedPlayback\": {\n    \"message\": \"Velocidade de Reprodução\"\n  },\n  \"play\": {\n    \"message\": \"Reproduzir\"\n  },\n  \"normalPlay\": {\n    \"message\": \"Reprodução Normal\"\n  },\n  \"pictureInPicture\": {\n    \"message\": \"Imagem em Imagem\"\n  },\n  \"fullscreen\": {\n    \"message\": \"Tela Cheia\"\n  },\n  \"screenshot\": {\n    \"message\": \"Capturar Tela\"\n  },\n  \"loop\": {\n    \"message\": \"Repetir\"\n  },\n  \"mute\": {\n    \"message\": \"Mudo\"\n  },\n  \"volume\": {\n    \"message\": \"Volume\"\n  },\n  \"functionEntry\": {\n    \"message\": \"Entrada de função\"\n  },\n  \"downloader\": {\n    \"message\": \"Downloader\"\n  },\n  \"parser\": {\n    \"message\": \"Analisador\"\n  },\n  \"m3u8Parser\": {\n    \"message\": \"Analisador M3U8\"\n  },\n  \"mpdParser\": {\n    \"message\": \"Analisador MPD\"\n  },\n  \"jsonFormatter\": {\n    \"message\": \"Formatador JSON\"\n  },\n  \"expandAll\": {\n    \"message\": \"Expandir Tudo\"\n  },\n  \"expandPlayable\": {\n    \"message\": \"Expandir Reproduzível\"\n  },\n  \"expandSelected\": {\n    \"message\": \"Expandir Selecionado\"\n  },\n  \"collapseAll\": {\n    \"message\": \"Agrupar Tudo\"\n  },\n  \"videoRecording\": {\n    \"message\": \"Gravar Vídeo\"\n  },\n  \"closeRecording\": {\n    \"message\": \"Fechar Gravação\"\n  },\n  \"recordWebRTC\": {\n    \"message\": \"Gravar WebRTC\"\n  },\n  \"screenCapture\": {\n    \"message\": \"Capturar Tela\"\n  },\n  \"simulateMobile\": {\n    \"message\": \"Simular Celular\"\n  },\n  \"autoDownload\": {\n    \"message\": \"Download Automático\"\n  },\n  \"onlineMerge\": {\n    \"message\": \"Mesclar\"\n  },\n  \"download\": {\n    \"message\": \"Baixar\"\n  },\n  \"copy\": {\n    \"message\": \"Copiar\"\n  },\n  \"selectAll\": {\n    \"message\": \"Selecionar Tudo\"\n  },\n  \"invertSelection\": {\n    \"message\": \"Alternar\"\n  },\n  \"filter\": {\n    \"message\": \"Filtrar\"\n  },\n  \"clear\": {\n    \"message\": \"Limpar\"\n  },\n  \"deepSearch\": {\n    \"message\": \"Buscar\"\n  },\n  \"closeSearch\": {\n    \"message\": \"Fechar Busca\"\n  },\n  \"cacheCapture\": {\n    \"message\": \"Capturar\"\n  },\n  \"closeCapture\": {\n    \"message\": \"Fechar Captura\"\n  },\n  \"moreFeatures\": {\n    \"message\": \"Mais\"\n  },\n  \"pause\": {\n    \"message\": \"Pausar\"\n  },\n  \"settings\": {\n    \"message\": \"Configurações\"\n  },\n  \"closeSimulation\": {\n    \"message\": \"Fechar Simulação\"\n  },\n  \"closeDownload\": {\n    \"message\": \"Fechar Download\"\n  },\n  \"enable\": {\n    \"message\": \"Habilitar\"\n  },\n  \"disable\": {\n    \"message\": \"Desabilitar\"\n  },\n  \"noData\": {\n    \"message\": \"Sem Peixe\"\n  },\n  \"regularFilterPlaceholder\": {\n    \"message\": \"Filtro de expressão regular, corresponde a um recurso URL, pressione Enter para confirmar\"\n  },\n  \"option\": {\n    \"message\": \"Opção\"\n  },\n  \"titleOption\": {\n    \"message\": \"Cat-catch Opção\"\n  },\n  \"titleDownload\": {\n    \"message\": \"Cat-catch Baixar\"\n  },\n  \"titleM3U8\": {\n    \"message\": \"Cat-catch Analisador M3u8\"\n  },\n  \"titleJson\": {\n    \"message\": \"Cat-catch Formatador Json\"\n  },\n  \"titledash\": {\n    \"message\": \"Cat-catch Analisador Dash\"\n  },\n  \"suffix\": {\n    \"message\": \"Sufixo\"\n  },\n  \"suffixTip\": {\n    \"message\": \"Preencha com sufixo que não contém '.', se nenhuma filtragem de tamanho for necessária, preencha com 0.\"\n  },\n  \"extensionName\": {\n    \"message\": \"Nome da Extensão\"\n  },\n  \"filterSize\": {\n    \"message\": \"Filtrar Tamanho\"\n  },\n  \"delete\": {\n    \"message\": \"Excluir\"\n  },\n  \"addSuffix\": {\n    \"message\": \"Adicionar Sufixo\"\n  },\n  \"extension\": {\n    \"message\": \"Extensão\"\n  },\n  \"disableAll\": {\n    \"message\": \"Desabilitar Tudo\"\n  },\n  \"enableAll\": {\n    \"message\": \"Habilitar Tudo\"\n  },\n  \"type\": {\n    \"message\": \"Tipo\"\n  },\n  \"addType\": {\n    \"message\": \"Adicionar Tipo\"\n  },\n  \"typeTip\": {\n    \"message\": \"Inserir o tipo de conteúdo correto; se nenhuma filtragem de tamanho for necessária, preencha com 0.\"\n  },\n  \"addTypeError\": {\n    \"message\": \"O formato do tipo de captura está incorreto, verifique\"\n  },\n  \"regexMatch\": {\n    \"message\": \"Correspondência Regex\"\n  },\n  \"blockResource\": {\n    \"message\": \"Bloquear Recurso\"\n  },\n  \"alert\": {\n    \"message\": \"Alerta\"\n  },\n  \"regexExpression\": {\n    \"message\": \"Expressão Regex\"\n  },\n  \"addRegex\": {\n    \"message\": \"Adicionar Regex\"\n  },\n  \"regexTest\": {\n    \"message\": \"Testar Regex\"\n  },\n  \"regex\": {\n    \"message\": \"regex\"\n  },\n  \"flag\": {\n    \"message\": \"Sinalizador\"\n  },\n  \"result\": {\n    \"message\": \"Resultado\"\n  },\n  \"match\": {\n    \"message\": \"Correspondente\"\n  },\n  \"noMatch\": {\n    \"message\": \"Sem Correspondente\"\n  },\n  \"blockResourceTip\": {\n    \"message\": \"Bloqueia os recursos que você não deseja que apareçam\"\n  },\n  \"flagTip\": {\n    \"message\": \"i: ignorar maiúsculas e minúsculas, g: pesquisa global. Também pode ser deixado em branco\"\n  },\n  \"regexSuffixTip\": {\n    \"message\": \"Atribuir um sufixo a URL obtida. Pode ser deixado em branco e o sufixo será automaticamente truncado (muitos arquivos não possuem sufixo)\"\n  },\n  \"regexTip\": {\n    \"message\": \"Expressões regulares consomem muitos recursos, use-as com cuidado se não for necessário\"\n  },\n  \"copyTip\": {\n    \"message\": \"Para maior comodidade de usar aplicativos de terceiros, personalize o conteúdo gravado na área de transferência pelo botão copiar\"\n  },\n  \"replaceKeywordList\": {\n    \"message\": \"Lista de Substitutos para Palavras-Chaves\"\n  },\n  \"otherFiles\": {\n    \"message\": \"Outros Arquivos\"\n  },\n  \"resetCopySettings\": {\n    \"message\": \"Restaurar Configuração de Copiar\"\n  },\n  \"autoSetRefererCookieParams\": {\n    \"message\": \"Definir Automaticamente Parâmetros de Referência e Cookies\"\n  },\n  \"secretKey\": {\n    \"message\": \"Chave Secreta\"\n  },\n  \"address\": {\n    \"message\": \"Endereço\"\n  },\n  \"documentation\": {\n    \"message\": \"Documentação\"\n  },\n  \"aria2Tip\": {\n    \"message\": \"Uma excelente ferramenta de download, veja como usar\"\n  },\n  \"m3u8DLTips\": {\n    \"message\": \"Uma excelente ferramenta de download de m3u8 e mpd de terceiros, veja como usar\"\n  },\n  \"invoke\": {\n    \"message\": \"Invocar\"\n  },\n  \"parameter\": {\n    \"message\": \"Parâmetros\"\n  },\n  \"parameterSetting\": {\n    \"message\": \"Configuração de Parâmetro\"\n  },\n  \"test\": {\n    \"message\": \"Testar\"\n  },\n  \"replaceTags\": {\n    \"message\": \"Substituir Etiquetas\"\n  },\n  \"customSaveFileName\": {\n    \"message\": \"Personalizar Nome do Arquivo Salvo\"\n  },\n  \"userAgentTip\": {\n    \"message\": \"Padrão para o User Agent do navegador atual\"\n  },\n  \"alwaysDisableCatCatcher\": {\n    \"message\": \"Sempre desativar o Cat-Catch Downloader\"\n  },\n  \"autoClosePageAfterDownload\": {\n    \"message\": \"Fechar Página Automaticamente Após Download\"\n  },\n  \"openDownloaderPageInBackground\": {\n    \"message\": \"Abrir a Página do Downloader em Segundo Plano\"\n  },\n  \"downloaderTip\": {\n    \"message\": \"Se o download do recurso falhar, ative automaticamente o downloader para tentar novamente.\"\n  },\n  \"autoDownM3u8Tip\": {\n    \"message\": \"Clique no botão de download e use o analisador m3u8 para iniciar a mesclagem e o download imediatamente\"\n  },\n  \"otherSettings\": {\n    \"message\": \"Outras Configurações\"\n  },\n  \"resetOtherSettings\": {\n    \"message\": \"Restaurar Outras Configuração\"\n  },\n  \"previewMode\": {\n    \"message\": \"Use o protocolo de chamada do player local para abrir visualizações de vídeo\"\n  },\n  \"previewModePlaceholder\": {\n    \"message\": \"Deixe em branco para desativá-lo. O padrão é usar a página pop-up para visualizar o vídeo\"\n  },\n  \"preview\": {\n    \"message\": \"Prévia\"\n  },\n  \"customFilenameOption\": {\n    \"message\": \"Usar um nome de arquivo personalizado para salvar o arquivo (o padrão é o título da página da web)\"\n  },\n  \"saveAsOption\": {\n    \"message\": \"Escolher o diretório de salvamento após o download\"\n  },\n  \"iconOption\": {\n    \"message\": \"Exibir o ícone do site\"\n  },\n  \"clearOption\": {\n    \"message\": \"Atualize, navegue para uma nova página e limpe os dados capturados pela guia atual\"\n  },\n  \"doNotClear\": {\n    \"message\": \"Não Limpar\"\n  },\n  \"normalClear\": {\n    \"message\": \"Limpeza Normal\"\n  },\n  \"moreFrequent\": {\n    \"message\": \"Mais Freqüente\"\n  },\n  \"dopreview\": {\n    \"message\": \"prévia\"\n  },\n  \"dopopup\": {\n    \"message\": \"balão\"\n  },\n  \"winpreview\": {\n    \"message\": \"janela da prévia\"\n  },\n  \"winpopup\": {\n    \"message\": \"janela do balão\"\n  },   \n  \"excludeDuplicateResources\": {\n    \"message\": \"Excluir recursos duplicados (consome muitos recursos da CPU)\"\n  },\n  \"customCSS\": {\n    \"message\": \"CSS Personalizado\"\n  },\n  \"MQTT\": {\n    \"message\": \"MQTT\"\n  },\n  \"mqttBroker\": {\n    \"message\": \"Endereço do Broker MQTT\"\n  },\n  \"mqttPath\": {\n    \"message\": \"Caminho\"\n  },\n  \"mqttProtocol\": {\n    \"message\": \"Protocolo\"\n  },\n  \"mqttClientId\": {\n    \"message\": \"ID do Cliente\"\n  },\n  \"mqttTitleLength\": {\n    \"message\": \"Tamanho Máximo do Título\"\n  },\n  \"mqttUsername\": {\n    \"message\": \"Nome de Usuário\"\n  },\n  \"mqttPassword\": {\n    \"message\": \"Senha\"\n  },\n  \"mqttTopic\": {\n    \"message\": \"Tópico\"\n  },\n  \"mqttQos\": {\n    \"message\": \"Nível de QoS\"\n  },\n  \"mqttQos0\": {\n    \"message\": \"(No Máximo Uma Vez)\"\n  },\n  \"mqttQos1\": {\n    \"message\": \"(Pelo Menos Uma Vez)\"\n  },\n  \"mqttQos2\": {\n    \"message\": \"(Exatamente Uma Vez)\"\n  },\n  \"mqttDataFormat\": {\n    \"message\": \"Formato dos Dados\"\n  },\n  \"mqttDataFormatHelp\": {\n    \"message\": \"{\\\"url\\\": \\\"$${url}\\\", \\\"title\\\": \\\"$${title}\\\", \\\"type\\\": \\\"$${type}\\\", \\\"ext\\\": \\\"$${ext}\\\", \\\"timestamp\\\": \\\"$${timestamp}\\\"}\"\n  },\n  \"mqttDataFormatVars\": {\n    \"message\": \"Variáveis disponíveis\"\n  },\n  \"mqttDataFormatDefault\": {\n    \"message\": \"Deixe em branco para usar o formato JSON padrão\"\n  },\n  \"mqttProtocolWss\": {\n    \"message\": \"WSS (Seguro)\"\n  },\n  \"mqttProtocolWs\": {\n    \"message\": \"WS (Não Seguro)\"\n  },\n  \"mqttTitleLengthHelp\": {\n    \"message\": \"Comprimento máximo do título nas mensagens MQTT\"\n  },\n  \"mqttBrokerHelp\": {\n    \"message\": \"Endereço do host ou IP do broker MQTT\"\n  },\n  \"mqttPathHelp\": {\n    \"message\": \"Caminho do WebSocket (geralmente /mqtt ou /ws)\"\n  },\n  \"mqttClientIdHelp\": {\n    \"message\": \"Identificador único do cliente para esta conexão\"\n  },\n  \"mqttTopicHelp\": {\n    \"message\": \"Tópico MQTT para publicar as mensagens\"\n  },\n  \"mqttQosHelp\": {\n    \"message\": \"Nível de Qualidade de Serviço (0=No Máximo Uma Vez, 1=Pelo Menos Uma Vez, 2=Exatamente Uma Vez)\"\n  },\n  \"mqttCredentialsHelp\": {\n    \"message\": \"Deixe em branco se não for necessária autenticação\"\n  },\n  \"operation\": {\n    \"message\": \"Operação\"\n  },\n  \"exportSettings\": {\n    \"message\": \"Exportar Configurações\"\n  },\n  \"importConfiguration\": {\n    \"message\": \"Importar Configurações\"\n  },\n  \"clearCapturedData\": {\n    \"message\": \"Limpar Dados Capturados\"\n  },\n  \"resetSettings\": {\n    \"message\": \"Restaurar Configurações\"\n  },\n  \"resetAllSettings\": {\n    \"message\": \"Restaurar Todas Configurações\"\n  },\n  \"restartExtension\": {\n    \"message\": \"Reiniciar Extensão\"\n  },\n  \"about\": {\n    \"message\": \"Sobre\"\n  },\n  \"confirmReset\": {\n    \"message\": \"Tem certeza de que deseja redefinir?\"\n  },\n  \"invokeProtocolTemplate\": {\n    \"message\": \"Invocar Modelo do Protocolo\"\n  },\n  \"customVLCProtocol\": {\n    \"message\": \"Protocolo VLC Personalizado\"\n  },\n  \"systemShare\": {\n    \"message\": \"Compartilhamento do Sistema\"\n  },\n  \"default\": {\n    \"message\": \"Padrão\"\n  },\n  \"goBack\": {\n    \"message\": \"Voltar\"\n  },\n  \"openDir\": {\n    \"message\": \"Abrir Diretório\"\n  },\n  \"downloadDir\": {\n    \"message\": \"Baixar Diretório\"\n  },\n  \"sendFfmpeg\": {\n    \"message\": \"Enviar para ffmpeg Online\"\n  },\n  \"autoCloserDownload\": {\n    \"message\": \"Fechar Página Automaticamente Após Download\"\n  },\n  \"openInBgDownload\": {\n    \"message\": \"Abrir a Página do Downloader em Segundo Plano\"\n  },\n  \"m3u8Placeholder\": {\n    \"message\": \"Insira o conteúdo do m3u8 ou a lista de fragmentos ts.\"\n  },\n  \"m3u8Url\": {\n    \"message\": \"URL do m3u8\"\n  },\n  \"nextLevel\": {\n    \"message\": \"Próximo Nível\"\n  },\n  \"nextLevelTip\": {\n    \"message\": \"Este arquivo M3U8 alinha vários arquivos M3U8.\"\n  },\n  \"multipleAudios\": {\n    \"message\": \"Múltiplos Áudios\"\n  },\n  \"multipleAudiosTip\": {\n    \"message\": \"Este arquivo M3U8 alinha múltiplos áudios\"\n  },\n  \"multipleSubtitles\": {\n    \"message\": \"Múltiplas Legendas\"\n  },\n  \"multipleSubtitlesTip\": {\n    \"message\": \"Este arquivo M3U8 alinha múltiplas legendas.\"\n  },\n  \"possibleKey\": {\n    \"message\": \"Chaves possíveis encontradas\"\n  },\n  \"loading\": {\n    \"message\": \"Carregando...\"\n  },\n  \"waitDownload\": {\n    \"message\": \"Aguardando download...\"\n  },\n  \"downloadSegmentList\": {\n    \"message\": \"Lista de download\"\n  },\n  \"originalM3u8\": {\n    \"message\": \"M3U8 Original\"\n  },\n  \"localM3u8\": {\n    \"message\": \"M3U8 Local\"\n  },\n  \"segmentList\": {\n    \"message\": \"Segmento\"\n  },\n  \"downloadProgress\": {\n    \"message\": \"Progresso do Download\"\n  },\n  \"getParameters\": {\n    \"message\": \"Parâmetros GET\"\n  },\n  \"restoreGetParameters\": {\n    \"message\": \"Restaurar Parâmetros GET\"\n  },\n  \"requestHeaders\": {\n    \"message\": \"Solicitar Cabeçalhos\"\n  },\n  \"setRequestHeaders\": {\n    \"message\": \"Configurar solicitação cabeçalhos.\"\n  },\n  \"invokeM3u8DL\": {\n    \"message\": \"Invocar M3U8DL\"\n  },\n  \"copyCommand\": {\n    \"message\": \"Copiar Comando\"\n  },\n  \"previewCommand\": {\n    \"message\": \"Prévia do Comando\"\n  },\n  \"addSettingParameters\": {\n    \"message\": \"Adicionar Parâmetros de Configuração\"\n  },\n  \"customKeyPlaceholder\": {\n    \"message\": \"Personalizar a chave em hexadecimal ou base64, ou o endereço da chave\"\n  },\n  \"uploadKey\": {\n    \"message\": \"Chave de upload\"\n  },\n  \"downloadThreads\": {\n    \"message\": \"Processos\"\n  },\n  \"ffmpegTranscoding\": {\n    \"message\": \"Transcodificação FFmpeg\"\n  },\n  \"mp4Format\": {\n    \"message\": \"MP4\"\n  },\n  \"downloadWhileSaving\": {\n    \"message\": \"Baixar Stream\"\n  },\n  \"audioOnly\": {\n    \"message\": \"Somente Áudio\"\n  },\n  \"saveAs\": {\n    \"message\": \"Salvar Como\"\n  },\n  \"skipDecryption\": {\n    \"message\": \"Pular Descriptografia\"\n  },\n  \"newDownloader\": {\n    \"message\": \"Novo Downloader\"\n  },\n  \"downloadRange\": {\n    \"message\": \"Faixa de Download\"\n  },\n  \"recordLive\": {\n    \"message\": \"Gravar\"\n  },\n  \"mergeDownloads\": {\n    \"message\": \"Mesclar Downloads\"\n  },\n  \"redownloadFailedItems\": {\n    \"message\": \"Baixar novamente Itens com Falha\"\n  },\n  \"downloadExistingData\": {\n    \"message\": \"Baixar Dados Existentes\"\n  },\n  \"stopDownload\": {\n    \"message\": \"Parar Download\"\n  },\n  \"start\": {\n    \"message\": \"Começar\"\n  },\n  \"end\": {\n    \"message\": \"Terminar\"\n  },\n  \"resolution\": {\n    \"message\": \"Resolução\"\n  },\n  \"duration\": {\n    \"message\": \"Duração\"\n  },\n  \"bitrate\": {\n    \"message\": \"Taxa de bits\"\n  },\n  \"ADTSerror\": {\n    \"message\": \"Não foi possível localizar o cabeçalho ADTS. Pode ser um recurso criptografado AES-128-ECB, que atualmente não é compatível com descriptografia. Usar software de mesclagem de terceiros.\"\n  },\n  \"m3u8Error\": {\n    \"message\": \"Há erros na análise ou reprodução do arquivo M3U8. Verifique o console para obter informações detalhadas sobre erros\"\n  },\n  \"noAudio\": {\n    \"message\": \"Sem Áudio\"\n  },\n  \"noVideo\": {\n    \"message\": \"Sem Vídeo\"\n  },\n  \"hevcTip\": {\n    \"message\": \"Arquivos de fragmentos codificados HEVC/H.265 são suportados apenas para transcodificação ffmpeg online\"\n  },\n  \"hevcPreviewTip\": {\n    \"message\": \"Arquivos de fragmentos codificados HEVC/H.265 não são suportados para visualização.\"\n  },\n  \"m3u8Info\": {\n    \"message\": \"Um total de $num$ arquivo(s), com duração total de $time$.\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      },\n      \"time\": {\n        \"content\": \"$2\"\n      }\n    }\n  },\n  \"encryptedHLS\": {\n    \"message\": \"HLS Criptografado\"\n  },\n  \"encryptedSAMPLE\": {\n    \"message\": \"Os recursos criptografados com SAMPLE-AES-CTR não podem ser manipulados no momento.\"\n  },\n  \"liveHLS\": {\n    \"message\": \"HLS Ao Vivo\"\n  },\n  \"keyAddress\": {\n    \"message\": \"Endereço Chave\"\n  },\n  \"key\": {\n    \"message\": \"Chave\"\n  },\n  \"encryptionAlgorithm\": {\n    \"message\": \"Método\"\n  },\n  \"keyDownloadFailed\": {\n    \"message\": \"Falha no Download da Chave\"\n  },\n  \"savePrompt\": {\n    \"message\": \"Salvo no disco, verifique o conteúdo baixado no navegador.\"\n  },\n  \"close\": {\n    \"message\": \"Fechar\"\n  },\n  \"blobM3u8DLError\": {\n    \"message\": \"URLs de blob não podem invocar M3U8DL para download\"\n  },\n  \"M3U8DLparameterLong\": {\n    \"message\": \"O parâmetro M3U8DL é muito longo.\"\n  },\n  \"runningCannotChangeSettings\": {\n    \"message\": \"Em execução, não é possível alterar as configurações\"\n  },\n  \"streamSaverTip\": {\n    \"message\": \"A função de 'baixar enquanto salva' não oferece suporte à conversão de formato online ffmpeg, não suporta download novamente de fatias erradas e não suporta 'salvar como'.\"\n  },\n  \"stopRecording\": {\n    \"message\": \"Parar de Gravação\"\n  },\n  \"waitingForLiveData\": {\n    \"message\": \"Esperando por Dados do Ao Vivo\"\n  },\n  \"sNumError\": {\n    \"message\": \"Erro no número de série\"\n  },\n  \"startGTend\": {\n    \"message\": \"O número inicial não pode ser maior que o número final\"\n  },\n  \"sNumMax\": {\n    \"message\": \"O número de série não pode exceder $num$\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"incorrectKey\": {\n    \"message\": \"Chave Incorreta\"\n  },\n  \"addParameters\": {\n    \"message\": \"Adicionar Parâmetros\"\n  },\n  \"decryptionError\": {\n    \"message\": \"Erro de Descriptografia\"\n  },\n  \"downloadFailed\": {\n    \"message\": \"Falha no Download\"\n  },\n  \"retryDownload\": {\n    \"message\": \"Tentar Baixar Novamente\"\n  },\n  \"recordingDuration\": {\n    \"message\": \"Duração da Gravação\"\n  },\n  \"downloaded\": {\n    \"message\": \"Baixado\"\n  },\n  \"downloadedVideoLength\": {\n    \"message\": \"Duração do Vídeo Baixado\"\n  },\n  \"downloadComplete\": {\n    \"message\": \"Download Completo\"\n  },\n  \"retryingDownload\": {\n    \"message\": \"Tentar Baixar Novamente\"\n  },\n  \"merging\": {\n    \"message\": \"Mesclando\"\n  },\n  \"fileTooLarge\": {\n    \"message\": \"Arquivo muito grande, arquivo maior que $size$\",\n    \"placeholders\": {\n      \"size\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"fileTooLargeStream\": {\n    \"message\": \"Arquivo maior que $size$, habilitar download da stream?\",\n    \"placeholders\": {\n      \"size\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"formatConversionError\": {\n    \"message\": \"Erro de Conversão de Formato\"\n  },\n  \"streamOnbeforeunload\": {\n    \"message\": \"A transmissão está em andamento, o download será interrompido após o fechamento\"\n  },\n  \"fileLoading\": {\n    \"message\": \"Carregamento de Arquivo\"\n  },\n  \"expandAllNodes\": {\n    \"message\": \"Expandir todos os nós JSON\"\n  },\n  \"collapseAllNodes\": {\n    \"message\": \"Agrupar todos os nós JSON\"\n  },\n  \"fileRetrievalFailed\": {\n    \"message\": \"Falha ao Salvar Arquivo\"\n  },\n  \"selectVideo\": {\n    \"message\": \"Selecionar Vídeo\"\n  },\n  \"extractSlices\": {\n    \"message\": \"Extrair Pedaços\"\n  },\n  \"convertToM3U8\": {\n    \"message\": \"Converter para Análise M3U8\"\n  },\n  \"selectAudio\": {\n    \"message\": \"Selecionar Áudio\"\n  },\n  \"audio\": {\n    \"message\": \"Áudio\"\n  },\n  \"video\": {\n    \"message\": \"Vídeo\"\n  },\n  \"DRMerror\": {\n    \"message\": \"A mídia possui proteção DRM, use ferramentas de terceiros para download\"\n  },\n  \"regexTitle\": {\n    \"message\": \"Correspondência de expressão regular ou de pesquisa profunda\"\n  },\n  \"downloadWithRequestHeader\": {\n    \"message\": \"Baixar com parâmetros de cabeçalho de solicitação.\"\n  },\n  \"m3u8Playlist\": {\n    \"message\": \"Lista de Reprodução M3U8\"\n  },\n  \"copiedToClipboard\": {\n    \"message\": \"Copiar para Área de Transferência\"\n  },\n  \"hasSent\": {\n    \"message\": \"Enviado\"\n  },\n  \"sendFailed\": {\n    \"message\": \"Envio Falhou\"\n  },\n  \"confirmDownload\": {\n    \"message\": \"$num$ arquivos no total, confirmar download?\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"confirmLoading\": {\n    \"message\": \"Existem $num$ recursos no total, deseja cancelar o carregamento?\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"waitingForMedia\": {\n    \"message\": \"Aguardando receber arquivos de mídia... Por favor, não feche esta página.\"\n  },\n  \"exit\": {\n    \"message\": \"Sair\"\n  },\n  \"unknownSize\": {\n    \"message\": \"Tamanho Desconhecido\"\n  },\n  \"saving\": {\n    \"message\": \"Salvando\"\n  },\n  \"saveFailed\": {\n    \"message\": \"Falha ao salvar\"\n  },\n  \"badgeNumber\": {\n    \"message\": \"Mostrar prompt do emblema do ícone\"\n  },\n  \"viewSlices\": {\n    \"message\": \"Exibir todas as fatias e progresso do download\"\n  },\n  \"send2local\": {\n    \"message\": \"Transmissão de dados\"\n  },\n  \"send2MQTT\": {\n    \"message\": \"Enviar para MQTT\"\n  },\n  \"sendingToMQTT\": {\n    \"message\": \"Enviando para servidor MQTT...\"\n  },\n  \"connectingToMQTT\": {\n    \"message\": \"Conectando ao servidor MQTT...\"\n  },\n  \"sendingMessageToMQTT\": {\n    \"message\": \"Enviando mensagem para servidor MQTT...\"\n  },\n  \"messageSentToMQTT\": {\n    \"message\": \"Mensagem enviada para servidor MQTT\"\n  },\n  \"popup\": {\n    \"message\": \"Balão\"\n  },\n  \"defaultPopup\": {\n    \"message\": \"Modo Balão Padrão\"\n  },\n  \"invokeApp\": {\n    \"message\": \"Invocar aplicativo\"\n  },\n  \"onlineServiceAddress\": {\n    \"message\": \"Endereço do Serviço Online\"\n  },\n  \"withinChina\": {\n    \"message\": \"Interior da China\"\n  },\n  \"dataFetchFailed\": {\n    \"message\": \"Falha na busca de dados\"\n  },\n  \"confirmParameters\": {\n    \"message\": \"Confirmar Parâmetros\"\n  },\n  \"searchingForRealKey\": {\n    \"message\": \"Procurando pela chave real\"\n  },\n  \"verifying\": {\n    \"message\": \"Verificando\"\n  },\n  \"realKeyNotFound\": {\n    \"message\": \"Chave real não encontrada\"\n  },\n  \"blockUrl\": {\n    \"message\": \"Bloquear URL\"\n  },\n  \"addUrl\": {\n    \"message\": \"Adicionar URL\"\n  },\n  \"wildcards\": {\n    \"message\": \"curingas\"\n  },\n  \"blockUrlTips\": {\n    \"message\": \"Suportar curingas * e ?\"\n  },\n  \"setWhiteList\": {\n    \"message\": \"Definir para lista de permissões\"\n  },\n  \"autoSend\": {\n    \"message\": \"Transmissão manual de dados\"\n  },\n  \"manualSend\": {\n    \"message\": \"Transmissão automática de dados\"\n  },\n  \"requestMethod\": {\n    \"message\": \"Método do Pedido\"\n  },\n  \"requestBody\": {\n    \"message\": \"Corpo do Pedido\"\n  },\n  \"sort\": {\n    \"message\": \"Ordenar\"\n  },\n  \"asc\": {\n    \"message\": \"Ascendentemente\"\n  },\n  \"desc\": {\n    \"message\": \"Descendentemente\"\n  },\n  \"getTime\": {\n    \"message\": \"Tempo de Recuperação\"\n  },\n  \"fileSize\": {\n    \"message\": \"Tamanho do Arquivo\"\n  },\n  \"title\": {\n    \"message\": \"Título\"\n  },\n  \"noKeyIsRequired\": {\n    \"message\": \"Nenhuma chave é necessária\"\n  },\n  \"estimateSize\": {\n    \"message\": \"Tamanho estimado\"\n  },\n  \"retryCount\": {\n    \"message\": \"Número de tentativas\"\n  },\n  \"useSidePanel\": {\n    \"message\": \"Usar painel lateral\"\n  },\n  \"Script\": {\n    \"message\": \"Script\"\n  },\n  \"alwaysSearch\": {\n    \"message\": \"Sempre habilitar pesquisa profunda\"\n  },\n  \"sideurlprotocol\": {\n  \t\"message\": \"Protocolo URL m3u8dl\"\n  },\n  \"deleteDuplicateFilenames\": {\n    \"message\": \"Excluir nomes de arquivos duplicados\"\n  },\n  \"alertimport\": {\n    \"message\": \"Importação concluída\"\n  },\n  \"isBlockedSite\": {\n    \"message\": \"Este site exige bloquear a operação desta extensão\"\n  }\n}\n"
  },
  {
    "path": "_locales/tr/messages.json",
    "content": "{\n  \"catCatch\": {\n    \"message\": \"cat-catch\"\n  },\n  \"description\": {\n    \"message\": \"Web medya sniffer aracı\"\n  },\n  \"confirm\": {\n    \"message\": \"Onayla\"\n  },\n  \"currentPage\": {\n    \"message\": \"Mevcut Sayfa\"\n  },\n  \"otherPage\": {\n    \"message\": \"Diğer Sayfa\"\n  },\n  \"otherFeatures\": {\n    \"message\": \"Diğer Özellikler\"\n  },\n  \"mediaControl\": {\n    \"message\": \"Medya Kontrolü\"\n  },\n  \"loadingData\": {\n    \"message\": \"Veriler Yükleniyor...\"\n  },\n  \"selectWebpage\": {\n    \"message\": \"Web Sayfası:\"\n  },\n  \"selectMedia\": {\n    \"message\": \"Medya:\"\n  },\n  \"noMediaDetected\": {\n    \"message\": \"Web Sayfasında Medya Algılanmadı\"\n  },\n  \"noControllableMediaDetected\": {\n    \"message\": \"Kontrol Edilebilir Medya Algılanmadı\"\n  },\n  \"multiplier\": {\n    \"message\": \"Çarpan:\"\n  },\n  \"speedPlayback\": {\n    \"message\": \"Oynatma Hızı\"\n  },\n  \"play\": {\n    \"message\": \"Oynat\"\n  },\n  \"normalPlay\": {\n    \"message\": \"Normal Oynatma\"\n  },\n  \"pictureInPicture\": {\n    \"message\": \"Resimde Resim\"\n  },\n  \"fullscreen\": {\n    \"message\": \"Tam Ekran\"\n  },\n  \"screenshot\": {\n    \"message\": \"Ekran Görüntüsü\"\n  },\n  \"loop\": {\n    \"message\": \"Döngü\"\n  },\n  \"mute\": {\n    \"message\": \"Sessize Al\"\n  },\n  \"volume\": {\n    \"message\": \"Ses Düzeyi\"\n  },\n  \"functionEntry\": {\n    \"message\": \"İşlev Girişi\"\n  },\n  \"downloader\": {\n    \"message\": \"İndirici\"\n  },\n  \"parser\": {\n    \"message\": \"Ayrıştırıcı\"\n  },\n  \"m3u8Parser\": {\n    \"message\": \"M3U8 Ayrıştırıcısı\"\n  },\n  \"mpdParser\": {\n    \"message\": \"MPD Ayrıştırıcısı\"\n  },\n  \"jsonFormatter\": {\n    \"message\": \"JSON Biçimlendirici\"\n  },\n  \"expandAll\": {\n    \"message\": \"Tümünü Genişlet\"\n  },\n  \"expandPlayable\": {\n    \"message\": \"Oynatılabilir Olanları Genişlet\"\n  },\n  \"expandSelected\": {\n    \"message\": \"Seçilenleri Genişlet\"\n  },\n  \"collapseAll\": {\n    \"message\": \"Tümünü Daralt\"\n  },\n  \"videoRecording\": {\n    \"message\": \"Video Kaydı\"\n  },\n  \"closeRecording\": {\n    \"message\": \"Kaydı Kapat\"\n  },\n  \"recordWebRTC\": {\n    \"message\": \"WebRTC Kaydet\"\n  },\n  \"screenCapture\": {\n    \"message\": \"Ekran Yakalama\"\n  },\n  \"simulateMobile\": {\n    \"message\": \"Mobil Cihazı Simüle Et\"\n  },\n  \"autoDownload\": {\n    \"message\": \"Otomatik İndir\"\n  },\n  \"onlineMerge\": {\n    \"message\": \"Birleştir\"\n  },\n  \"download\": {\n    \"message\": \"İndir\"\n  },\n  \"copy\": {\n    \"message\": \"Kopyala\"\n  },\n  \"selectAll\": {\n    \"message\": \"Tümünü Seç\"\n  },\n  \"invertSelection\": {\n    \"message\": \"Seçimi Tersine Çevir\"\n  },\n  \"filter\": {\n    \"message\": \"Filtre\"\n  },\n  \"clear\": {\n    \"message\": \"Temizle\"\n  },\n  \"deepSearch\": {\n    \"message\": \"Ara\"\n  },\n  \"closeSearch\": {\n    \"message\": \"Aramayı Kapat\"\n  },\n  \"cacheCapture\": {\n    \"message\": \"Yakala\"\n  },\n  \"closeCapture\": {\n    \"message\": \"Yakalamayı Kapat\"\n  },\n  \"moreFeatures\": {\n    \"message\": \"Daha Fazla\"\n  },\n  \"pause\": {\n    \"message\": \"Duraklat\"\n  },\n  \"settings\": {\n    \"message\": \"Ayarlar\"\n  },\n  \"closeSimulation\": {\n    \"message\": \"Simülasyonu Kapat\"\n  },\n  \"closeDownload\": {\n    \"message\": \"İndirmeyi Kapat\"\n  },\n  \"enable\": {\n    \"message\": \"Etkinleştir\"\n  },\n  \"disable\": {\n    \"message\": \"Devre Dışı Bırak\"\n  },\n  \"noData\": {\n    \"message\": \"Balık Yok\"\n  },\n  \"regularFilterPlaceholder\": {\n    \"message\": \"Düzenli ifade filtresi, kaynak URL ile eşleş, onaylamak için Enter tuşuna bas\"\n  },\n  \"option\": {\n    \"message\": \"Seçenek\"\n  },\n  \"titleOption\": {\n    \"message\": \"cat-catch Seçeneği\"\n  },\n  \"titleDownload\": {\n    \"message\": \"cat-catch İndir\"\n  },\n  \"titleM3U8\": {\n    \"message\": \"cat-catch m3u8 Ayrıştırıcısı\"\n  },\n  \"titleJson\": {\n    \"message\": \"cat-catch json biçimlendirici\"\n  },\n  \"titledash\": {\n    \"message\": \"cat-catch Dash Ayrıştırıcısı\"\n  },\n  \"suffix\": {\n    \"message\": \"Eki\"\n  },\n  \"suffixTip\": {\n    \"message\": \"'.' içermeyen eki doldurun, boyut filtrelemeye gerek yoksa 0 doldurun.\"\n  },\n  \"extensionName\": {\n    \"message\": \"Uzantı Adı\"\n  },\n  \"filterSize\": {\n    \"message\": \"Boyut Filtresi\"\n  },\n  \"delete\": {\n    \"message\": \"Sil\"\n  },\n  \"addSuffix\": {\n    \"message\": \"Ek Ekle\"\n  },\n  \"extension\": {\n    \"message\": \"Uzantı\"\n  },\n  \"disableAll\": {\n    \"message\": \"Tümünü Devre Dışı Bırak\"\n  },\n  \"enableAll\": {\n    \"message\": \"Tümünü Etkinleştir\"\n  },\n  \"type\": {\n    \"message\": \"Tür\"\n  },\n  \"addType\": {\n    \"message\": \"Tür Ekle\"\n  },\n  \"typeTip\": {\n    \"message\": \"Doğru içerik türünü girin, boyut filtrelemeye gerek yoksa 0 doldurun.\"\n  },\n  \"addTypeError\": {\n    \"message\": \"Yakalama türünün biçimi yanlış, lütfen kontrol edin\"\n  },\n  \"regexMatch\": {\n    \"message\": \"Düzenli İfade Eşleşmesi\"\n  },\n  \"blockResource\": {\n    \"message\": \"Kaynağı Engelle\"\n  },\n  \"alert\": {\n    \"message\": \"Uyarı\"\n  },\n  \"regexExpression\": {\n    \"message\": \"Düzenli İfade\"\n  },\n  \"addRegex\": {\n    \"message\": \"Düzenli İfade Ekle\"\n  },\n  \"regexTest\": {\n    \"message\": \"Düzenli ifade testi\"\n  },\n  \"regex\": {\n    \"message\": \"düzenli ifade\"\n  },\n  \"flag\": {\n    \"message\": \"Bayrak\"\n  },\n  \"result\": {\n    \"message\": \"Sonuç\"\n  },\n  \"match\": {\n    \"message\": \"Eşleş\"\n  },\n  \"noMatch\": {\n    \"message\": \"Eşleşme Yok\"\n  },\n  \"blockResourceTip\": {\n    \"message\": \"Görünmesini istemediğiniz kaynakları engelleyin\"\n  },\n  \"flagTip\": {\n    \"message\": \"i: büyük/küçük harfe duyarlı olmayan, g: global arama. Boş da bırakılabilir\"\n  },\n  \"regexSuffixTip\": {\n    \"message\": \"Elde edilen URL'ye bir ek atayın. Boş bırakılabilir ve ek otomatik olarak kesilecektir (birçok dosyanın eki yoktur)\"\n  },\n  \"regexTip\": {\n    \"message\": \"Düzenli ifadeler çok kaynak tüketir, gerekmedikçe dikkatli kullanın\"\n  },\n  \"copyTip\": {\n    \"message\": \"Üçüncü taraf uygulamalar kullanmanın kolaylığı için, kopya düğmesi tarafından panoya yazılan içeriği özelleştirin\"\n  },\n  \"replaceKeywordList\": {\n    \"message\": \"Anahtar Kelime Listesini Değiştir\"\n  },\n  \"otherFiles\": {\n    \"message\": \"Diğer Dosyalar\"\n  },\n  \"resetCopySettings\": {\n    \"message\": \"Kopya Ayarlarını Sıfırla\"\n  },\n  \"autoSetRefererCookieParams\": {\n    \"message\": \"Referrer ve Cookie Parametrelerini Otomatik Ayarla\"\n  },\n  \"secretKey\": {\n    \"message\": \"Gizli Anahtar\"\n  },\n  \"address\": {\n    \"message\": \"Adres\"\n  },\n  \"documentation\": {\n    \"message\": \"Belgeler\"\n  },\n  \"aria2Tip\": {\n    \"message\": \"Mükemmel bir indirme aracı, kullanımı öğrenin\"\n  },\n  \"m3u8DLTips\": {\n    \"message\": \"Mükemmel bir üçüncü taraf m3u8 ve mpd indirme aracı, kullanımı öğrenin\"\n  },\n  \"invoke\": {\n    \"message\": \"Çağır\"\n  },\n  \"parameter\": {\n    \"message\": \"Parametre\"\n  },\n  \"parameterSetting\": {\n    \"message\": \"Parametre Ayarı\"\n  },\n  \"test\": {\n    \"message\": \"Test\"\n  },\n  \"replaceTags\": {\n    \"message\": \"Etiketleri Değiştir\"\n  },\n  \"customSaveFileName\": {\n    \"message\": \"Özel Kaydet Dosya Adı\"\n  },\n  \"userAgentTip\": {\n    \"message\": \"Varsayılan olarak geçerli tarayıcının Kullanıcı Aracısı\"\n  },\n  \"alwaysDisableCatCatcher\": {\n    \"message\": \"Her Zaman Cat-Catch İndiriciyi Devre Dışı Bırak\"\n  },\n  \"autoClosePageAfterDownload\": {\n    \"message\": \"İndirmeden Sonra Sayfayı Otomatik Kapat\"\n  },\n  \"openDownloaderPageInBackground\": {\n    \"message\": \"İndirici Sayfasını Arka Planda Aç\"\n  },\n  \"downloaderTip\": {\n    \"message\": \"Kaynak indirmesi başarısız olursa, indiriciyi otomatik olarak etkinleştirerek tekrar deneyin.\"\n  },\n  \"autoDownM3u8Tip\": {\n    \"message\": \"İndir düğmesine tıklayın ve m3u8 ayrıştırıcısını kullanarak hemen birleştirmeyi ve indirmeyi başlatın\"\n  },\n  \"otherSettings\": {\n    \"message\": \"Diğer Ayarlar\"\n  },\n  \"resetOtherSettings\": {\n    \"message\": \"Diğer Ayarları Sıfırla\"\n  },\n  \"previewMode\": {\n    \"message\": \"Yerel oynatıcının çağrı protokolünü kullanarak video önizlemesini aç\"\n  },\n  \"previewModePlaceholder\": {\n    \"message\": \"Devre dışı bırakmak için boş bırakın. Varsayılan olarak video önizlemesi için açılır sayfayı kullanın\"\n  },\n  \"preview\": {\n    \"message\": \"Önizleme\"\n  },\n  \"customFilenameOption\": {\n    \"message\": \"Dosyayı kaydetmek için özel dosya adı kullanın (varsayılan web sayfası başlığıdır)\"\n  },\n  \"saveAsOption\": {\n    \"message\": \"İndirmeden sonra kaydet dizinini seçin\"\n  },\n  \"iconOption\": {\n    \"message\": \"Web sitesi simgesini görüntüle\"\n  },\n  \"clearOption\": {\n    \"message\": \"Yenile, yeni bir sayfaya git ve mevcut sekmede yakalanan verileri temizle\"\n  },\n  \"doNotClear\": {\n    \"message\": \"Temizleme\"\n  },\n  \"normalClear\": {\n    \"message\": \"Normal Temizle\"\n  },\n  \"moreFrequent\": {\n    \"message\": \"Daha Sık\"\n  },\n  \"dopreview\": {\n    \"message\": \"preview\"\n  },\n  \"dopopup\": {\n    \"message\": \"popup\"\n  },\n  \"winpreview\": {\n    \"message\": \"window preview\"\n  },\n  \"winpopup\": {\n    \"message\": \"window popup\"\n  },   \n  \"excludeDuplicateResources\": {\n    \"message\": \"Yinelenen kaynakları hariç tut (çok fazla kaynak çok fazla CPU tüketecektir)\"\n  },\n  \"customCSS\": {\n    \"message\": \"Özel CSS\"\n  },\n  \"MQTT\": {\n    \"message\": \"MQTT\"\n  },\n  \"mqttBroker\": {\n    \"message\": \"Broker adresi\"\n  },\n  \"mqttPath\": {\n    \"message\": \"Yol\"\n  },\n  \"mqttProtocol\": {\n    \"message\": \"Protokol\"\n  },\n  \"mqttClientId\": {\n    \"message\": \"İstemci Kimliği\"\n  },\n  \"mqttTitleLength\": {\n    \"message\": \"Başlık Maksimum Uzunluğu\"\n  },\n  \"mqttUsername\": {\n    \"message\": \"Kullanıcı Adı\"\n  },\n  \"mqttPassword\": {\n    \"message\": \"Şifre\"\n  },\n  \"mqttTopic\": {\n    \"message\": \"Başlık\"\n  },\n  \"mqttQos\": {\n    \"message\": \"QoS Düzeyi\"\n  },\n  \"mqttQos0\": {\n    \"message\": \"(En çok bir kez)\"\n  },\n  \"mqttQos1\": {\n    \"message\": \"(En az bir kez)\"\n  },\n  \"mqttQos2\": {\n    \"message\": \"(Tam olarak bir kez)\"\n  },\n  \"mqttDataFormat\": {\n    \"message\": \"Veri Biçimi\"\n  },\n  \"mqttDataFormatHelp\": {\n    \"message\": \"{\\\"url\\\": \\\"$${url}\\\", \\\"title\\\": \\\"$${title}\\\", \\\"type\\\": \\\"$${type}\\\", \\\"ext\\\": \\\"$${ext}\\\", \\\"timestamp\\\": \\\"$${timestamp}\\\"}\"\n  },\n  \"mqttDataFormatVars\": {\n    \"message\": \"Mevcut değişkenler\"\n  },\n  \"mqttDataFormatDefault\": {\n    \"message\": \"Varsayılan JSON biçimini kullanmak için boş bırakın\"\n  },\n  \"mqttProtocolWss\": {\n    \"message\": \"WSS (Güvenli)\"\n  },\n  \"mqttProtocolWs\": {\n    \"message\": \"WS (Güvensiz)\"\n  },\n  \"mqttTitleLengthHelp\": {\n    \"message\": \"MQTT iletilerine gönderilecek başlığın maksimum uzunluğu\"\n  },\n  \"mqttBrokerHelp\": {\n    \"message\": \"MQTT broker'ının ana bilgisayar adı veya IP adresi\"\n  },\n  \"mqttPathHelp\": {\n    \"message\": \"WebSocket yolu (genellikle /mqtt veya /ws)\"\n  },\n  \"mqttClientIdHelp\": {\n    \"message\": \"Bu bağlantı için benzersiz istemci tanımlayıcı\"\n  },\n  \"mqttTopicHelp\": {\n    \"message\": \"İletileri yayınlanacak MQTT başlığı\"\n  },\n  \"mqttQosHelp\": {\n    \"message\": \"Hizmet Kalitesi düzeyi (0=En çok bir kez, 1=En az bir kez, 2=Tam olarak bir kez)\"\n  },\n  \"mqttCredentialsHelp\": {\n    \"message\": \"Gerekli değilse kullanıcı adı/şifre boş bırakın\"\n  },\n  \"operation\": {\n    \"message\": \"İşlem\"\n  },\n  \"exportSettings\": {\n    \"message\": \"Ayarları Dışarı Aktar\"\n  },\n  \"importConfiguration\": {\n    \"message\": \"Yapılandırmayı İçeri Aktar\"\n  },\n  \"clearCapturedData\": {\n    \"message\": \"Yakalanan Verileri Temizle\"\n  },\n  \"resetSettings\": {\n    \"message\": \"Ayarları Sıfırla\"\n  },\n  \"resetAllSettings\": {\n    \"message\": \"Tüm Ayarları Sıfırla\"\n  },\n  \"restartExtension\": {\n    \"message\": \"Uzantıyı Yeniden Başlat\"\n  },\n  \"about\": {\n    \"message\": \"Hakkında\"\n  },\n  \"confirmReset\": {\n    \"message\": \"Sıfırlamak istediğinizden emin misiniz?\"\n  },\n  \"invokeProtocolTemplate\": {\n    \"message\": \"Protokol Şablonunu Çağır\"\n  },\n  \"customVLCProtocol\": {\n    \"message\": \"Özel VLC Protokolü\"\n  },\n  \"systemShare\": {\n    \"message\": \"Sistem Paylaşımı\"\n  },\n  \"default\": {\n    \"message\": \"Varsayılan\"\n  },\n  \"goBack\": {\n    \"message\": \"Geri Git\"\n  },\n  \"openDir\": {\n    \"message\": \"Dizini Aç\"\n  },\n  \"downloadDir\": {\n    \"message\": \"İndirme Dizini\"\n  },\n  \"sendFfmpeg\": {\n    \"message\": \"Çevrimiçi ffmpeg'e Gönder\"\n  },\n  \"autoCloserDownload\": {\n    \"message\": \"İndirmeden Sonra Sayfayı Otomatik Kapat\"\n  },\n  \"openInBgDownload\": {\n    \"message\": \"İndirici Sayfasını Arka Planda Aç\"\n  },\n  \"m3u8Placeholder\": {\n    \"message\": \"m3u8 bağlantısını / m3u8 içeriğini / segment listesini / $${range} etiketini girin\"\n  },\n  \"m3u8Url\": {\n    \"message\": \"m3u8 URL'si\"\n  },\n  \"nextLevel\": {\n    \"message\": \"Sonraki Düzey\"\n  },\n  \"nextLevelTip\": {\n    \"message\": \"Bu M3U8 dosyası birden fazla M3U8 dosyasını iç içe yerleştirir.\"\n  },\n  \"multipleAudios\": {\n    \"message\": \"Birden Fazla Ses\"\n  },\n  \"multipleAudiosTip\": {\n    \"message\": \"Bu M3U8 dosyası birden fazla ses kaydını iç içe yerleştirir\"\n  },\n  \"multipleSubtitles\": {\n    \"message\": \"Birden Fazla Altyazı\"\n  },\n  \"multipleSubtitlesTip\": {\n    \"message\": \"Bu M3U8 dosyası birden fazla altyazı içeriyor.\"\n  },\n  \"possibleKey\": {\n    \"message\": \"Olası anahtarlar bulundu\"\n  },\n  \"loading\": {\n    \"message\": \"Yükleniyor...\"\n  },\n  \"waitDownload\": {\n    \"message\": \"İndirme bekleniyor...\"\n  },\n  \"downloadSegmentList\": {\n    \"message\": \"Liste İndir\"\n  },\n  \"originalM3u8\": {\n    \"message\": \"Orijinal M3U8\"\n  },\n  \"localM3u8\": {\n    \"message\": \"Yerel M3U8\"\n  },\n  \"segmentList\": {\n    \"message\": \"Segment\"\n  },\n  \"downloadProgress\": {\n    \"message\": \"İndirme İlerleme Durumu\"\n  },\n  \"getParameters\": {\n    \"message\": \"GET Parametreleri\"\n  },\n  \"restoreGetParameters\": {\n    \"message\": \"GET Parametrelerini Geri Yükle\"\n  },\n  \"requestHeaders\": {\n    \"message\": \"İstek Başlıkları\"\n  },\n  \"setRequestHeaders\": {\n    \"message\": \"İstek başlıklarını ayarlayın.\"\n  },\n  \"invokeM3u8DL\": {\n    \"message\": \"M3U8DL'yi Çağır\"\n  },\n  \"copyCommand\": {\n    \"message\": \"Komut Kopyala\"\n  },\n  \"previewCommand\": {\n    \"message\": \"Komutu Önizle\"\n  },\n  \"addSettingParameters\": {\n    \"message\": \"Ayar Parametreleri Ekle\"\n  },\n  \"customKeyPlaceholder\": {\n    \"message\": \"Hexadecimal veya base64'te anahtarı özelleştirin veya anahtar adresini yazın\"\n  },\n  \"uploadKey\": {\n    \"message\": \"Anahtarı Yükle\"\n  },\n  \"downloadThreads\": {\n    \"message\": \"İş Parçacığı\"\n  },\n  \"ffmpegTranscoding\": {\n    \"message\": \"FFmpeg kod çözme\"\n  },\n  \"mp4Format\": {\n    \"message\": \"MP4\"\n  },\n  \"downloadWhileSaving\": {\n    \"message\": \"Akış indirmesi\"\n  },\n  \"audioOnly\": {\n    \"message\": \"Yalnız Ses\"\n  },\n  \"saveAs\": {\n    \"message\": \"Farklı Kaydet\"\n  },\n  \"skipDecryption\": {\n    \"message\": \"Şifre Çözmeyi Atla\"\n  },\n  \"newDownloader\": {\n    \"message\": \"Yeni İndirici\"\n  },\n  \"downloadRange\": {\n    \"message\": \"İndirme Aralığı\"\n  },\n  \"recordLive\": {\n    \"message\": \"Kayıt\"\n  },\n  \"mergeDownloads\": {\n    \"message\": \"İndirmeleri Birleştir\"\n  },\n  \"redownloadFailedItems\": {\n    \"message\": \"Başarısız Öğeleri Yeniden İndir\"\n  },\n  \"downloadExistingData\": {\n    \"message\": \"Mevcut Verileri İndir\"\n  },\n  \"stopDownload\": {\n    \"message\": \"İndirmeyi Durdur\"\n  },\n  \"start\": {\n    \"message\": \"Başla\"\n  },\n  \"end\": {\n    \"message\": \"Son\"\n  },\n  \"resolution\": {\n    \"message\": \"Çözünürlük\"\n  },\n  \"duration\": {\n    \"message\": \"Süre\"\n  },\n  \"bitrate\": {\n    \"message\": \"Bit Hızı\"\n  },\n  \"ADTSerror\": {\n    \"message\": \"ADTS başlığı bulunamıyor. AES-128-ECB şifreli kaynak olabilir, şu anda şifre çözme desteklenmiyor. Lütfen üçüncü taraf birleştirme yazılımını kullanın.\"\n  },\n  \"m3u8Error\": {\n    \"message\": \"M3U8 dosyasını ayrıştırırken veya oynatırken hata var, ayrıntılı hata bilgileri için konsolu kontrol edin\"\n  },\n  \"noAudio\": {\n    \"message\": \"Ses Yok\"\n  },\n  \"noVideo\": {\n    \"message\": \"Video Yok\"\n  },\n  \"hevcTip\": {\n    \"message\": \"HEVC/H.265 kodlanmış fragment dosyaları yalnızca çevrimiçi ffmpeg kod çözme için destekleniyor\"\n  },\n  \"hevcPreviewTip\": {\n    \"message\": \"HEVC/H.265 kodlanmış fragment dosyaları önizleme için desteklenmiyor.\"\n  },\n  \"m3u8Info\": {\n    \"message\": \"Toplam $num$ dosya, toplam süre $time$.\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      },\n      \"time\": {\n        \"content\": \"$2\"\n      }\n    }\n  },\n  \"encryptedHLS\": {\n    \"message\": \"Şifreli HLS\"\n  },\n  \"encryptedSAMPLE\": {\n    \"message\": \"SAMPLE-AES-CTR ile şifrelenmiş kaynaklar şu anda işlenemiyor.\"\n  },\n  \"liveHLS\": {\n    \"message\": \"Canlı HLS\"\n  },\n  \"keyAddress\": {\n    \"message\": \"Anahtar Adresi\"\n  },\n  \"key\": {\n    \"message\": \"Anahtar\"\n  },\n  \"encryptionAlgorithm\": {\n    \"message\": \"Yöntem\"\n  },\n  \"keyDownloadFailed\": {\n    \"message\": \"Anahtar İndirmesi Başarısız Oldu\"\n  },\n  \"savePrompt\": {\n    \"message\": \"Diske kaydedildi, lütfen tarayıcıda indirilen içeriği kontrol edin.\"\n  },\n  \"close\": {\n    \"message\": \"Kapat\"\n  },\n  \"blobM3u8DLError\": {\n    \"message\": \"Blob URL'leri M3U8DL indirmesini çağıramaz\"\n  },\n  \"M3U8DLparameterLong\": {\n    \"message\": \"M3U8DL parametresi çok uzun.\"\n  },\n  \"runningCannotChangeSettings\": {\n    \"message\": \"Çalışıyor, Ayarlar Değiştirilemez\"\n  },\n  \"streamSaverTip\": {\n    \"message\": \"'İndirirken kaydet' işlevi ffmpeg çevrimiçi biçim dönüştürmeyi desteklemez, hatalı dilimleri yeniden indirmeyi desteklemez ve 'farklı kaydet'i desteklemez.\"\n  },\n  \"stopRecording\": {\n    \"message\": \"Kaydı Durdur\"\n  },\n  \"waitingForLiveData\": {\n    \"message\": \"Canlı Veriler Bekleniyor\"\n  },\n  \"sNumError\": {\n    \"message\": \"Seri Numarası Hatası\"\n  },\n  \"startGTend\": {\n    \"message\": \"Başlama Numarası Bitiş Numarasından Büyük Olamaz\"\n  },\n  \"sNumMax\": {\n    \"message\": \"Seri Numarası $num$ Aşamaz\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"incorrectKey\": {\n    \"message\": \"Yanlış Anahtar\"\n  },\n  \"addParameters\": {\n    \"message\": \"Parametreler Ekle\"\n  },\n  \"decryptionError\": {\n    \"message\": \"Şifre Çözme Hatası\"\n  },\n  \"downloadFailed\": {\n    \"message\": \"İndirme Başarısız Oldu\"\n  },\n  \"retryDownload\": {\n    \"message\": \"İndirmeyi Yeniden Dene\"\n  },\n  \"recordingDuration\": {\n    \"message\": \"Kayıt Süresi\"\n  },\n  \"downloaded\": {\n    \"message\": \"İndirildi\"\n  },\n  \"downloadedVideoLength\": {\n    \"message\": \"İndirilen Video Uzunluğu\"\n  },\n  \"downloadComplete\": {\n    \"message\": \"İndirme Tamamlandı\"\n  },\n  \"retryingDownload\": {\n    \"message\": \"İndirme Yeniden Deneniyor\"\n  },\n  \"merging\": {\n    \"message\": \"Birleştiriliyor\"\n  },\n  \"fileTooLarge\": {\n    \"message\": \"Dosya Çok Büyük, $size$ değerinden büyük dosya\",\n    \"placeholders\": {\n      \"size\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"fileTooLargeStream\": {\n    \"message\": \"$size$ değerinden büyük dosya, akış indirmeyi etkinleştir?\",\n    \"placeholders\": {\n      \"size\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"formatConversionError\": {\n    \"message\": \"Biçim Dönüştürme Hatası\"\n  },\n  \"streamOnbeforeunload\": {\n    \"message\": \"Akış devam ediyor, kapatıldıktan sonra indirme durur\"\n  },\n  \"fileLoading\": {\n    \"message\": \"Dosya Yükleniyor\"\n  },\n  \"expandAllNodes\": {\n    \"message\": \"Tüm JSON düğümlerini genişlet\"\n  },\n  \"collapseAllNodes\": {\n    \"message\": \"Tüm JSON düğümlerini daralt\"\n  },\n  \"fileRetrievalFailed\": {\n    \"message\": \"Dosya Alımı Başarısız\"\n  },\n  \"selectVideo\": {\n    \"message\": \"Video Seç\"\n  },\n  \"extractSlices\": {\n    \"message\": \"Dilimleri Çıkar\"\n  },\n  \"convertToM3U8\": {\n    \"message\": \"M3U8 Ayrıştırmasına Dönüştür\"\n  },\n  \"selectAudio\": {\n    \"message\": \"Ses Seç\"\n  },\n  \"audio\": {\n    \"message\": \"Ses\"\n  },\n  \"video\": {\n    \"message\": \"Video\"\n  },\n  \"DRMerror\": {\n    \"message\": \"Medyanın DRM koruması var, lütfen indirmek için üçüncü taraf araçları kullanın\"\n  },\n  \"regexTitle\": {\n    \"message\": \"Düzenli ifade eşleşmesi veya derin aramadan\"\n  },\n  \"downloadWithRequestHeader\": {\n    \"message\": \"İstek başlığı parametreleriyle indirin.\"\n  },\n  \"m3u8Playlist\": {\n    \"message\": \"M3U8 Çalma Listesi\"\n  },\n  \"copiedToClipboard\": {\n    \"message\": \"Panoya Kopyalandı\"\n  },\n  \"hasSent\": {\n    \"message\": \"Gönderildi\"\n  },\n  \"sendFailed\": {\n    \"message\": \"Gönderme Başarısız Oldu\"\n  },\n  \"confirmDownload\": {\n    \"message\": \"Toplam $num$ dosya, indirmeyi onayla?\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"confirmLoading\": {\n    \"message\": \"Toplam $num$ kaynak var, yüklemeyi iptal etmek istiyor musunuz?\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"waitingForMedia\": {\n    \"message\": \"Medya dosyalarını almayı bekleniyor... Lütfen bu sayfayı kapatmayın.\"\n  },\n  \"exit\": {\n    \"message\": \"Çık\"\n  },\n  \"unknownSize\": {\n    \"message\": \"Bilinmeyen boyut\"\n  },\n  \"saving\": {\n    \"message\": \"Kaydediliyor\"\n  },\n  \"saveFailed\": {\n    \"message\": \"Kaydetme başarısız\"\n  },\n  \"badgeNumber\": {\n    \"message\": \"Simge rozet istemini göster\"\n  },\n  \"viewSlices\": {\n    \"message\": \"Tüm dilimleri ve indirme ilerleme durumunu görüntüle\"\n  },\n  \"send2local\": {\n    \"message\": \"Veri aktarımı\"\n  },\n  \"send2MQTT\": {\n    \"message\": \"MQTT'ye Gönder\"\n  },\n  \"sendingToMQTT\": {\n    \"message\": \"MQTT sunucusuna gönderiliyor...\"\n  },\n  \"connectingToMQTT\": {\n    \"message\": \"MQTT sunucusuna bağlanıyor...\"\n  },\n  \"sendingMessageToMQTT\": {\n    \"message\": \"MQTT sunucusuna ileti gönderiliyor...\"\n  },\n  \"messageSentToMQTT\": {\n    \"message\": \"İleti MQTT sunucusuna gönderildi\"\n  },\n  \"popup\": {\n    \"message\": \"Açılır Pencere\"\n  },\n  \"defaultPopup\": {\n    \"message\": \"Varsayılan Açılır Pencere Modu\"\n  },\n  \"invokeApp\": {\n    \"message\": \"Uygulamayı Çağır\"\n  },\n  \"onlineServiceAddress\": {\n    \"message\": \"Çevrimiçi Hizmet Adresi\"\n  },\n  \"withinChina\": {\n    \"message\": \"Çin İçinde\"\n  },\n  \"dataFetchFailed\": {\n    \"message\": \"Veri alımı başarısız\"\n  },\n  \"confirmParameters\": {\n    \"message\": \"Parametreleri Onayla\"\n  },\n  \"searchingForRealKey\": {\n    \"message\": \"Gerçek anahtar aranıyor\"\n  },\n  \"verifying\": {\n    \"message\": \"Doğrulanıyor\"\n  },\n  \"realKeyNotFound\": {\n    \"message\": \"Gerçek anahtar bulunamadı\"\n  },\n  \"blockUrl\": {\n    \"message\": \"URL'yi Engelle\"\n  },\n  \"addUrl\": {\n    \"message\": \"URL Ekle\"\n  },\n  \"wildcards\": {\n    \"message\": \"joker karakterler\"\n  },\n  \"blockUrlTips\": {\n    \"message\": \"Joker karakterler * ve ? destekler\"\n  },\n  \"setWhiteList\": {\n    \"message\": \"Beyaz listeye ayarla\"\n  },\n  \"autoSend\": {\n    \"message\": \"Otomatik veri aktarımı\"\n  },\n  \"manualSend\": {\n    \"message\": \"Manuel veri aktarımı\"\n  },\n  \"requestMethod\": {\n    \"message\": \"İstek Yöntemi\"\n  },\n  \"requestBody\": {\n    \"message\": \"İstek Gövdesi\"\n  },\n  \"sort\": {\n    \"message\": \"Sırala\"\n  },\n  \"asc\": {\n    \"message\": \"Artan\"\n  },\n  \"desc\": {\n    \"message\": \"Azalan\"\n  },\n  \"getTime\": {\n    \"message\": \"Alma Zamanı\"\n  },\n  \"fileSize\": {\n    \"message\": \"Dosya Boyutu\"\n  },\n  \"title\": {\n    \"message\": \"Başlık\"\n  },\n  \"noKeyIsRequired\": {\n    \"message\": \"Anahtar gerekli değil\"\n  },\n  \"estimateSize\": {\n    \"message\": \"Tahmini boyut\"\n  },\n  \"retryCount\": {\n    \"message\": \"Yeniden deneme sayısı\"\n  },\n  \"useSidePanel\": {\n    \"message\": \"Yan paneli kullan\"\n  },\n  \"Script\": {\n    \"message\": \"Betik\"\n  },\n  \"alwaysSearch\": {\n    \"message\": \"Her zaman derin aramayı etkinleştir\"\n  },\n  \"alertimport\": {\n  \t\"message\": \"İçe aktarma tamamlandı\"\n  },\n  \"deleteDuplicateFilenames\": {\n    \"message\": \"Yinelenen dosya adlarını sil\"\n  }\n}\n"
  },
  {
    "path": "_locales/vi/messages.json",
    "content": "{\n  \"catCatch\": {\n    \"message\": \"cat-catch\"\n  },\n  \"description\": {\n    \"message\": \"Công cụ bắt link media trên web\"\n  },\n  \"confirm\": {\n    \"message\": \"Xác nhận\"\n  },\n  \"currentPage\": {\n    \"message\": \"Trang hiện tại\"\n  },\n  \"otherPage\": {\n    \"message\": \"Trang khác\"\n  },\n  \"otherFeatures\": {\n    \"message\": \"Tính năng khác\"\n  },\n  \"mediaControl\": {\n    \"message\": \"Điều khiển Media\"\n  },\n  \"loadingData\": {\n    \"message\": \"Đang tải dữ liệu...\"\n  },\n  \"selectWebpage\": {\n    \"message\": \"Trang web:\"\n  },\n  \"selectMedia\": {\n    \"message\": \"Media:\"\n  },\n  \"noMediaDetected\": {\n    \"message\": \"Không tìm thấy Media nào trên trang web\"\n  },\n  \"noControllableMediaDetected\": {\n    \"message\": \"Không tìm thấy Media có thể điều khiển\"\n  },\n  \"multiplier\": {\n    \"message\": \"Hệ số nhân:\"\n  },\n  \"speedPlayback\": {\n    \"message\": \"Tốc độ phát\"\n  },\n  \"play\": {\n    \"message\": \"Phát\"\n  },\n  \"normalPlay\": {\n    \"message\": \"Phát bình thường\"\n  },\n  \"pictureInPicture\": {\n    \"message\": \"Hình trong hình (PiP)\"\n  },\n  \"fullscreen\": {\n    \"message\": \"Toàn màn hình\"\n  },\n  \"screenshot\": {\n    \"message\": \"Chụp màn hình\"\n  },\n  \"loop\": {\n    \"message\": \"Lặp lại\"\n  },\n  \"mute\": {\n    \"message\": \"Tắt tiếng\"\n  },\n  \"volume\": {\n    \"message\": \"Âm lượng\"\n  },\n  \"functionEntry\": {\n    \"message\": \"Lối vào chức năng\"\n  },\n  \"downloader\": {\n    \"message\": \"Trình tải xuống\"\n  },\n  \"parser\": {\n    \"message\": \"Trình phân tích\"\n  },\n  \"m3u8Parser\": {\n    \"message\": \"Trình phân tích M3U8\"\n  },\n  \"mpdParser\": {\n    \"message\": \"Trình phân tích MPD\"\n  },\n  \"jsonFormatter\": {\n    \"message\": \"Định dạng JSON\"\n  },\n  \"expandAll\": {\n    \"message\": \"Mở rộng tất cả\"\n  },\n  \"expandPlayable\": {\n    \"message\": \"Mở rộng cái có thể phát\"\n  },\n  \"expandSelected\": {\n    \"message\": \"Mở rộng cái đã chọn\"\n  },\n  \"collapseAll\": {\n    \"message\": \"Thu gọn tất cả\"\n  },\n  \"videoRecording\": {\n    \"message\": \"Ghi hình video\"\n  },\n  \"closeRecording\": {\n    \"message\": \"Đóng ghi hình\"\n  },\n  \"recordWebRTC\": {\n    \"message\": \"Ghi WebRTC\"\n  },\n  \"screenCapture\": {\n    \"message\": \"Quay màn hình\"\n  },\n  \"simulateMobile\": {\n    \"message\": \"Giả lập Mobile\"\n  },\n  \"autoDownload\": {\n    \"message\": \"Tự động tải xuống\"\n  },\n  \"onlineMerge\": {\n    \"message\": \"Ghép nối\"\n  },\n  \"download\": {\n    \"message\": \"Tải xuống\"\n  },\n  \"copy\": {\n    \"message\": \"Sao chép\"\n  },\n  \"selectAll\": {\n    \"message\": \"Chọn tất cả\"\n  },\n  \"invertSelection\": {\n    \"message\": \"Đảo ngược chọn\"\n  },\n  \"filter\": {\n    \"message\": \"Lọc\"\n  },\n  \"clear\": {\n    \"message\": \"Xóa\"\n  },\n  \"deepSearch\": {\n    \"message\": \"Tìm kiếm\"\n  },\n  \"closeSearch\": {\n    \"message\": \"Đóng tìm kiếm\"\n  },\n  \"cacheCapture\": {\n    \"message\": \"Bắt\"\n  },\n  \"closeCapture\": {\n    \"message\": \"Đóng bắt link\"\n  },\n  \"moreFeatures\": {\n    \"message\": \"Thêm\"\n  },\n  \"pause\": {\n    \"message\": \"Tạm dừng\"\n  },\n  \"settings\": {\n    \"message\": \"Cài đặt\"\n  },\n  \"closeSimulation\": {\n    \"message\": \"Đóng giả lập\"\n  },\n  \"closeDownload\": {\n    \"message\": \"Đóng tải xuống\"\n  },\n  \"enable\": {\n    \"message\": \"Bật\"\n  },\n  \"disable\": {\n    \"message\": \"Tắt\"\n  },\n  \"noData\": {\n    \"message\": \"Không có cá\"\n  },\n  \"regularFilterPlaceholder\": {\n    \"message\": \"Bộ lọc biểu thức chính quy, khớp URL tài nguyên, nhấn Enter để xác nhận\"\n  },\n  \"option\": {\n    \"message\": \"Tùy chọn\"\n  },\n  \"titleOption\": {\n    \"message\": \"Tùy chọn cat-catch\"\n  },\n  \"titleDownload\": {\n    \"message\": \"Tải xuống cat-catch\"\n  },\n  \"titleM3U8\": {\n    \"message\": \"Trình phân tích m3u8 cat-catch\"\n  },\n  \"titleJson\": {\n    \"message\": \"Trình định dạng json cat-catch\"\n  },\n  \"titledash\": {\n    \"message\": \"Trình phân tích Dash cat-catch\"\n  },\n  \"suffix\": {\n    \"message\": \"Đuôi tệp\"\n  },\n  \"suffixTip\": {\n    \"message\": \"Điền vào đuôi tệp không chứa dấu '.', nếu không cần lọc kích thước thì điền 0.\"\n  },\n  \"extensionName\": {\n    \"message\": \"Tên tiện ích\"\n  },\n  \"filterSize\": {\n    \"message\": \"Lọc kích thước\"\n  },\n  \"delete\": {\n    \"message\": \"Xóa\"\n  },\n  \"addSuffix\": {\n    \"message\": \"Thêm đuôi tệp\"\n  },\n  \"extension\": {\n    \"message\": \"Phần mở rộng\"\n  },\n  \"disableAll\": {\n    \"message\": \"Tắt tất cả\"\n  },\n  \"enableAll\": {\n    \"message\": \"Bật tất cả\"\n  },\n  \"type\": {\n    \"message\": \"Loại\"\n  },\n  \"addType\": {\n    \"message\": \"Thêm loại\"\n  },\n  \"typeTip\": {\n    \"message\": \"Nhập đúng content-type, nếu không cần lọc kích thước thì điền 0.\"\n  },\n  \"addTypeError\": {\n    \"message\": \"Định dạng loại bắt không chính xác, vui lòng kiểm tra lại\"\n  },\n  \"regexMatch\": {\n    \"message\": \"Khớp Regex\"\n  },\n  \"blockResource\": {\n    \"message\": \"Chặn tài nguyên\"\n  },\n  \"alert\": {\n    \"message\": \"Cảnh báo\"\n  },\n  \"regexExpression\": {\n    \"message\": \"Biểu thức Regex\"\n  },\n  \"addRegex\": {\n    \"message\": \"Thêm Regex\"\n  },\n  \"regexTest\": {\n    \"message\": \"Kiểm tra Regex\"\n  },\n  \"regex\": {\n    \"message\": \"regex\"\n  },\n  \"flag\": {\n    \"message\": \"Cờ (Flag)\"\n  },\n  \"result\": {\n    \"message\": \"Kết quả\"\n  },\n  \"match\": {\n    \"message\": \"Khớp\"\n  },\n  \"noMatch\": {\n    \"message\": \"Không khớp\"\n  },\n  \"blockResourceTip\": {\n    \"message\": \"Chặn các tài nguyên bạn không muốn xuất hiện\"\n  },\n  \"flagTip\": {\n    \"message\": \"i: không phân biệt hoa thường, g: tìm kiếm toàn cục. Có thể để trống\"\n  },\n  \"regexSuffixTip\": {\n    \"message\": \"Gán đuôi tệp cho URL thu được. Có thể để trống, và đuôi tệp sẽ tự động được cắt bớt (nhiều tệp không có đuôi)\"\n  },\n  \"regexTip\": {\n    \"message\": \"Biểu thức chính quy tiêu tốn nhiều tài nguyên, hãy sử dụng cẩn thận nếu không cần thiết\"\n  },\n  \"copyTip\": {\n    \"message\": \"Để thuận tiện cho việc sử dụng ứng dụng bên thứ ba, tùy chỉnh nội dung được ghi vào bộ nhớ tạm bởi nút sao chép\"\n  },\n  \"replaceKeywordList\": {\n    \"message\": \"Danh sách từ khóa thay thế\"\n  },\n  \"otherFiles\": {\n    \"message\": \"Các tệp khác\"\n  },\n  \"resetCopySettings\": {\n    \"message\": \"Đặt lại cài đặt sao chép\"\n  },\n  \"autoSetRefererCookieParams\": {\n    \"message\": \"Tự động thiết lập tham số Referer và Cookie\"\n  },\n  \"secretKey\": {\n    \"message\": \"Khóa bí mật (Secret Key)\"\n  },\n  \"address\": {\n    \"message\": \"Địa chỉ\"\n  },\n  \"documentation\": {\n    \"message\": \"Tài liệu\"\n  },\n  \"aria2Tip\": {\n    \"message\": \"Một công cụ tải xuống tuyệt vời, xem cách sử dụng\"\n  },\n  \"m3u8DLTips\": {\n    \"message\": \"Công cụ tải m3u8 và mpd bên thứ ba tuyệt vời, xem cách sử dụng\"\n  },\n  \"invoke\": {\n    \"message\": \"Gọi\"\n  },\n  \"parameter\": {\n    \"message\": \"Tham số\"\n  },\n  \"parameterSetting\": {\n    \"message\": \"Cài đặt tham số\"\n  },\n  \"test\": {\n    \"message\": \"Kiểm tra\"\n  },\n  \"replaceTags\": {\n    \"message\": \"Thay thế thẻ (Tags)\"\n  },\n  \"customSaveFileName\": {\n    \"message\": \"Tên tệp lưu tùy chỉnh\"\n  },\n  \"userAgentTip\": {\n    \"message\": \"Mặc định sử dụng User Agent của trình duyệt hiện tại\"\n  },\n  \"alwaysDisableCatCatcher\": {\n    \"message\": \"Luôn tắt trình tải xuống Cat-Catch\"\n  },\n  \"autoClosePageAfterDownload\": {\n    \"message\": \"Tự động đóng trang sau khi tải xong\"\n  },\n  \"openDownloaderPageInBackground\": {\n    \"message\": \"Mở trang tải xuống trong nền\"\n  },\n  \"downloaderTip\": {\n    \"message\": \"Nếu tải tài nguyên thất bại, tự động bật trình tải xuống để thử lại.\"\n  },\n  \"autoDownM3u8Tip\": {\n    \"message\": \"Nhấp nút tải xuống và sử dụng trình phân tích m3u8 để bắt đầu ghép và tải ngay lập tức\"\n  },\n  \"otherSettings\": {\n    \"message\": \"Cài đặt khác\"\n  },\n  \"resetOtherSettings\": {\n    \"message\": \"Đặt lại các cài đặt khác\"\n  },\n  \"previewMode\": {\n    \"message\": \"Sử dụng giao thức gọi của trình phát cục bộ để mở xem trước video\"\n  },\n  \"previewModePlaceholder\": {\n    \"message\": \"Để trống để tắt. Mặc định sử dụng trang popup để xem trước video\"\n  },\n  \"preview\": {\n    \"message\": \"Xem trước\"\n  },\n  \"customFilenameOption\": {\n    \"message\": \"Sử dụng tên tệp tùy chỉnh để lưu (mặc định là tiêu đề trang web)\"\n  },\n  \"saveAsOption\": {\n    \"message\": \"Chọn thư mục lưu sau khi tải xuống\"\n  },\n  \"iconOption\": {\n    \"message\": \"Hiển thị biểu tượng trang web\"\n  },\n  \"clearOption\": {\n    \"message\": \"Làm mới, điều hướng đến trang mới, và xóa dữ liệu đã bắt được bởi tab hiện tại\"\n  },\n  \"doNotClear\": {\n    \"message\": \"Không xóa\"\n  },\n  \"normalClear\": {\n    \"message\": \"Xóa bình thường\"\n  },\n  \"moreFrequent\": {\n    \"message\": \"Thường xuyên hơn\"\n  },\n  \"dopreview\": {\n    \"message\": \"xem trước\"\n  },\n  \"dopopup\": {\n    \"message\": \"popup\"\n  },\n  \"winpreview\": {\n    \"message\": \"cửa sổ xem trước\"\n  },\n  \"winpopup\": {\n    \"message\": \"cửa sổ popup\"\n  },\n  \"excludeDuplicateResources\": {\n    \"message\": \"Loại trừ tài nguyên trùng lặp (quá nhiều tài nguyên sẽ tiêu tốn nhiều CPU)\"\n  },\n  \"customCSS\": {\n    \"message\": \"CSS tùy chỉnh\"\n  },\n  \"MQTT\": {\n    \"message\": \"MQTT\"\n  },\n  \"mqttBroker\": {\n    \"message\": \"Địa chỉ Broker\"\n  },\n  \"mqttPath\": {\n    \"message\": \"Đường dẫn (Path)\"\n  },\n  \"mqttProtocol\": {\n    \"message\": \"Giao thức\"\n  },\n  \"mqttClientId\": {\n    \"message\": \"Client ID\"\n  },\n  \"mqttTitleLength\": {\n    \"message\": \"Độ dài tiêu đề tối đa\"\n  },\n  \"mqttUsername\": {\n    \"message\": \"Tên đăng nhập\"\n  },\n  \"mqttPassword\": {\n    \"message\": \"Mật khẩu\"\n  },\n  \"mqttTopic\": {\n    \"message\": \"Chủ đề (Topic)\"\n  },\n  \"mqttQos\": {\n    \"message\": \"Mức QoS\"\n  },\n  \"mqttQos0\": {\n    \"message\": \"(Tối đa một lần)\"\n  },\n  \"mqttQos1\": {\n    \"message\": \"(Ít nhất một lần)\"\n  },\n  \"mqttQos2\": {\n    \"message\": \"(Chính xác một lần)\"\n  },\n  \"mqttDataFormat\": {\n    \"message\": \"Định dạng dữ liệu\"\n  },\n  \"mqttDataFormatHelp\": {\n    \"message\": \"{\\\"url\\\": \\\"$${url}\\\", \\\"title\\\": \\\"$${title}\\\", \\\"type\\\": \\\"$${type}\\\", \\\"ext\\\": \\\"$${ext}\\\", \\\"timestamp\\\": \\\"$${timestamp}\\\"}\"\n  },\n  \"mqttDataFormatVars\": {\n    \"message\": \"Các biến khả dụng\"\n  },\n  \"mqttDataFormatDefault\": {\n    \"message\": \"Để trống để dùng định dạng JSON mặc định\"\n  },\n  \"mqttProtocolWss\": {\n    \"message\": \"WSS (An toàn)\"\n  },\n  \"mqttProtocolWs\": {\n    \"message\": \"WS (Không an toàn)\"\n  },\n  \"mqttTitleLengthHelp\": {\n    \"message\": \"Độ dài tối đa của tiêu đề gửi trong tin nhắn MQTT\"\n  },\n  \"mqttBrokerHelp\": {\n    \"message\": \"Tên máy chủ hoặc địa chỉ IP của MQTT broker\"\n  },\n  \"mqttPathHelp\": {\n    \"message\": \"Đường dẫn WebSocket (thường là /mqtt hoặc /ws)\"\n  },\n  \"mqttClientIdHelp\": {\n    \"message\": \"Mã định danh client duy nhất cho kết nối này\"\n  },\n  \"mqttTopicHelp\": {\n    \"message\": \"Chủ đề MQTT để xuất bản tin nhắn\"\n  },\n  \"mqttQosHelp\": {\n    \"message\": \"Mức chất lượng dịch vụ (0=Tối đa một lần, 1=Ít nhất một lần, 2=Chính xác một lần)\"\n  },\n  \"mqttCredentialsHelp\": {\n    \"message\": \"Để trống tên đăng nhập/mật khẩu nếu không yêu cầu\"\n  },\n  \"operation\": {\n    \"message\": \"Thao tác\"\n  },\n  \"exportSettings\": {\n    \"message\": \"Xuất cài đặt\"\n  },\n  \"importConfiguration\": {\n    \"message\": \"Nhập cấu hình\"\n  },\n  \"clearCapturedData\": {\n    \"message\": \"Xóa dữ liệu đã bắt\"\n  },\n  \"resetSettings\": {\n    \"message\": \"Đặt lại cài đặt\"\n  },\n  \"resetAllSettings\": {\n    \"message\": \"Đặt lại tất cả cài đặt\"\n  },\n  \"restartExtension\": {\n    \"message\": \"Khởi động lại tiện ích\"\n  },\n  \"about\": {\n    \"message\": \"Giới thiệu\"\n  },\n  \"confirmReset\": {\n    \"message\": \"Bạn có chắc chắn muốn đặt lại không?\"\n  },\n  \"invokeProtocolTemplate\": {\n    \"message\": \"Mẫu giao thức gọi\"\n  },\n  \"customVLCProtocol\": {\n    \"message\": \"Giao thức VLC tùy chỉnh\"\n  },\n  \"systemShare\": {\n    \"message\": \"Chia sẻ hệ thống\"\n  },\n  \"default\": {\n    \"message\": \"Mặc định\"\n  },\n  \"goBack\": {\n    \"message\": \"Quay lại\"\n  },\n  \"openDir\": {\n    \"message\": \"Mở thư mục\"\n  },\n  \"downloadDir\": {\n    \"message\": \"Thư mục tải xuống\"\n  },\n  \"sendFfmpeg\": {\n    \"message\": \"Gửi đến ffmpeg trực tuyến\"\n  },\n  \"autoCloserDownload\": {\n    \"message\": \"Tự động đóng trang sau khi tải xong\"\n  },\n  \"openInBgDownload\": {\n    \"message\": \"Mở trang tải xuống trong nền\"\n  },\n  \"m3u8Placeholder\": {\n    \"message\": \"Vui lòng nhập link m3u8 / nội dung m3u8 / danh sách phân đoạn / thẻ $${range}\"\n  },\n  \"m3u8Url\": {\n    \"message\": \"URL M3U8\"\n  },\n  \"nextLevel\": {\n    \"message\": \"Cấp tiếp theo\"\n  },\n  \"nextLevelTip\": {\n    \"message\": \"Tệp M3U8 này lồng nhiều tệp M3U8 khác.\"\n  },\n  \"multipleAudios\": {\n    \"message\": \"Đa âm thanh\"\n  },\n  \"multipleAudiosTip\": {\n    \"message\": \"Tệp M3U8 này lồng nhiều luồng âm thanh\"\n  },\n  \"multipleSubtitles\": {\n    \"message\": \"Đa phụ đề\"\n  },\n  \"multipleSubtitlesTip\": {\n    \"message\": \"Tệp M3U8 này lồng nhiều phụ đề.\"\n  },\n  \"possibleKey\": {\n    \"message\": \"Tìm thấy khóa khả thi\"\n  },\n  \"loading\": {\n    \"message\": \"Đang tải...\"\n  },\n  \"waitDownload\": {\n    \"message\": \"Đang chờ tải xuống...\"\n  },\n  \"downloadSegmentList\": {\n    \"message\": \"Tải danh sách\"\n  },\n  \"originalM3u8\": {\n    \"message\": \"M3U8 gốc\"\n  },\n  \"localM3u8\": {\n    \"message\": \"M3U8 cục bộ\"\n  },\n  \"segmentList\": {\n    \"message\": \"Phân đoạn\"\n  },\n  \"downloadProgress\": {\n    \"message\": \"Tiến độ tải xuống\"\n  },\n  \"getParameters\": {\n    \"message\": \"Tham số GET\"\n  },\n  \"restoreGetParameters\": {\n    \"message\": \"Khôi phục tham số GET\"\n  },\n  \"requestHeaders\": {\n    \"message\": \"Header yêu cầu\"\n  },\n  \"setRequestHeaders\": {\n    \"message\": \"Đặt header yêu cầu.\"\n  },\n  \"invokeM3u8DL\": {\n    \"message\": \"Gọi M3U8DL\"\n  },\n  \"copyCommand\": {\n    \"message\": \"Sao chép lệnh\"\n  },\n  \"previewCommand\": {\n    \"message\": \"Xem trước lệnh\"\n  },\n  \"addSettingParameters\": {\n    \"message\": \"Thêm tham số cài đặt\"\n  },\n  \"customKeyPlaceholder\": {\n    \"message\": \"Tùy chỉnh khóa dạng hex hoặc base64, hoặc địa chỉ chứa khóa\"\n  },\n  \"uploadKey\": {\n    \"message\": \"Tải lên khóa\"\n  },\n  \"downloadThreads\": {\n    \"message\": \"Luồng\"\n  },\n  \"ffmpegTranscoding\": {\n    \"message\": \"Chuyển mã FFmpeg\"\n  },\n  \"mp4Format\": {\n    \"message\": \"MP4\"\n  },\n  \"downloadWhileSaving\": {\n    \"message\": \"Vừa tải vừa lưu\"\n  },\n  \"audioOnly\": {\n    \"message\": \"Chỉ âm thanh\"\n  },\n  \"saveAs\": {\n    \"message\": \"Lưu thành\"\n  },\n  \"skipDecryption\": {\n    \"message\": \"Bỏ qua giải mã\"\n  },\n  \"newDownloader\": {\n    \"message\": \"Trình tải mới\"\n  },\n  \"downloadRange\": {\n    \"message\": \"Phạm vi tải\"\n  },\n  \"recordLive\": {\n    \"message\": \"Ghi hình\"\n  },\n  \"mergeDownloads\": {\n    \"message\": \"Ghép nối bản tải xuống\"\n  },\n  \"redownloadFailedItems\": {\n    \"message\": \"Tải lại các mục lỗi\"\n  },\n  \"downloadExistingData\": {\n    \"message\": \"Tải xuống dữ liệu hiện có\"\n  },\n  \"stopDownload\": {\n    \"message\": \"Dừng tải xuống\"\n  },\n  \"start\": {\n    \"message\": \"Bắt đầu\"\n  },\n  \"end\": {\n    \"message\": \"Kết thúc\"\n  },\n  \"resolution\": {\n    \"message\": \"Độ phân giải\"\n  },\n  \"duration\": {\n    \"message\": \"Thời lượng\"\n  },\n  \"bitrate\": {\n    \"message\": \"Tốc độ bit (Bitrate)\"\n  },\n  \"ADTSerror\": {\n    \"message\": \"Không tìm thấy header ADTS. Có thể là tài nguyên mã hóa AES-128-ECB, hiện chưa hỗ trợ giải mã. Vui lòng dùng phần mềm ghép nối bên thứ ba.\"\n  },\n  \"m3u8Error\": {\n    \"message\": \"Có lỗi khi phân tích hoặc phát tệp M3U8, kiểm tra console để biết chi tiết lỗi\"\n  },\n  \"noAudio\": {\n    \"message\": \"Không có âm thanh\"\n  },\n  \"noVideo\": {\n    \"message\": \"Không có hình ảnh\"\n  },\n  \"hevcTip\": {\n    \"message\": \"Tệp phân đoạn mã hóa HEVC/H.265 chỉ được hỗ trợ chuyển mã ffmpeg trực tuyến\"\n  },\n  \"hevcPreviewTip\": {\n    \"message\": \"Tệp phân đoạn mã hóa HEVC/H.265 không hỗ trợ xem trước.\"\n  },\n  \"m3u8Info\": {\n    \"message\": \"Tổng cộng $num$ tệp, tổng thời lượng $time$.\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      },\n      \"time\": {\n        \"content\": \"$2\"\n      }\n    }\n  },\n  \"encryptedHLS\": {\n    \"message\": \"HLS được mã hóa\"\n  },\n  \"encryptedSAMPLE\": {\n    \"message\": \"Tài nguyên được mã hóa SAMPLE-AES-CTR hiện chưa thể xử lý.\"\n  },\n  \"liveHLS\": {\n    \"message\": \"HLS trực tiếp\"\n  },\n  \"keyAddress\": {\n    \"message\": \"Địa chỉ Key\"\n  },\n  \"key\": {\n    \"message\": \"Key\"\n  },\n  \"encryptionAlgorithm\": {\n    \"message\": \"Phương thức\"\n  },\n  \"keyDownloadFailed\": {\n    \"message\": \"Tải Key thất bại\"\n  },\n  \"savePrompt\": {\n    \"message\": \"Đã lưu vào ổ đĩa, vui lòng kiểm tra nội dung tải xuống trong trình duyệt.\"\n  },\n  \"close\": {\n    \"message\": \"Đóng\"\n  },\n  \"blobM3u8DLError\": {\n    \"message\": \"URL Blob không thể gọi M3U8DL để tải xuống\"\n  },\n  \"M3U8DLparameterLong\": {\n    \"message\": \"Tham số M3U8DL quá dài.\"\n  },\n  \"runningCannotChangeSettings\": {\n    \"message\": \"Đang chạy, không thể thay đổi cài đặt\"\n  },\n  \"streamSaverTip\": {\n    \"message\": \"Chức năng 'vừa tải vừa lưu' không hỗ trợ chuyển đổi định dạng ffmpeg trực tuyến, không hỗ trợ tải lại các lát bị lỗi, và không hỗ trợ 'lưu thành'.\"\n  },\n  \"stopRecording\": {\n    \"message\": \"Dừng ghi\"\n  },\n  \"waitingForLiveData\": {\n    \"message\": \"Đang chờ dữ liệu trực tiếp (Live)\"\n  },\n  \"sNumError\": {\n    \"message\": \"Lỗi số thứ tự\"\n  },\n  \"startGTend\": {\n    \"message\": \"Số bắt đầu không thể lớn hơn số kết thúc\"\n  },\n  \"sNumMax\": {\n    \"message\": \"Số thứ tự không thể vượt quá $num$\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"incorrectKey\": {\n    \"message\": \"Key không chính xác\"\n  },\n  \"addParameters\": {\n    \"message\": \"Thêm tham số\"\n  },\n  \"decryptionError\": {\n    \"message\": \"Lỗi giải mã\"\n  },\n  \"downloadFailed\": {\n    \"message\": \"Tải xuống thất bại\"\n  },\n  \"retryDownload\": {\n    \"message\": \"Thử lại tải xuống\"\n  },\n  \"recordingDuration\": {\n    \"message\": \"Thời gian ghi\"\n  },\n  \"downloaded\": {\n    \"message\": \"Đã tải\"\n  },\n  \"downloadedVideoLength\": {\n    \"message\": \"Độ dài video đã tải\"\n  },\n  \"downloadComplete\": {\n    \"message\": \"Tải xuống hoàn tất\"\n  },\n  \"retryingDownload\": {\n    \"message\": \"Đang thử lại tải xuống\"\n  },\n  \"merging\": {\n    \"message\": \"Đang ghép nối\"\n  },\n  \"fileTooLarge\": {\n    \"message\": \"Tệp quá lớn, tệp lớn hơn $size$\",\n    \"placeholders\": {\n      \"size\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"fileTooLargeStream\": {\n    \"message\": \"Tệp lớn hơn $size$, bật chế độ vừa tải vừa lưu?\",\n    \"placeholders\": {\n      \"size\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"formatConversionError\": {\n    \"message\": \"Lỗi chuyển đổi định dạng\"\n  },\n  \"streamOnbeforeunload\": {\n    \"message\": \"Đang stream, quá trình tải sẽ dừng sau khi đóng\"\n  },\n  \"fileLoading\": {\n    \"message\": \"Đang tải tệp\"\n  },\n  \"expandAllNodes\": {\n    \"message\": \"Mở rộng tất cả node JSON\"\n  },\n  \"collapseAllNodes\": {\n    \"message\": \"Thu gọn tất cả node JSON\"\n  },\n  \"fileRetrievalFailed\": {\n    \"message\": \"Lấy tệp thất bại\"\n  },\n  \"selectVideo\": {\n    \"message\": \"Chọn Video\"\n  },\n  \"extractSlices\": {\n    \"message\": \"Trích xuất các lát (slices)\"\n  },\n  \"convertToM3U8\": {\n    \"message\": \"Chuyển sang phân tích M3U8\"\n  },\n  \"selectAudio\": {\n    \"message\": \"Chọn Audio\"\n  },\n  \"audio\": {\n    \"message\": \"Âm thanh\"\n  },\n  \"video\": {\n    \"message\": \"Hình ảnh\"\n  },\n  \"DRMerror\": {\n    \"message\": \"Media có bảo vệ DRM, vui lòng dùng công cụ bên thứ ba để tải\"\n  },\n  \"regexTitle\": {\n    \"message\": \"Khớp biểu thức chính quy hoặc từ tìm kiếm sâu\"\n  },\n  \"downloadWithRequestHeader\": {\n    \"message\": \"Tải xuống với tham số request header.\"\n  },\n  \"m3u8Playlist\": {\n    \"message\": \"Danh sách phát M3U8\"\n  },\n  \"copiedToClipboard\": {\n    \"message\": \"Đã sao chép vào bộ nhớ tạm\"\n  },\n  \"hasSent\": {\n    \"message\": \"Đã gửi\"\n  },\n  \"sendFailed\": {\n    \"message\": \"Gửi thất bại\"\n  },\n  \"confirmDownload\": {\n    \"message\": \"Tổng cộng $num$ tệp, xác nhận tải xuống?\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"confirmLoading\": {\n    \"message\": \"Có tổng cộng $num$ tài nguyên, bạn có muốn hủy tải không?\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"waitingForMedia\": {\n    \"message\": \"Đang chờ nhận tệp media... Vui lòng không đóng trang này.\"\n  },\n  \"exit\": {\n    \"message\": \"Thoát\"\n  },\n  \"unknownSize\": {\n    \"message\": \"Kích thước không xác định\"\n  },\n  \"saving\": {\n    \"message\": \"Đang lưu\"\n  },\n  \"saveFailed\": {\n    \"message\": \"Lưu thất bại\"\n  },\n  \"badgeNumber\": {\n    \"message\": \"Hiển thị số trên biểu tượng\"\n  },\n  \"viewSlices\": {\n    \"message\": \"Xem tất cả các lát và tiến độ tải xuống\"\n  },\n  \"send2local\": {\n    \"message\": \"Truyền dữ liệu\"\n  },\n  \"send2MQTT\": {\n    \"message\": \"Gửi đến MQTT\"\n  },\n  \"sendingToMQTT\": {\n    \"message\": \"Đang gửi đến máy chủ MQTT...\"\n  },\n  \"connectingToMQTT\": {\n    \"message\": \"Đang kết nối đến máy chủ MQTT...\"\n  },\n  \"sendingMessageToMQTT\": {\n    \"message\": \"Đang gửi tin nhắn đến máy chủ MQTT...\"\n  },\n  \"messageSentToMQTT\": {\n    \"message\": \"Tin nhắn đã gửi đến máy chủ MQTT\"\n  },\n  \"popup\": {\n    \"message\": \"Popup\"\n  },\n  \"defaultPopup\": {\n    \"message\": \"Chế độ Popup mặc định\"\n  },\n  \"invokeApp\": {\n    \"message\": \"Gọi ứng dụng\"\n  },\n  \"onlineServiceAddress\": {\n    \"message\": \"Địa chỉ dịch vụ trực tuyến\"\n  },\n  \"withinChina\": {\n    \"message\": \"Trong nội địa Trung Quốc\"\n  },\n  \"dataFetchFailed\": {\n    \"message\": \"Lấy dữ liệu thất bại\"\n  },\n  \"confirmParameters\": {\n    \"message\": \"Xác nhận tham số\"\n  },\n  \"searchingForRealKey\": {\n    \"message\": \"Đang tìm kiếm key thực\"\n  },\n  \"verifying\": {\n    \"message\": \"Đang xác minh\"\n  },\n  \"realKeyNotFound\": {\n    \"message\": \"Không tìm thấy key thực\"\n  },\n  \"blockUrl\": {\n    \"message\": \"Chặn URL\"\n  },\n  \"addUrl\": {\n    \"message\": \"Thêm URL\"\n  },\n  \"wildcards\": {\n    \"message\": \"ký tự đại diện\"\n  },\n  \"blockUrlTips\": {\n    \"message\": \"Hỗ trợ ký tự đại diện * và ?\"\n  },\n  \"setWhiteList\": {\n    \"message\": \"Đặt vào danh sách trắng\"\n  },\n  \"autoSend\": {\n    \"message\": \"Tự động truyền dữ liệu\"\n  },\n  \"manualSend\": {\n    \"message\": \"Truyền dữ liệu thủ công\"\n  },\n  \"requestMethod\": {\n    \"message\": \"Phương thức Request\"\n  },\n  \"requestBody\": {\n    \"message\": \"Request Body\"\n  },\n  \"sort\": {\n    \"message\": \"Sắp xếp\"\n  },\n  \"asc\": {\n    \"message\": \"Tăng dần\"\n  },\n  \"desc\": {\n    \"message\": \"Giảm dần\"\n  },\n  \"getTime\": {\n    \"message\": \"Thời gian lấy\"\n  },\n  \"fileSize\": {\n    \"message\": \"Kích thước tệp\"\n  },\n  \"title\": {\n    \"message\": \"Tiêu đề\"\n  },\n  \"noKeyIsRequired\": {\n    \"message\": \"Không yêu cầu key\"\n  },\n  \"estimateSize\": {\n    \"message\": \"Kích thước ước tính\"\n  },\n  \"retryCount\": {\n    \"message\": \"Số lần thử lại\"\n  },\n  \"useSidePanel\": {\n    \"message\": \"Sử dụng bảng điều khiển bên (Side Panel)\"\n  },\n  \"Script\": {\n    \"message\": \"Script\"\n  },\n  \"alwaysSearch\": {\n    \"message\": \"Luôn bật tìm kiếm sâu\"\n  },\n  \"sideurlprotocol\": {\n    \"message\": \"Giao thức URL m3u8dl\"\n  },\n  \"deleteDuplicateFilenames\": {\n    \"message\": \"Xóa tên tệp trùng lặp\"\n  },\n  \"alertimport\": {\n    \"message\": \"Nhập hoàn tất\"\n  },\n  \"isBlockedSite\": {\n    \"message\": \"Trang web này yêu cầu chặn hoạt động của tiện ích mở rộng này\"\n  }\n}\n"
  },
  {
    "path": "_locales/zh_CN/messages.json",
    "content": "{\n  \"catCatch\": {\n    \"message\": \"猫抓\"\n  },\n  \"description\": {\n    \"message\": \"网页媒体嗅探工具\"\n  },\n  \"confirm\": {\n    \"message\": \"确认\"\n  },\n  \"currentPage\": {\n    \"message\": \"当前页面\"\n  },\n  \"otherPage\": {\n    \"message\": \"其他页面\"\n  },\n  \"otherFeatures\": {\n    \"message\": \"其他功能\"\n  },\n  \"mediaControl\": {\n    \"message\": \"媒体控制\"\n  },\n  \"loadingData\": {\n    \"message\": \"数据载入中...\"\n  },\n  \"selectWebpage\": {\n    \"message\": \"选择页面:\"\n  },\n  \"selectMedia\": {\n    \"message\": \"选择媒体:\"\n  },\n  \"noMediaDetected\": {\n    \"message\": \"未检测到有媒体的网页\"\n  },\n  \"noControllableMediaDetected\": {\n    \"message\": \"未检测到可操控媒体\"\n  },\n  \"multiplier\": {\n    \"message\": \"倍数:\"\n  },\n  \"speedPlayback\": {\n    \"message\": \"倍速播放\"\n  },\n  \"play\": {\n    \"message\": \"播放\"\n  },\n  \"normalPlay\": {\n    \"message\": \"正常播放\"\n  },\n  \"pictureInPicture\": {\n    \"message\": \"画中画\"\n  },\n  \"fullscreen\": {\n    \"message\": \"全屏\"\n  },\n  \"screenshot\": {\n    \"message\": \"截图\"\n  },\n  \"loop\": {\n    \"message\": \"循环\"\n  },\n  \"mute\": {\n    \"message\": \"静音\"\n  },\n  \"volume\": {\n    \"message\": \"音 量\"\n  },\n  \"functionEntry\": {\n    \"message\": \"功能入口\"\n  },\n  \"downloader\": {\n    \"message\": \"下载器\"\n  },\n  \"parser\": {\n    \"message\": \"解析\"\n  },\n  \"m3u8Parser\": {\n    \"message\": \"M3U8解析器\"\n  },\n  \"mpdParser\": {\n    \"message\": \"MPD解析器\"\n  },\n  \"jsonFormatter\": {\n    \"message\": \"JSON格式化\"\n  },\n  \"expandAll\": {\n    \"message\": \"展开全部\"\n  },\n  \"expandPlayable\": {\n    \"message\": \"展开可播放\"\n  },\n  \"expandSelected\": {\n    \"message\": \"展开所选\"\n  },\n  \"collapseAll\": {\n    \"message\": \"关闭展开\"\n  },\n  \"videoRecording\": {\n    \"message\": \"视频录制\"\n  },\n  \"closeRecording\": {\n    \"message\": \"关闭录制\"\n  },\n  \"recordWebRTC\": {\n    \"message\": \"录制webRTC\"\n  },\n  \"screenCapture\": {\n    \"message\": \"屏幕捕捉\"\n  },\n  \"simulateMobile\": {\n    \"message\": \"模拟手机\"\n  },\n  \"autoDownload\": {\n    \"message\": \"自动下载\"\n  },\n  \"onlineMerge\": {\n    \"message\": \"在线合并\"\n  },\n  \"download\": {\n    \"message\": \"下载\"\n  },\n  \"copy\": {\n    \"message\": \"复制\"\n  },\n  \"selectAll\": {\n    \"message\": \"全选\"\n  },\n  \"invertSelection\": {\n    \"message\": \"反选\"\n  },\n  \"filter\": {\n    \"message\": \"筛选\"\n  },\n  \"clear\": {\n    \"message\": \"清空\"\n  },\n  \"deepSearch\": {\n    \"message\": \"深度搜索\"\n  },\n  \"closeSearch\": {\n    \"message\": \"关闭搜索\"\n  },\n  \"cacheCapture\": {\n    \"message\": \"缓存捕捉\"\n  },\n  \"closeCapture\": {\n    \"message\": \"关闭捕捉\"\n  },\n  \"moreFeatures\": {\n    \"message\": \"更多功能\"\n  },\n  \"pause\": {\n    \"message\": \"暂停\"\n  },\n  \"settings\": {\n    \"message\": \"设置\"\n  },\n  \"closeSimulation\": {\n    \"message\": \"关闭模拟\"\n  },\n  \"closeDownload\": {\n    \"message\": \"关闭下载\"\n  },\n  \"enable\": {\n    \"message\": \"启用\"\n  },\n  \"disable\": {\n    \"message\": \"禁用\"\n  },\n  \"noData\": {\n    \"message\": \"还没闻到味儿~\"\n  },\n  \"regularFilterPlaceholder\": {\n    \"message\": \"正则筛选 匹配资源url 回车确认\"\n  },\n  \"option\": {\n    \"message\": \"选项\"\n  },\n  \"titleOption\": {\n    \"message\": \"猫抓 设置\"\n  },\n  \"titleDownload\": {\n    \"message\": \"猫抓 下载器\"\n  },\n  \"titleM3U8\": {\n    \"message\": \"猫抓 M3U8解析器\"\n  },\n  \"titleJson\": {\n    \"message\": \"猫抓 json格式化\"\n  },\n  \"titledash\": {\n    \"message\": \"猫抓 dash解析器\"\n  },\n  \"suffix\": {\n    \"message\": \"后缀\"\n  },\n  \"suffixTip\": {\n    \"message\": \"填写不含'.'的后缀, 不过滤大小填0\"\n  },\n  \"extensionName\": {\n    \"message\": \"扩展名\"\n  },\n  \"filterSize\": {\n    \"message\": \"过滤大小\"\n  },\n  \"delete\": {\n    \"message\": \"删除\"\n  },\n  \"addSuffix\": {\n    \"message\": \"添加后缀\"\n  },\n  \"extension\": {\n    \"message\": \"扩展\"\n  },\n  \"disableAll\": {\n    \"message\": \"全部禁用\"\n  },\n  \"enableAll\": {\n    \"message\": \"全部启用\"\n  },\n  \"type\": {\n    \"message\": \"类型\"\n  },\n  \"addType\": {\n    \"message\": \"添加类型\"\n  },\n  \"typeTip\": {\n    \"message\": \"填入正确的content-type类型, 不过滤大小填0\"\n  },\n  \"addTypeError\": {\n    \"message\": \"抓取类型格式错误，请检查\"\n  },\n  \"regexMatch\": {\n    \"message\": \"正则匹配\"\n  },\n  \"blockResource\": {\n    \"message\": \"屏蔽资源\"\n  },\n  \"alert\": {\n    \"message\": \"提示\"\n  },\n  \"regexExpression\": {\n    \"message\": \"正则表达式\"\n  },\n  \"addRegex\": {\n    \"message\": \"添加正则\"\n  },\n  \"regexTest\": {\n    \"message\": \"正则测试\"\n  },\n  \"regex\": {\n    \"message\": \"正则\"\n  },\n  \"flag\": {\n    \"message\": \"标识符\"\n  },\n  \"result\": {\n    \"message\": \"结果\"\n  },\n  \"match\": {\n    \"message\": \"匹配\"\n  },\n  \"noMatch\": {\n    \"message\": \"不匹配\"\n  },\n  \"blockResourceTip\": {\n    \"message\": \"屏蔽不想出现的资源\"\n  },\n  \"flagTip\": {\n    \"message\": \"i: 忽略大小写, g: 全局搜索。也可为空。\"\n  },\n  \"regexSuffixTip\": {\n    \"message\": \"为获取到的URL指定一个后缀名, 可留空，会自动截取后缀名(很多文件并不存在后缀名)\"\n  },\n  \"regexTip\": {\n    \"message\": \"正则表达式占用资源较高, 非必要谨慎使用\"\n  },\n  \"copyTip\": {\n    \"message\": \"为方便使用第三方应用，自定义复制按钮写入剪贴板的内容。\"\n  },\n  \"replaceKeywordList\": {\n    \"message\": \"替换关键词列表\"\n  },\n  \"otherFiles\": {\n    \"message\": \"其他文件\"\n  },\n  \"resetCopySettings\": {\n    \"message\": \"重置复制设置\"\n  },\n  \"autoSetRefererCookieParams\": {\n    \"message\": \"自动设置Referer cookie参数\"\n  },\n  \"secretKey\": {\n    \"message\": \"密钥\"\n  },\n  \"address\": {\n    \"message\": \"地址\"\n  },\n  \"documentation\": {\n    \"message\": \"文档\"\n  },\n  \"aria2Tip\": {\n    \"message\": \"非常优秀的下载工具 使用方法查看\"\n  },\n  \"m3u8DLTips\": {\n    \"message\": \"非常优秀的m3u8 mpd第三方下载工具 使用方法查看\"\n  },\n  \"invoke\": {\n    \"message\": \"调用\"\n  },\n  \"parameter\": {\n    \"message\": \"参数\"\n  },\n  \"parameterSetting\": {\n    \"message\": \"参数设置\"\n  },\n  \"test\": {\n    \"message\": \"测试\"\n  },\n  \"replaceTags\": {\n    \"message\": \"替换标签\"\n  },\n  \"customSaveFileName\": {\n    \"message\": \"自定义保存文件名\"\n  },\n  \"userAgentTip\": {\n    \"message\": \"留空默认为当前浏览器 User Agent\"\n  },\n  \"alwaysDisableCatCatcher\": {\n    \"message\": \"始终不启用猫抓下载器\"\n  },\n  \"autoClosePageAfterDownload\": {\n    \"message\": \"下载完自动关闭页面\"\n  },\n  \"openDownloaderPageInBackground\": {\n    \"message\": \"后台打开下载器页面\"\n  },\n  \"downloaderTip\": {\n    \"message\": \"如果检测到资源下载失败, 自动启用下载器再次尝试下载.\"\n  },\n  \"autoDownM3u8Tip\": {\n    \"message\": \"点击 下载按钮 使用m3u8解析器立即开始合并下载\"\n  },\n  \"otherSettings\": {\n    \"message\": \"其他设置\"\n  },\n  \"resetOtherSettings\": {\n    \"message\": \"重置其他设置\"\n  },\n  \"previewMode\": {\n    \"message\": \"使用本地播放器调用协议打开视频预览\"\n  },\n  \"previewModePlaceholder\": {\n    \"message\": \"留空为不启用 默认使用popup页面预览视频\"\n  },\n  \"preview\": {\n    \"message\": \"预览\"\n  },\n  \"customFilenameOption\": {\n    \"message\": \"使用自定义文件名保存文件(默认为网页标题)\"\n  },\n  \"saveAsOption\": {\n    \"message\": \"下载完选择保存目录\"\n  },\n  \"iconOption\": {\n    \"message\": \"显示网站图标\"\n  },\n  \"clearOption\": {\n    \"message\": \"刷新、跳转到新页面 清空当前标签抓取的数据\"\n  },\n  \"doNotClear\": {\n    \"message\": \"不清空\"\n  },\n  \"normalClear\": {\n    \"message\": \"正常清理\"\n  },\n  \"moreFrequent\": {\n    \"message\": \"更频繁\"\n  },\n  \"dopreview\": {\n    \"message\": \"preview\"\n  },\n  \"dopopup\": {\n    \"message\": \"popup\"\n  },\n  \"winpreview\": {\n    \"message\": \"window preview\"\n  },\n  \"winpopup\": {\n    \"message\": \"window popup\"\n  },\n  \"excludeDuplicateResources\": {\n    \"message\": \"排除重复的资源 (资源过多会占用大量CPU)\"\n  },\n  \"customCSS\": {\n    \"message\": \"自定义CSS\"\n  },\n  \"MQTT\": {\n    \"message\": \"MQTT\"\n  },\n  \"mqttBroker\": {\n    \"message\": \"MQTT代理地址\"\n  },\n  \"mqttPath\": {\n    \"message\": \"路径\"\n  },\n  \"mqttProtocol\": {\n    \"message\": \"协议\"\n  },\n  \"mqttClientId\": {\n    \"message\": \"客户端ID\"\n  },\n  \"mqttTitleLength\": {\n    \"message\": \"标题最大长度\"\n  },\n  \"mqttUsername\": {\n    \"message\": \"用户名\"\n  },\n  \"mqttPassword\": {\n    \"message\": \"密码\"\n  },\n  \"mqttTopic\": {\n    \"message\": \"主题\"\n  },\n  \"mqttQos\": {\n    \"message\": \"QoS级别\"\n  },\n  \"mqttQos0\": {\n    \"message\": \"(最多一次)\"\n  },\n  \"mqttQos1\": {\n    \"message\": \"(至少一次)\"\n  },\n  \"mqttQos2\": {\n    \"message\": \"(确保一次)\"\n  },\n  \"mqttDataFormat\": {\n    \"message\": \"数据格式\"\n  },\n  \"mqttDataFormatHelp\": {\n    \"message\": \"{\\\"url\\\": \\\"$${url}\\\", \\\"title\\\": \\\"$${title}\\\", \\\"type\\\": \\\"$${type}\\\", \\\"ext\\\": \\\"$${ext}\\\", \\\"timestamp\\\": \\\"$${timestamp}\\\"}\"\n  },\n  \"mqttDataFormatVars\": {\n    \"message\": \"可用变量\"\n  },\n  \"mqttDataFormatDefault\": {\n    \"message\": \"留空则使用默认JSON格式\"\n  },\n  \"mqttProtocolWss\": {\n    \"message\": \"WSS (安全)\"\n  },\n  \"mqttProtocolWs\": {\n    \"message\": \"WS (非安全)\"\n  },\n  \"mqttTitleLengthHelp\": {\n    \"message\": \"MQTT消息中标题的最大长度\"\n  },\n  \"mqttBrokerHelp\": {\n    \"message\": \"MQTT代理的主机名或IP地址\"\n  },\n  \"mqttPathHelp\": {\n    \"message\": \"WebSocket路径 (通常为/mqtt或/ws)\"\n  },\n  \"mqttClientIdHelp\": {\n    \"message\": \"此连接的唯一客户端标识符\"\n  },\n  \"mqttTopicHelp\": {\n    \"message\": \"发布消息的MQTT主题\"\n  },\n  \"mqttQosHelp\": {\n    \"message\": \"服务质量级别 (0=最多一次, 1=至少一次, 2=确保一次)\"\n  },\n  \"mqttCredentialsHelp\": {\n    \"message\": \"如不需要认证，请留空用户名和密码\"\n  },\n  \"operation\": {\n    \"message\": \"操作\"\n  },\n  \"exportSettings\": {\n    \"message\": \"导出设置\"\n  },\n  \"importConfiguration\": {\n    \"message\": \"导入配置\"\n  },\n  \"clearCapturedData\": {\n    \"message\": \"清空抓取的数据\"\n  },\n  \"resetSettings\": {\n    \"message\": \"重置设置\"\n  },\n  \"resetAllSettings\": {\n    \"message\": \"重置所有设置\"\n  },\n  \"restartExtension\": {\n    \"message\": \"重启扩展\"\n  },\n  \"about\": {\n    \"message\": \"关于\"\n  },\n  \"confirmReset\": {\n    \"message\": \"确认重置吗？\"\n  },\n  \"invokeProtocolTemplate\": {\n    \"message\": \"调用协议模板\"\n  },\n  \"customVLCProtocol\": {\n    \"message\": \"自定义VLC协议\"\n  },\n  \"systemShare\": {\n    \"message\": \"系统分享\"\n  },\n  \"default\": {\n    \"message\": \"默认\"\n  },\n  \"goBack\": {\n    \"message\": \"返回上一页\"\n  },\n  \"openDir\": {\n    \"message\": \"打开下载目录\"\n  },\n  \"downloadDir\": {\n    \"message\": \"打载目录\"\n  },\n  \"sendFfmpeg\": {\n    \"message\": \"发送到在线ffmpeg\"\n  },\n  \"autoCloserDownload\": {\n    \"message\": \"下载完自动关闭页面\"\n  },\n  \"openInBgDownload\": {\n    \"message\": \"后台打开下载器页面\"\n  },\n  \"m3u8Placeholder\": {\n    \"message\": \"请输入m3u8连接 / m3u8内容 / 切片清单 / $${range}标签\"\n  },\n  \"m3u8Url\": {\n    \"message\": \"m3u8地址\"\n  },\n  \"nextLevel\": {\n    \"message\": \"下一级文件\"\n  },\n  \"nextLevelTip\": {\n    \"message\": \"该m3u8文件中嵌套多个m3u8文件\"\n  },\n  \"multipleAudios\": {\n    \"message\": \"多个音频\"\n  },\n  \"multipleAudiosTip\": {\n    \"message\": \"该m3u8文件中嵌套多个音频\"\n  },\n  \"multipleSubtitles\": {\n    \"message\": \"多个字幕\"\n  },\n  \"multipleSubtitlesTip\": {\n    \"message\": \"该m3u8文件中嵌套多个字幕\"\n  },\n  \"possibleKey\": {\n    \"message\": \"寻找到疑似密钥\"\n  },\n  \"loading\": {\n    \"message\": \"载入中...\"\n  },\n  \"waitDownload\": {\n    \"message\": \"等待下载...\"\n  },\n  \"downloadSegmentList\": {\n    \"message\": \"下载切片列表\"\n  },\n  \"originalM3u8\": {\n    \"message\": \"原始m3u8\"\n  },\n  \"localM3u8\": {\n    \"message\": \"本地m3u8\"\n  },\n  \"segmentList\": {\n    \"message\": \"切片列表\"\n  },\n  \"downloadProgress\": {\n    \"message\": \"下载进度\"\n  },\n  \"getParameters\": {\n    \"message\": \"get参数\"\n  },\n  \"restoreGetParameters\": {\n    \"message\": \"还原get参数\"\n  },\n  \"requestHeaders\": {\n    \"message\": \"请求头\"\n  },\n  \"setRequestHeaders\": {\n    \"message\": \"设置请求头\"\n  },\n  \"invokeM3u8DL\": {\n    \"message\": \"调用m3u8DL\"\n  },\n  \"copyCommand\": {\n    \"message\": \"复制命令\"\n  },\n  \"previewCommand\": {\n    \"message\": \"预览命令\"\n  },\n  \"addSettingParameters\": {\n    \"message\": \"加入设置参数\"\n  },\n  \"customKeyPlaceholder\": {\n    \"message\": \"自定义 密钥 16进制 或 base64 或 密钥地址\"\n  },\n  \"uploadKey\": {\n    \"message\": \"上传密钥\"\n  },\n  \"downloadThreads\": {\n    \"message\": \"下载线程\"\n  },\n  \"ffmpegTranscoding\": {\n    \"message\": \"ffmpeg 转码\"\n  },\n  \"mp4Format\": {\n    \"message\": \"mp4格式\"\n  },\n  \"downloadWhileSaving\": {\n    \"message\": \"边下边存\"\n  },\n  \"audioOnly\": {\n    \"message\": \"只要音频\"\n  },\n  \"saveAs\": {\n    \"message\": \"另存为\"\n  },\n  \"skipDecryption\": {\n    \"message\": \"跳过解密\"\n  },\n  \"newDownloader\": {\n    \"message\": \"新下载器\"\n  },\n  \"downloadRange\": {\n    \"message\": \"下载范围\"\n  },\n  \"recordLive\": {\n    \"message\": \"录制直播\"\n  },\n  \"mergeDownloads\": {\n    \"message\": \"合并下载\"\n  },\n  \"redownloadFailedItems\": {\n    \"message\": \"重下失败项\"\n  },\n  \"downloadExistingData\": {\n    \"message\": \"下载已有数据\"\n  },\n  \"stopDownload\": {\n    \"message\": \"停止下载\"\n  },\n  \"start\": {\n    \"message\": \"开始\"\n  },\n  \"end\": {\n    \"message\": \"结束\"\n  },\n  \"resolution\": {\n    \"message\": \"分辨率\"\n  },\n  \"duration\": {\n    \"message\": \"时长\"\n  },\n  \"bitrate\": {\n    \"message\": \"码率\"\n  },\n  \"ADTSerror\": {\n    \"message\": \"找不到ADTS头 可能是AES-128-ECB加密资源,暂不支持解密.请使用第三方合并软件...\"\n  },\n  \"m3u8Error\": {\n    \"message\": \"解析或播放m3u8文件中有错误, 详细错误信息查看控制台\"\n  },\n  \"noAudio\": {\n    \"message\": \"无音频\"\n  },\n  \"noVideo\": {\n    \"message\": \"无视频\"\n  },\n  \"hevcTip\": {\n    \"message\": \"HEVC/H.265编码切片文件 只支持在线ffmpeg转码\"\n  },\n  \"hevcPreviewTip\": {\n    \"message\": \"HEVC/H.265编码切片文件 不支持预览\"\n  },\n  \"m3u8Info\": {\n    \"message\": \"共 $num$ 个文件, 总时长 $time$\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      },\n      \"time\": {\n        \"content\": \"$2\"\n      }\n    }\n  },\n  \"encryptedHLS\": {\n    \"message\": \"加密HLS\"\n  },\n  \"encryptedSAMPLE\": {\n    \"message\": \"使用SAMPLE-AES-CTR加密的资源, 目前无法处理.\"\n  },\n  \"liveHLS\": {\n    \"message\": \"直播HLS\"\n  },\n  \"keyAddress\": {\n    \"message\": \"密钥地址\"\n  },\n  \"key\": {\n    \"message\": \"密钥\"\n  },\n  \"encryptionAlgorithm\": {\n    \"message\": \"加密算法\"\n  },\n  \"keyDownloadFailed\": {\n    \"message\": \"密钥下载失败\"\n  },\n  \"savePrompt\": {\n    \"message\": \"已保存到硬盘, 请查看浏览器已下载内容\"\n  },\n  \"close\": {\n    \"message\": \"关闭\"\n  },\n  \"blobM3u8DLError\": {\n    \"message\": \"blob地址无法调用m3u8DL下载\"\n  },\n  \"M3U8DLparameterLong\": {\n    \"message\": \"m3u8dl参数太长,可能导致无法唤醒m3u8DL, 请手动复制到m3u8DL下载\"\n  },\n  \"runningCannotChangeSettings\": {\n    \"message\": \"正在运行, 不能更改设置\"\n  },\n  \"streamSaverTip\": {\n    \"message\": \"边下边存功能 不支持ffmpeg在线转换格式 不支持错误切片重下 不支持另存为\"\n  },\n  \"stopRecording\": {\n    \"message\": \"停止录制\"\n  },\n  \"waitingForLiveData\": {\n    \"message\": \"等待直播数据\"\n  },\n  \"sNumError\": {\n    \"message\": \"序号错误\"\n  },\n  \"startGTend\": {\n    \"message\": \"开始序号不能大于结束序号\"\n  },\n  \"sNumMax\": {\n    \"message\": \"序号最大不能超过 $num$\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"incorrectKey\": {\n    \"message\": \"密钥不正确\"\n  },\n  \"addParameters\": {\n    \"message\": \"添加参数\"\n  },\n  \"decryptionError\": {\n    \"message\": \"解密错误\"\n  },\n  \"downloadFailed\": {\n    \"message\": \"下载失败\"\n  },\n  \"retryDownload\": {\n    \"message\": \"重新下载\"\n  },\n  \"recordingDuration\": {\n    \"message\": \"录制时长\"\n  },\n  \"downloaded\": {\n    \"message\": \"已下载\"\n  },\n  \"downloadedVideoLength\": {\n    \"message\": \"已下载视频长度\"\n  },\n  \"downloadComplete\": {\n    \"message\": \"下载完成\"\n  },\n  \"retryingDownload\": {\n    \"message\": \"正在重新下载\"\n  },\n  \"merging\": {\n    \"message\": \"合并中\"\n  },\n  \"fileTooLarge\": {\n    \"message\": \"文件大于$size$ 无法使用在线ffmpeg, 正在下载合并文件, 文件较大请耐心等待\",\n    \"placeholders\": {\n      \"size\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"fileTooLargeStream\": {\n    \"message\": \"文件大于 $size$ 是否启用边下边存?\",\n    \"placeholders\": {\n      \"size\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"formatConversionError\": {\n    \"message\": \"格式转换错误, 请取消mp4转换, 重新下载.\"\n  },\n  \"streamOnbeforeunload\": {\n    \"message\": \"正在推流, 关闭后停止下载...\"\n  },\n  \"fileLoading\": {\n    \"message\": \"文件载入中\"\n  },\n  \"expandAllNodes\": {\n    \"message\": \"展开所有节点\"\n  },\n  \"collapseAllNodes\": {\n    \"message\": \"折叠所有节点\"\n  },\n  \"fileRetrievalFailed\": {\n    \"message\": \"文件获取失败\"\n  },\n  \"selectVideo\": {\n    \"message\": \"选择视频\"\n  },\n  \"extractSlices\": {\n    \"message\": \"提取切片\"\n  },\n  \"convertToM3U8\": {\n    \"message\": \"转为m3u8解析\"\n  },\n  \"selectAudio\": {\n    \"message\": \"选择音频\"\n  },\n  \"audio\": {\n    \"message\": \"音频\"\n  },\n  \"video\": {\n    \"message\": \"视频\"\n  },\n  \"DRMerror\": {\n    \"message\": \"媒体有DRM保护, 请使用第三方工具下载.\"\n  },\n  \"regexTitle\": {\n    \"message\": \"正则表达式匹配 或 来自深度搜索\"\n  },\n  \"downloadWithRequestHeader\": {\n    \"message\": \"携带请求头参数下载\"\n  },\n  \"m3u8Playlist\": {\n    \"message\": \"m3u8播放列表\"\n  },\n  \"copiedToClipboard\": {\n    \"message\": \"已复制到剪贴板\"\n  },\n  \"hasSent\": {\n    \"message\": \"已发送\"\n  },\n  \"sendFailed\": {\n    \"message\": \"发送失败\"\n  },\n  \"confirmDownload\": {\n    \"message\": \"共 $num$ 个文件，是否确认下载?\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"confirmLoading\": {\n    \"message\": \"共 $num$ 条资源，是否取消加载?\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"waitingForMedia\": {\n    \"message\": \"等待接收媒体文件...请勿关闭本页面...\"\n  },\n  \"exit\": {\n    \"message\": \"退出\"\n  },\n  \"unknownSize\": {\n    \"message\": \"未知大小\"\n  },\n  \"saving\": {\n    \"message\": \"正在保存\"\n  },\n  \"saveFailed\": {\n    \"message\": \"保存失败\"\n  },\n  \"badgeNumber\": {\n    \"message\": \"图标上显示数字角标\"\n  },\n  \"viewSlices\": {\n    \"message\": \"查看所有切片和下载进度\"\n  },\n  \"send2local\": {\n    \"message\": \"数据发送\"\n  },\n  \"send2MQTT\": {\n    \"message\": \"发送到 MQTT\"\n  },\n  \"sendingToMQTT\": {\n    \"message\": \"正在发送到MQTT服务器...\"\n  },\n  \"connectingToMQTT\": {\n    \"message\": \"正在连接 MQTT 服务器...\"\n  },\n  \"sendingMessageToMQTT\": {\n    \"message\": \"正在发送消息到 MQTT 服务器...\"\n  },\n  \"messageSentToMQTT\": {\n    \"message\": \"已发送消息到 MQTT 服务器\"\n  },\n  \"popup\": {\n    \"message\": \"弹出\"\n  },\n  \"defaultPopup\": {\n    \"message\": \"默认弹出模式\"\n  },\n  \"invokeApp\": {\n    \"message\": \"调用程序\"\n  },\n  \"onlineServiceAddress\": {\n    \"message\": \"在线服务地址\"\n  },\n  \"withinChina\": {\n    \"message\": \"中国境内\"\n  },\n  \"dataFetchFailed\": {\n    \"message\": \"数据获取失败\"\n  },\n  \"confirmParameters\": {\n    \"message\": \"确认参数\"\n  },\n  \"searchingForRealKey\": {\n    \"message\": \"寻找真实密钥\"\n  },\n  \"verifying\": {\n    \"message\": \"验证中\"\n  },\n  \"realKeyNotFound\": {\n    \"message\": \"未找到真实密钥\"\n  },\n  \"blockUrl\": {\n    \"message\": \"屏蔽网址\"\n  },\n  \"addUrl\": {\n    \"message\": \"添加网址\"\n  },\n  \"wildcards\": {\n    \"message\": \"通配符\"\n  },\n  \"blockUrlTips\": {\n    \"message\": \"支持通配符 * 和 ?\"\n  },\n  \"setWhiteList\": {\n    \"message\": \"设置为白名单\"\n  },\n  \"autoSend\": {\n    \"message\": \"自动发送\"\n  },\n  \"manualSend\": {\n    \"message\": \"手动发送\"\n  },\n  \"requestMethod\": {\n    \"message\": \"请求方式\"\n  },\n  \"requestBody\": {\n    \"message\": \"请求体\"\n  },\n  \"sort\": {\n    \"message\": \"排序\"\n  },\n  \"asc\": {\n    \"message\": \"升序\"\n  },\n  \"desc\": {\n    \"message\": \"降序\"\n  },\n  \"getTime\": {\n    \"message\": \"获取时间\"\n  },\n  \"fileSize\": {\n    \"message\": \"文件大小\"\n  },\n  \"title\": {\n    \"message\": \"标题\"\n  },\n  \"noKeyIsRequired\": {\n    \"message\": \"不需要密钥\"\n  },\n  \"estimateSize\": {\n    \"message\": \"估算大小\"\n  },\n  \"retryCount\": {\n    \"message\": \"重试次数\"\n  },\n  \"useSidePanel\": {\n    \"message\": \"使用侧边栏\"\n  },\n  \"Script\": {\n    \"message\": \"脚本\"\n  },\n  \"alwaysSearch\": {\n    \"message\": \"始终打开深度搜索\"\n  },\n  \"sideurlprotocol\": {\n    \"message\": \"m3u8dl 自定义协议\"\n  },\n  \"deleteDuplicateFilenames\": {\n    \"message\": \"删除重复文件名\"\n  },\n  \"alertimport\": {\n    \"message\": \"导入完成\"\n  },\n  \"isBlockedSite\": {\n    \"message\": \"该网站要求禁止运行本程序\"\n  }\n}"
  },
  {
    "path": "_locales/zh_TW/messages.json",
    "content": "{\n  \"catCatch\": {\n    \"message\": \"貓抓\"\n  },\n  \"description\": {\n    \"message\": \"網頁媒體嗅探工具\"\n  },\n  \"confirm\": {\n    \"message\": \"確認\"\n  },\n  \"currentPage\": {\n    \"message\": \"當前頁面\"\n  },\n  \"otherPage\": {\n    \"message\": \"其他頁面\"\n  },\n  \"otherFeatures\": {\n    \"message\": \"其他功能\"\n  },\n  \"mediaControl\": {\n    \"message\": \"媒體控制\"\n  },\n  \"loadingData\": {\n    \"message\": \"資料載入...\"\n  },\n  \"selectWebpage\": {\n    \"message\": \"選擇頁面:\"\n  },\n  \"selectMedia\": {\n    \"message\": \"選擇媒體:\"\n  },\n  \"noMediaDetected\": {\n    \"message\": \"未偵測到有媒體的網頁\"\n  },\n  \"noControllableMediaDetected\": {\n    \"message\": \"未偵測到可操控媒體\"\n  },\n  \"multiplier\": {\n    \"message\": \"倍數:\"\n  },\n  \"speedPlayback\": {\n    \"message\": \"倍速播放\"\n  },\n  \"play\": {\n    \"message\": \"播放\"\n  },\n  \"normalPlay\": {\n    \"message\": \"正常播放\"\n  },\n  \"pictureInPicture\": {\n    \"message\": \"畫中畫\"\n  },\n  \"fullscreen\": {\n    \"message\": \"全螢幕\"\n  },\n  \"screenshot\": {\n    \"message\": \"截圖\"\n  },\n  \"loop\": {\n    \"message\": \"循環\"\n  },\n  \"mute\": {\n    \"message\": \"靜音\"\n  },\n  \"volume\": {\n    \"message\": \"音 量\"\n  },\n  \"functionEntry\": {\n    \"message\": \"功能入口\"\n  },\n  \"downloader\": {\n    \"message\": \"下載器\"\n  },\n  \"parser\": {\n    \"message\": \"解析\"\n  },\n  \"m3u8Parser\": {\n    \"message\": \"M3U8解析器\"\n  },\n  \"mpdParser\": {\n    \"message\": \"MPD解析器\"\n  },\n  \"jsonFormatter\": {\n    \"message\": \"JSON格式化\"\n  },\n  \"expandAll\": {\n    \"message\": \"展開全部\"\n  },\n  \"expandPlayable\": {\n    \"message\": \"展開可播放\"\n  },\n  \"expandSelected\": {\n    \"message\": \"展開所選\"\n  },\n  \"collapseAll\": {\n    \"message\": \"關閉展開\"\n  },\n  \"videoRecording\": {\n    \"message\": \"錄影\"\n  },\n  \"closeRecording\": {\n    \"message\": \"關閉錄製\"\n  },\n  \"recordWebRTC\": {\n    \"message\": \"錄製webRTC\"\n  },\n  \"screenCapture\": {\n    \"message\": \"螢幕捕捉\"\n  },\n  \"simulateMobile\": {\n    \"message\": \"模擬手機\"\n  },\n  \"autoDownload\": {\n    \"message\": \"自動下載\"\n  },\n  \"onlineMerge\": {\n    \"message\": \"線上合併\"\n  },\n  \"download\": {\n    \"message\": \"下載\"\n  },\n  \"copy\": {\n    \"message\": \"複製\"\n  },\n  \"selectAll\": {\n    \"message\": \"全選\"\n  },\n  \"invertSelection\": {\n    \"message\": \"反選\"\n  },\n  \"filter\": {\n    \"message\": \"篩選\"\n  },\n  \"clear\": {\n    \"message\": \"清空\"\n  },\n  \"deepSearch\": {\n    \"message\": \"深度搜尋\"\n  },\n  \"closeSearch\": {\n    \"message\": \"關閉搜尋\"\n  },\n  \"cacheCapture\": {\n    \"message\": \"快取捕捉\"\n  },\n  \"closeCapture\": {\n    \"message\": \"關閉捕捉\"\n  },\n  \"moreFeatures\": {\n    \"message\": \"更多功能\"\n  },\n  \"pause\": {\n    \"message\": \"暫停\"\n  },\n  \"settings\": {\n    \"message\": \"設定\"\n  },\n  \"closeSimulation\": {\n    \"message\": \"關閉模擬\"\n  },\n  \"closeDownload\": {\n    \"message\": \"關閉下載\"\n  },\n  \"enable\": {\n    \"message\": \"啟用\"\n  },\n  \"disable\": {\n    \"message\": \"禁用\"\n  },\n  \"noData\": {\n    \"message\": \"還沒聞到味兒~\"\n  },\n  \"regularFilterPlaceholder\": {\n    \"message\": \"正規篩選 匹配資源url 回車確認\"\n  },\n  \"option\": {\n    \"message\": \"選項\"\n  },\n  \"titleOption\": {\n    \"message\": \"貓抓 設定\"\n  },\n  \"titleDownload\": {\n    \"message\": \"貓抓 下載器\"\n  },\n  \"titleM3U8\": {\n    \"message\": \"貓抓 M3U8解析器\"\n  },\n  \"titleJson\": {\n    \"message\": \"貓抓 json格式化\"\n  },\n  \"titledash\": {\n    \"message\": \"貓抓 dash解析器\"\n  },\n  \"suffix\": {\n    \"message\": \"後綴\"\n  },\n  \"suffixTip\": {\n    \"message\": \"填入不含'.'的字尾, 不過濾大小填0\"\n  },\n  \"extensionName\": {\n    \"message\": \"副檔名\"\n  },\n  \"filterSize\": {\n    \"message\": \"過濾大小\"\n  },\n  \"delete\": {\n    \"message\": \"刪除\"\n  },\n  \"addSuffix\": {\n    \"message\": \"新增後綴\"\n  },\n  \"extension\": {\n    \"message\": \"擴充\"\n  },\n  \"disableAll\": {\n    \"message\": \"全部停用\"\n  },\n  \"enableAll\": {\n    \"message\": \"全部啟用\"\n  },\n  \"type\": {\n    \"message\": \"類型\"\n  },\n  \"addType\": {\n    \"message\": \"新增類型\"\n  },\n  \"typeTip\": {\n    \"message\": \"填入正確的content-type型別, 不過濾大小填0\"\n  },\n  \"addTypeError\": {\n    \"message\": \"抓取類型格式錯誤，請檢查\"\n  },\n  \"regexMatch\": {\n    \"message\": \"正規符合\"\n  },\n  \"blockResource\": {\n    \"message\": \"屏蔽資源\"\n  },\n  \"alert\": {\n    \"message\": \"提示\"\n  },\n  \"regexExpression\": {\n    \"message\": \"正規表示式\"\n  },\n  \"addRegex\": {\n    \"message\": \"新增正規則\"\n  },\n  \"regexTest\": {\n    \"message\": \"正規測試\"\n  },\n  \"regex\": {\n    \"message\": \"正規\"\n  },\n  \"flag\": {\n    \"message\": \"識別字\"\n  },\n  \"result\": {\n    \"message\": \"結果\"\n  },\n  \"match\": {\n    \"message\": \"匹配\"\n  },\n  \"noMatch\": {\n    \"message\": \"不匹配\"\n  },\n  \"blockResourceTip\": {\n    \"message\": \"封鎖不想出現的資源\"\n  },\n  \"flagTip\": {\n    \"message\": \"i: 忽略大小寫, g: 全域搜尋。也可為空。\"\n  },\n  \"regexSuffixTip\": {\n    \"message\": \"為取得到的URL指定一個後綴名, 可留空，會自動截取後綴名(很多檔案並不存在後綴名)\"\n  },\n  \"regexTip\": {\n    \"message\": \"正規表示式佔用資源較高, 非必要謹慎使用\"\n  },\n  \"copyTip\": {\n    \"message\": \"為方便使用第三方應用，自訂複製按鈕寫入剪貼簿的內容。\"\n  },\n  \"replaceKeywordList\": {\n    \"message\": \"替換關鍵字清單\"\n  },\n  \"otherFiles\": {\n    \"message\": \"其他文件\"\n  },\n  \"resetCopySettings\": {\n    \"message\": \"重設複製設定\"\n  },\n  \"autoSetRefererCookieParams\": {\n    \"message\": \"自動設定Referer cookie參數\"\n  },\n  \"secretKey\": {\n    \"message\": \"金鑰\"\n  },\n  \"address\": {\n    \"message\": \"地址\"\n  },\n  \"documentation\": {\n    \"message\": \"文檔\"\n  },\n  \"aria2Tip\": {\n    \"message\": \"非常優秀的下載工具 使用方法檢視\"\n  },\n  \"m3u8DLTips\": {\n    \"message\": \"非常優秀的m3u8 mpd第三方下載工具 使用方法查看\"\n  },\n  \"invoke\": {\n    \"message\": \"呼叫\"\n  },\n  \"parameter\": {\n    \"message\": \"參數\"\n  },\n  \"parameterSetting\": {\n    \"message\": \"參數設定\"\n  },\n  \"test\": {\n    \"message\": \"測試\"\n  },\n  \"replaceTags\": {\n    \"message\": \"替換標籤\"\n  },\n  \"customSaveFileName\": {\n    \"message\": \"自訂儲存檔案名稱\"\n  },\n  \"userAgentTip\": {\n    \"message\": \"留空預設為目前瀏覽器 User Agent\"\n  },\n  \"alwaysDisableCatCatcher\": {\n    \"message\": \"永遠不啟用貓抓下載器\"\n  },\n  \"autoClosePageAfterDownload\": {\n    \"message\": \"下載完自動關閉頁面\"\n  },\n  \"openDownloaderPageInBackground\": {\n    \"message\": \"後台開啟下載器頁面\"\n  },\n  \"downloaderTip\": {\n    \"message\": \"如果偵測到資源下載失敗, 自動啟用下載器再次嘗試下載.\"\n  },\n  \"autoDownM3u8Tip\": {\n    \"message\": \"點擊 下載按鈕 使用m3u8解析器立即開始合併下載\"\n  },\n  \"otherSettings\": {\n    \"message\": \"其他設定\"\n  },\n  \"resetOtherSettings\": {\n    \"message\": \"重設其他設定\"\n  },\n  \"previewMode\": {\n    \"message\": \"使用本機播放器呼叫協定開啟影片預覽\"\n  },\n  \"previewModePlaceholder\": {\n    \"message\": \"留空為不啟用 預設使用popup頁面預覽影片\"\n  },\n  \"preview\": {\n    \"message\": \"影片\"\n  },\n  \"customFilenameOption\": {\n    \"message\": \"使用自訂檔案名稱儲存檔案(預設為網頁標題)\"\n  },\n  \"saveAsOption\": {\n    \"message\": \"下載完選擇儲存目錄\"\n  },\n  \"iconOption\": {\n    \"message\": \"顯示網站圖示\"\n  },\n  \"clearOption\": {\n    \"message\": \"刷新、跳到新頁面 清空目前標籤抓取的資料\"\n  },\n  \"doNotClear\": {\n    \"message\": \"不清空\"\n  },\n  \"normalClear\": {\n    \"message\": \"正常清理\"\n  },\n  \"moreFrequent\": {\n    \"message\": \"更頻繁\"\n  },\n  \"dopreview\": {\n    \"message\": \"preview\"\n  },\n  \"dopopup\": {\n    \"message\": \"popup\"\n  },\n  \"winpreview\": {\n    \"message\": \"window preview\"\n  },\n  \"winpopup\": {\n    \"message\": \"window popup\"\n  },\n  \"excludeDuplicateResources\": {\n    \"message\": \"排除重複的資源 (資源過多會佔用大量CPU)\"\n  },\n  \"customCSS\": {\n    \"message\": \"自訂CSS\"\n  },\n  \"MQTT\": {\n    \"message\": \"MQTT\"\n  },\n  \"mqttBroker\": {\n    \"message\": \"MQTT代理位址\"\n  },\n  \"mqttPath\": {\n    \"message\": \"路徑\"\n  },\n  \"mqttProtocol\": {\n    \"message\": \"通訊協定\"\n  },\n  \"mqttClientId\": {\n    \"message\": \"客戶端ID\"\n  },\n  \"mqttTitleLength\": {\n    \"message\": \"標題最大長度\"\n  },\n  \"mqttUsername\": {\n    \"message\": \"使用者名稱\"\n  },\n  \"mqttPassword\": {\n    \"message\": \"密碼\"\n  },\n  \"mqttTopic\": {\n    \"message\": \"主題\"\n  },\n  \"mqttQos\": {\n    \"message\": \"QoS級別\"\n  },\n  \"mqttQos0\": {\n    \"message\": \"(最多一次)\"\n  },\n  \"mqttQos1\": {\n    \"message\": \"(至少一次)\"\n  },\n  \"mqttQos2\": {\n    \"message\": \"(確保一次)\"\n  },\n  \"mqttDataFormat\": {\n    \"message\": \"資料格式\"\n  },\n  \"mqttDataFormatHelp\": {\n    \"message\": \"{\\\"url\\\": \\\"$${url}\\\", \\\"title\\\": \\\"$${title}\\\", \\\"type\\\": \\\"$${type}\\\", \\\"ext\\\": \\\"$${ext}\\\", \\\"timestamp\\\": \\\"$${timestamp}\\\"}\"\n  },\n  \"mqttDataFormatVars\": {\n    \"message\": \"可用變數\"\n  },\n  \"mqttDataFormatDefault\": {\n    \"message\": \"留空則使用預設JSON格式\"\n  },\n  \"mqttProtocolWss\": {\n    \"message\": \"WSS (安全)\"\n  },\n  \"mqttProtocolWs\": {\n    \"message\": \"WS (非安全)\"\n  },\n  \"mqttTitleLengthHelp\": {\n    \"message\": \"MQTT訊息中標題的最大長度\"\n  },\n  \"mqttBrokerHelp\": {\n    \"message\": \"MQTT代理的主機名稱或IP位址\"\n  },\n  \"mqttPathHelp\": {\n    \"message\": \"WebSocket路徑 (通常為/mqtt或/ws)\"\n  },\n  \"mqttClientIdHelp\": {\n    \"message\": \"此連線的唯一客戶端識別碼\"\n  },\n  \"mqttTopicHelp\": {\n    \"message\": \"發佈訊息的MQTT主題\"\n  },\n  \"mqttQosHelp\": {\n    \"message\": \"服務品質級別 (0=最多一次, 1=至少一次, 2=確保一次)\"\n  },\n  \"mqttCredentialsHelp\": {\n    \"message\": \"如不需要認證，請留空使用者名稱和密碼\"\n  },\n  \"operation\": {\n    \"message\": \"操作\"\n  },\n  \"exportSettings\": {\n    \"message\": \"匯出設定\"\n  },\n  \"importConfiguration\": {\n    \"message\": \"導入配置\"\n  },\n  \"clearCapturedData\": {\n    \"message\": \"清空抓取的資料\"\n  },\n  \"resetSettings\": {\n    \"message\": \"重置設定\"\n  },\n  \"resetAllSettings\": {\n    \"message\": \"重置所有設定\"\n  },\n  \"restartExtension\": {\n    \"message\": \"重啟擴充\"\n  },\n  \"about\": {\n    \"message\": \"關於\"\n  },\n  \"confirmReset\": {\n    \"message\": \"確認重設嗎？\"\n  },\n  \"invokeProtocolTemplate\": {\n    \"message\": \"呼叫協定範本\"\n  },\n  \"customVLCProtocol\": {\n    \"message\": \"自訂VLC協定\"\n  },\n  \"systemShare\": {\n    \"message\": \"系統分享\"\n  },\n  \"default\": {\n    \"message\": \"預設\"\n  },\n  \"goBack\": {\n    \"message\": \"返回上一頁\"\n  },\n  \"openDir\": {\n    \"message\": \"開啟下載目錄\"\n  },\n  \"downloadDir\": {\n    \"message\": \"打載目錄\"\n  },\n  \"sendFfmpeg\": {\n    \"message\": \"發送到線上ffmpeg\"\n  },\n  \"autoCloserDownload\": {\n    \"message\": \"下載完自動關閉頁面\"\n  },\n  \"openInBgDownload\": {\n    \"message\": \"後台開啟下載器頁面\"\n  },\n  \"m3u8Placeholder\": {\n    \"message\": \"請輸入m3u8連結 / m3u8內容 / 切片清單 / $${range}標籤\"\n  },\n  \"m3u8Url\": {\n    \"message\": \"m3u8位址\"\n  },\n  \"nextLevel\": {\n    \"message\": \"下一級檔案\"\n  },\n  \"nextLevelTip\": {\n    \"message\": \"該m3u8檔中嵌套多個m3u8檔\"\n  },\n  \"multipleAudios\": {\n    \"message\": \"多個音訊\"\n  },\n  \"multipleAudiosTip\": {\n    \"message\": \"該m3u8檔中嵌套多個音訊\"\n  },\n  \"multipleSubtitles\": {\n    \"message\": \"多個字幕\"\n  },\n  \"multipleSubtitlesTip\": {\n    \"message\": \"該m3u8檔中嵌套多個字幕\"\n  },\n  \"possibleKey\": {\n    \"message\": \"尋找到疑似金鑰\"\n  },\n  \"loading\": {\n    \"message\": \"載入中...\"\n  },\n  \"waitDownload\": {\n    \"message\": \"等待下載...\"\n  },\n  \"downloadSegmentList\": {\n    \"message\": \"下載切片清單\"\n  },\n  \"originalM3u8\": {\n    \"message\": \"原始m3u8\"\n  },\n  \"localM3u8\": {\n    \"message\": \"本地m3u8\"\n  },\n  \"segmentList\": {\n    \"message\": \"切片清單\"\n  },\n  \"downloadProgress\": {\n    \"message\": \"下載進度\"\n  },\n  \"getParameters\": {\n    \"message\": \"get參數\"\n  },\n  \"restoreGetParameters\": {\n    \"message\": \"還原get參數\"\n  },\n  \"requestHeaders\": {\n    \"message\": \"請求頭\"\n  },\n  \"setRequestHeaders\": {\n    \"message\": \"設定請求頭\"\n  },\n  \"invokeM3u8DL\": {\n    \"message\": \"呼叫m3u8DL\"\n  },\n  \"copyCommand\": {\n    \"message\": \"複製指令\"\n  },\n  \"previewCommand\": {\n    \"message\": \"預覽指令\"\n  },\n  \"addSettingParameters\": {\n    \"message\": \"加入設定參數\"\n  },\n  \"customKeyPlaceholder\": {\n    \"message\": \"自訂 金鑰 16進位 或 base64 或 金鑰位址\"\n  },\n  \"uploadKey\": {\n    \"message\": \"上傳金鑰\"\n  },\n  \"downloadThreads\": {\n    \"message\": \"下載執行緒\"\n  },\n  \"ffmpegTranscoding\": {\n    \"message\": \"ffmpeg 轉碼\"\n  },\n  \"mp4Format\": {\n    \"message\": \"mp4格式\"\n  },\n  \"downloadWhileSaving\": {\n    \"message\": \"邊下邊存\"\n  },\n  \"audioOnly\": {\n    \"message\": \"只要音訊\"\n  },\n  \"saveAs\": {\n    \"message\": \"另存為\"\n  },\n  \"skipDecryption\": {\n    \"message\": \"跳過解密\"\n  },\n  \"newDownloader\": {\n    \"message\": \"新下載器\"\n  },\n  \"downloadRange\": {\n    \"message\": \"下載範圍\"\n  },\n  \"recordLive\": {\n    \"message\": \"錄音直播\"\n  },\n  \"mergeDownloads\": {\n    \"message\": \"合併下載\"\n  },\n  \"redownloadFailedItems\": {\n    \"message\": \"重下失敗項目\"\n  },\n  \"downloadExistingData\": {\n    \"message\": \"下載已有資料\"\n  },\n  \"stopDownload\": {\n    \"message\": \"停止下載\"\n  },\n  \"start\": {\n    \"message\": \"開始\"\n  },\n  \"end\": {\n    \"message\": \"結束\"\n  },\n  \"resolution\": {\n    \"message\": \"解析度\"\n  },\n  \"duration\": {\n    \"message\": \"長度\"\n  },\n  \"bitrate\": {\n    \"message\": \"碼率\"\n  },\n  \"ADTSerror\": {\n    \"message\": \"找不到ADTS頭 可能是AES-128-ECB加密資源,暫不支援解密.請使用第三方合併軟體...\"\n  },\n  \"m3u8Error\": {\n    \"message\": \"解析或播放m3u8檔案中有錯誤, 詳細錯誤訊息檢視控制台\"\n  },\n  \"noAudio\": {\n    \"message\": \"無音訊\"\n  },\n  \"noVideo\": {\n    \"message\": \"無視訊\"\n  },\n  \"hevcTip\": {\n    \"message\": \"HEVC/H.265編碼切片檔案 只支援線上ffmpeg轉碼\"\n  },\n  \"hevcPreviewTip\": {\n    \"message\": \"HEVC/H.265編碼切片檔案 不支援預覽\"\n  },\n  \"m3u8Info\": {\n    \"message\": \"共 $num$ 個檔案, 總長度 $time$\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      },\n      \"time\": {\n        \"content\": \"$2\"\n      }\n    }\n  },\n  \"encryptedHLS\": {\n    \"message\": \"加密HLS\"\n  },\n  \"encryptedSAMPLE\": {\n    \"message\": \"使用SAMPLE-AES-CTR加密的資源, 目前無法處理.\"\n  },\n  \"liveHLS\": {\n    \"message\": \"直播HLS\"\n  },\n  \"keyAddress\": {\n    \"message\": \"金鑰位址\"\n  },\n  \"key\": {\n    \"message\": \"金鑰\"\n  },\n  \"encryptionAlgorithm\": {\n    \"message\": \"加密演算法\"\n  },\n  \"keyDownloadFailed\": {\n    \"message\": \"金鑰下載失敗\"\n  },\n  \"savePrompt\": {\n    \"message\": \"已儲存至硬碟, 請檢視瀏覽器已下載內容\"\n  },\n  \"close\": {\n    \"message\": \"關閉\"\n  },\n  \"blobM3u8DLError\": {\n    \"message\": \"blob位址無法呼叫m3u8DL下載\"\n  },\n  \"M3U8DLparameterLong\": {\n    \"message\": \"m3u8dl參數太長,可能導致無法喚醒m3u8DL, 請手動複製到m3u8DL下載\"\n  },\n  \"runningCannotChangeSettings\": {\n    \"message\": \"正在運作, 不能更改設定\"\n  },\n  \"streamSaverTip\": {\n    \"message\": \"邊下邊存功能 不支援ffmpeg線上轉換格式 不支援錯誤切片重下 不支援另存為\"\n  },\n  \"stopRecording\": {\n    \"message\": \"停止錄製\"\n  },\n  \"waitingForLiveData\": {\n    \"message\": \"等待直播資料\"\n  },\n  \"sNumError\": {\n    \"message\": \"序號錯誤\"\n  },\n  \"startGTend\": {\n    \"message\": \"開始序號不能大於結束序號\"\n  },\n  \"sNumMax\": {\n    \"message\": \"序號最大不能超過 $num$\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"incorrectKey\": {\n    \"message\": \"金鑰不正確\"\n  },\n  \"addParameters\": {\n    \"message\": \"新增參數\"\n  },\n  \"decryptionError\": {\n    \"message\": \"解密錯誤\"\n  },\n  \"downloadFailed\": {\n    \"message\": \"下載失敗\"\n  },\n  \"retryDownload\": {\n    \"message\": \"重新下載\"\n  },\n  \"recordingDuration\": {\n    \"message\": \"錄製時間\"\n  },\n  \"downloaded\": {\n    \"message\": \"已下載\"\n  },\n  \"downloadedVideoLength\": {\n    \"message\": \"已下載影片長度\"\n  },\n  \"downloadComplete\": {\n    \"message\": \"下載完成\"\n  },\n  \"retryingDownload\": {\n    \"message\": \"正在重新下載\"\n  },\n  \"merging\": {\n    \"message\": \"合併中\"\n  },\n  \"fileTooLarge\": {\n    \"message\": \"檔案大於$size$ 無法使用線上ffmpeg, 正在下載合併檔案, 檔案較大請耐心等待\",\n    \"placeholders\": {\n      \"size\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"fileTooLargeStream\": {\n    \"message\": \"文件大於 $size$ 是否啟用邊下邊存?\",\n    \"placeholders\": {\n      \"size\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"formatConversionError\": {\n    \"message\": \"格式轉換錯誤, 請取消mp4轉換, 重新下載.\"\n  },\n  \"streamOnbeforeunload\": {\n    \"message\": \"正在推流, 關閉後停止下載...\"\n  },\n  \"fileLoading\": {\n    \"message\": \"檔案載入\"\n  },\n  \"expandAllNodes\": {\n    \"message\": \"展開所有節點\"\n  },\n  \"collapseAllNodes\": {\n    \"message\": \"折疊所有節點\"\n  },\n  \"fileRetrievalFailed\": {\n    \"message\": \"文件取得失敗\"\n  },\n  \"selectVideo\": {\n    \"message\": \"選擇影片\"\n  },\n  \"extractSlices\": {\n    \"message\": \"提取切片\"\n  },\n  \"convertToM3U8\": {\n    \"message\": \"轉為m3u8解析\"\n  },\n  \"selectAudio\": {\n    \"message\": \"選擇音訊\"\n  },\n  \"audio\": {\n    \"message\": \"音訊\"\n  },\n  \"video\": {\n    \"message\": \"影片\"\n  },\n  \"DRMerror\": {\n    \"message\": \"媒體有DRM保護, 請使用第三方工具下載.\"\n  },\n  \"regexTitle\": {\n    \"message\": \"正規表示式匹配 或 來自深度搜尋\"\n  },\n  \"downloadWithRequestHeader\": {\n    \"message\": \"攜帶請求頭參數下載\"\n  },\n  \"m3u8Playlist\": {\n    \"message\": \"m3u8播放清單\"\n  },\n  \"copiedToClipboard\": {\n    \"message\": \"已複製到剪貼簿\"\n  },\n  \"hasSent\": {\n    \"message\": \"已發送\"\n  },\n  \"sendFailed\": {\n    \"message\": \"發送失敗\"\n  },\n  \"confirmDownload\": {\n    \"message\": \"共 $num$ 個文件，是否確認下載?\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"confirmLoading\": {\n    \"message\": \"共 $num$ 個資源，是否取消載入?\",\n    \"placeholders\": {\n      \"num\": {\n        \"content\": \"$1\"\n      }\n    }\n  },\n  \"waitingForMedia\": {\n    \"message\": \"等待接收媒體檔案...請勿關閉本頁...\"\n  },\n  \"exit\": {\n    \"message\": \"退出\"\n  },\n  \"unknownSize\": {\n    \"message\": \"未知大小\"\n  },\n  \"saving\": {\n    \"message\": \"正在儲存\"\n  },\n  \"saveFailed\": {\n    \"message\": \"儲存失敗\"\n  },\n  \"badgeNumber\": {\n    \"message\": \"圖示上顯示數字角標\"\n  },\n  \"viewSlices\": {\n    \"message\": \"查看所有切片和下載進度\"\n  },\n  \"send2local\": {\n    \"message\": \"數據發送\"\n  },\n  \"send2MQTT\": {\n    \"message\": \"傳送到 MQTT\"\n  },\n  \"sendingToMQTT\": {\n    \"message\": \"正在傳送到 MQTT 伺服器...\"\n  },\n  \"connectingToMQTT\": {\n    \"message\": \"正在連接 MQTT 伺服器...\"\n  },\n  \"sendingMessageToMQTT\": {\n    \"message\": \"正在發送訊息到 MQTT 伺服器...\"\n  },\n  \"messageSentToMQTT\": {\n    \"message\": \"已發送訊息到 MQTT 伺服器\"\n  },\n  \"popup\": {\n    \"message\": \"彈出\"\n  },\n  \"defaultPopup\": {\n    \"message\": \"默認彈出模式\"\n  },\n  \"invokeApp\": {\n    \"message\": \"呼叫應用\"\n  },\n  \"onlineServiceAddress\": {\n    \"message\": \"在線服務地址\"\n  },\n  \"withinChina\": {\n    \"message\": \"中國境內\"\n  },\n  \"dataFetchFailed\": {\n    \"message\": \"資料獲取失敗\"\n  },\n  \"confirmParameters\": {\n    \"message\": \"確認參數\"\n  },\n  \"searchingForRealKey\": {\n    \"message\": \"尋找真實密鑰\"\n  },\n  \"verifying\": {\n    \"message\": \"驗證中\"\n  },\n  \"realKeyNotFound\": {\n    \"message\": \"未找到真實密鑰\"\n  },\n  \"blockUrl\": {\n    \"message\": \"封鎖網址\"\n  },\n  \"addUrl\": {\n    \"message\": \"添加網址\"\n  },\n  \"wildcards\": {\n    \"message\": \"萬用字元\"\n  },\n  \"blockUrlTips\": {\n    \"message\": \"支持萬用字元*和？\"\n  },\n  \"setWhiteList\": {\n    \"message\": \"設定為白名單\"\n  },\n  \"autoSend\": {\n    \"message\": \"自動發送\"\n  },\n  \"manualSend\": {\n    \"message\": \"手動發送\"\n  },\n  \"requestMethod\": {\n    \"message\": \"請求管道\"\n  },\n  \"requestBody\": {\n    \"message\": \"請求體\"\n  },\n  \"sort\": {\n    \"message\": \"排序\"\n  },\n  \"asc\": {\n    \"message\": \"昇冪\"\n  },\n  \"desc\": {\n    \"message\": \"降冪\"\n  },\n  \"getTime\": {\n    \"message\": \"獲取時間\"\n  },\n  \"fileSize\": {\n    \"message\": \"文件大小\"\n  },\n  \"title\": {\n    \"message\": \"標題\"\n  },\n  \"noKeyIsRequired\": {\n    \"message\": \"不需要金鑰\"\n  },\n  \"estimateSize\": {\n    \"message\": \"估算大小\"\n  },\n  \"retryCount\": {\n    \"message\": \"重試次數\"\n  },\n  \"useSidePanel\": {\n    \"message\": \"使用側邊欄\"\n  },\n  \"Script\": {\n    \"message\": \"腳本\"\n  },\n  \"alwaysSearch\": {\n    \"message\": \"始終開啟深度搜索\"\n  },\n  \"sideurlprotocol\": {\n    \"message\": \"m3u8dl 自訂協定\"\n  },\n  \"deleteDuplicateFilenames\": {\n    \"message\": \"删除重複檔名\"\n  },\n  \"alertimport\": {\n    \"message\": \"導入完成\"\n  },\n  \"isBlockedSite\": {\n    \"message\": \"該網站要求禁止運行本程式\"\n  }\n}"
  },
  {
    "path": "catch-script/catch.js",
    "content": "(function () {\n    class CatCatcher {\n        constructor() {\n            console.log(\"catch.js Start\");\n\n            // 初始化属性\n            this.enable = true;  // 捕获开关\n            this.language = navigator.language;   // 语言设置\n            this.isComplete = false; // 捕获完成标志\n            this.catchMedia = [];   // 捕获的媒体数据\n            this.mediaSize = 0; // 捕获的媒体数据大小\n            this.setFileName = null;    // 文件名\n            this.catCatch = null; // UI元素\n\n            // 移动面板相关属性\n            this.x = 0;\n            this.y = 0;\n\n            // 初始化语言\n            if (window.CatCatchI18n) {\n                if (!window.CatCatchI18n.languages.includes(this.language)) {\n                    this.language = this.language.split(\"-\")[0];\n                    if (!window.CatCatchI18n.languages.includes(this.language)) {\n                        this.language = \"en\";\n                    }\n                }\n            }\n\n            // 初始化组件\n            // 删除iframe sandbox属性 避免 issues #576\n            this.setupIframeProcessing();\n\n            // 初始化 Trusted Types\n            this.initTrustedTypes();\n\n            // 创建和设置UI\n            this.createUI();\n\n            // 代理MediaSource方法\n            this.proxyMediaSourceMethods();\n\n            // 自动跳转到缓冲尾\n            if (localStorage.getItem(\"CatCatchCatch_autoToBuffered\") == \"checked\") {\n                const autoToBufferedInterval = setInterval(() => {\n                    const videos = document.querySelectorAll('video');\n                    if (videos.length > 0 && Array.from(videos).some(video => !video.paused && video.readyState > 2)) {\n                        const autoToBufferedElement = this.catCatch.querySelector(\"#autoToBuffered\");\n                        if (autoToBufferedElement) {\n                            autoToBufferedElement.click();\n                            clearInterval(autoToBufferedInterval);\n                        }\n                    }\n                }, 1000);\n            }\n        }\n\n        /**\n         * 设置iframe处理，删除sandbox属性\n         * 解决 issues #576\n         */\n        setupIframeProcessing() {\n            document.addEventListener('DOMContentLoaded', () => {\n                const processIframe = (iframe) => {\n                    if (iframe && iframe.hasAttribute && iframe.hasAttribute('sandbox')) {\n                        const clonedIframe = iframe.cloneNode(true);\n                        clonedIframe.removeAttribute('sandbox');\n                        if (iframe.parentNode) {\n                            iframe.parentNode.replaceChild(clonedIframe, iframe);\n                        }\n                    }\n                };\n\n                document.querySelectorAll('iframe').forEach(processIframe);\n\n                const observer = new MutationObserver((mutationsList) => {\n                    for (const mutation of mutationsList) {\n                        if (mutation.type === 'childList') {\n                            mutation.addedNodes.forEach(node => {\n                                if (node.nodeName === 'IFRAME') {\n                                    processIframe(node);\n                                } else if (node.querySelectorAll) {\n                                    node.querySelectorAll('iframe').forEach(processIframe);\n                                }\n                            });\n                        }\n                    }\n                });\n                observer.observe(document.body || document.documentElement, { childList: true, subtree: true });\n            });\n        }\n\n        /**\n         * 初始化 Trusted Types\n         */\n        initTrustedTypes() {\n            let createHTML = (string) => {\n                try {\n                    const fakeDiv = document.createElement('div');\n                    fakeDiv.innerHTML = string;\n                    createHTML = (string) => string;\n                } catch (e) {\n                    if (typeof trustedTypes !== 'undefined') {\n                        const policy = trustedTypes.createPolicy('catCatchPolicy', { createHTML: (s) => s });\n                        createHTML = (string) => policy.createHTML(string);\n                        const _innerHTML = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML');\n                        Object.defineProperty(Element.prototype, 'innerHTML', {\n                            set: function (value) {\n                                _innerHTML.set.call(this, createHTML(value));\n                            }\n                        });\n                    } else {\n                        console.warn(\"trustedTypes不可用，跳过安全策略设置\");\n                    }\n                }\n            };\n            createHTML(\"<div></div>\");\n        }\n\n        /**\n         * 创建UI元素\n         */\n        createUI() {\n            const buttonStyle = 'style=\"border:solid 1px #000;margin:2px;padding:2px;background:#fff;border-radius:4px;border:solid 1px #c7c7c780;color:#000;\"';\n            const checkboxStyle = 'style=\"-webkit-appearance: auto;\"';\n\n            this.catCatch = document.createElement(\"div\");\n            this.catCatch.setAttribute(\"id\", \"CatCatchCatch\");\n            const style = `\n                display: flex;\n                flex-direction: column;\n                align-items: flex-start;`;\n            this.catCatch.innerHTML = `<img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYBAMAAAASWSDLAAAAKlBMVEUAAADLlROxbBlRAD16GS5oAjWWQiOCIytgADidUx/95gHqwwTx0gDZqwT6kfLuAAAACnRSTlMA/vUejV7kuzi8za0PswAAANpJREFUGNNjwA1YSxkYTEqhnKZLLi6F1w0gnKA1shdvHYNxdq1atWobjLMKCOAyC3etlVrUAOH4HtNZmLgoAMKpXX37zO1FwcZAwMDguGq1zKpFmTNnzqx0Bpp2WvrU7ttn9py+I8JgLn1R8Pad22vurNkjwsBReHv33junzuyRnOnMwNCSeFH27K5dq1SNgcZxFMnuWrNq1W5VkNntihdv7ToteGcT0C7mIkE1qbWCYjJnM4CqEoWKdoslChXuUgXJqIcLebiphSgCZRhaPDhcDFhdmUMCGIgEAFA+Uc02aZg9AAAAAElFTkSuQmCC\" style=\"-webkit-user-drag: none;width: 20px;\">\n            <div id=\"catCatch\" style=\"${style}\">\n                <div id=\"tips\"></div>\n                <button id=\"download\" ${buttonStyle} data-i18n=\"downloadCapturedData\">下载已捕获的数据</button>\n                <button id=\"clean\" ${buttonStyle} data-i18n=\"deleteCapturedData\">删除已捕获数据</button>\n                <div><button id=\"hide\" ${buttonStyle} data-i18n=\"hide\">隐藏</button><button id=\"close\" ${buttonStyle} data-i18n=\"close\">关闭</button></div>\n                <label><input type=\"checkbox\" id=\"autoDown\" ${localStorage.getItem(\"CatCatchCatch_autoDown\") || \"\"} ${checkboxStyle}><span data-i18n=\"automaticDownload\">完成捕获自动下载</span></label>\n                <label><input type=\"checkbox\" id=\"ffmpeg\" ${localStorage.getItem(\"CatCatchCatch_ffmpeg\") || \"\"} ${checkboxStyle}><span data-i18n=\"ffmpeg\">使用ffmpeg合并</span></label>\n                <label><input type=\"checkbox\" id=\"autoToBuffered\" ${checkboxStyle}><span data-i18n=\"autoToBuffered\">自动跳转缓冲尾</span></label>\n                <label><input type=\"checkbox\" id=\"checkHead\" ${checkboxStyle}><span data-i18n=\"checkHead\">清理多余头部数据</span></label>\n                <label><input type=\"checkbox\" id=\"completeClearCache\" ${localStorage.getItem(\"CatCatchCatch_completeClearCache\") || \"\"} ${checkboxStyle}><span data-i18n=\"completeClearCache\">下载完成后清空数据</span></label>\n                <details>\n                    <summary data-i18n=\"fileName\" id=\"summary\">文件名设置</summary>\n                    <div style=\"font-weight:bold;\"><span data-i18n=\"fileName\">文件名</span>: </div><div id=\"fileName\"></div>\n                    <div style=\"font-weight:bold;\"><span data-i18n=\"selector\">表达式</span>: </div><div id=\"selector\">Null</div>\n                    <div style=\"font-weight:bold;\"><span data-i18n=\"regular\">正则</span>: </div><div id=\"regular\">Null</div>\n                    <button id=\"setSelector\" ${buttonStyle} data-i18n=\"usingSelector\">表达式提取</button>\n                    <button id=\"setRegular\" ${buttonStyle} data-i18n=\"usingRegular\">正则提取</button>\n                    <button id=\"setFileName\" ${buttonStyle} data-i18n=\"customize\">手动填写</button>\n                </details>\n                <details>\n                <summary>test</summary>\n                    <button id=\"test\" ${buttonStyle}>test</button>\n                    <button id=\"restart\" ${buttonStyle} data-i18n=\"capturedBeginning\">从头捕获</button>\n                    <label><input type=\"checkbox\" id=\"restartAlways\" ${localStorage.getItem(\"CatCatchCatch_restart\") || \"\"} ${checkboxStyle}><span data-i18n=\"alwaysCapturedBeginning\">始终从头捕获</span>(beta)</label>\n                </details>\n            </div>`;\n            this.catCatch.style = `\n                position: fixed;\n                z-index: 999999;\n                top: 10%;\n                left: 90%;\n                background: rgb(255 255 255 / 85%);\n                border: solid 1px #c7c7c7;\n                border-radius: 4px;\n                color: rgb(26, 115, 232);\n                padding: 5px 5px 5px 5px;\n                font-size: 12px;\n                font-family: \"Microsoft YaHei\", \"Helvetica\", \"Arial\", sans-serif;\n                user-select: none;`;\n\n            // 创建 Shadow DOM\n            this.createShadowRoot();\n\n            // 初始化UI元素引用\n            this.tips = this.catCatch.querySelector(\"#tips\");\n            this.fileName = this.catCatch.querySelector(\"#fileName\");\n            this.selector = this.catCatch.querySelector(\"#selector\");\n            this.regular = this.catCatch.querySelector(\"#regular\");\n\n            if (!this.tips || !this.fileName || !this.selector || !this.regular) {\n                console.error(\"UI元素初始化失败，找不到必要的DOM元素\");\n            }\n\n            // 初始化显示\n            this.tips.innerHTML = this.i18n(\"waiting\", \"等待视频播放\");\n            this.selector.innerHTML = localStorage.getItem(\"CatCatchCatch_selector\") ?? \"Null\";\n            this.regular.innerHTML = localStorage.getItem(\"CatCatchCatch_regular\") ?? \"Null\";\n\n            // 绑定事件\n            this.bindEvents();\n\n            // 自动从头捕获设置\n            if (localStorage.getItem(\"CatCatchCatch_restart\") == \"checked\") {\n                this.setupAutoRestart();\n            }\n        }\n\n        /**\n         * 创建 Shadow DOM\n         * 解决 issues #693 安全使用attachShadow 从iframe中获取原生方法\n         */\n        createShadowRoot() {\n            try {\n                // 解决 issues #693 安全使用attachShadow 从iframe中获取原生方法\n                const createSecureShadowRoot = (element, mode = 'closed') => {\n                    const getPristineAttachShadow = () => {\n                        try {\n                            const iframe = document.createElement('iframe');\n                            const parentNode = document.body || document.documentElement;\n                            parentNode.appendChild(iframe);\n                            const pristineMethod = iframe.contentDocument.createElement('div').attachShadow;\n                            iframe.remove();\n                            if (pristineMethod) return pristineMethod;\n                        } catch (e) {\n                            console.log(\"获取原生attachShadow方法失败:\", e);\n                        }\n                        return Element.prototype.attachShadow;\n                    };\n\n                    const executor = Element.prototype.attachShadow.toString().includes('[native code]')\n                        ? Element.prototype.attachShadow.bind(element)\n                        : getPristineAttachShadow().bind(element);\n\n                    try {\n                        return executor({ mode });\n                    } catch (e) {\n                        console.error('Shadow DOM 创建失败:', e);\n                        // 应急处理：降级方案\n                        return document.createElement('div');\n                    }\n                };\n\n                // 创建 Shadow DOM 放入CatCatch\n                const divShadow = document.createElement('div');\n                const shadowRoot = createSecureShadowRoot(divShadow);\n                shadowRoot.appendChild(this.catCatch);\n\n                // 页面插入Shadow DOM\n                const htmlElement = document.getElementsByTagName('html')[0];\n                if (htmlElement) {\n                    htmlElement.appendChild(divShadow);\n                } else {\n                    document.appendChild(divShadow);\n                }\n            } catch (error) {\n                console.error(\"创建Shadow DOM失败:\", error);\n                // 降级方案：直接添加到body\n                try {\n                    const body = document.body || document.documentElement;\n                    body.appendChild(this.catCatch);\n                } catch (e) {\n                    console.error(\"降级添加UI也失败:\", e);\n                }\n            }\n        }\n\n        /**\n         * 绑定事件处理函数\n         */\n        bindEvents() {\n            // 移动面板相关事件\n            this.catCatch.addEventListener('mousedown', this.handleDragStart.bind(this));\n\n            // 设置选项相关事件\n            const autoDown = this.catCatch.querySelector(\"#autoDown\");\n            if (autoDown) autoDown.addEventListener('change', this.handleAutoDownChange.bind(this));\n\n            const ffmpeg = this.catCatch.querySelector(\"#ffmpeg\");\n            if (ffmpeg) ffmpeg.addEventListener('change', this.handleFfmpegChange.bind(this));\n\n            const restartAlways = this.catCatch.querySelector(\"#restartAlways\");\n            if (restartAlways) restartAlways.addEventListener('change', this.handleRestartAlwaysChange.bind(this));\n\n            // 按钮相关事件\n            const clean = this.catCatch.querySelector(\"#clean\");\n            if (clean) clean.addEventListener('click', this.handleClean.bind(this));\n\n            const download = this.catCatch.querySelector(\"#download\");\n            if (download) download.addEventListener('click', this.handleDownload.bind(this));\n\n            const hide = this.catCatch.querySelector(\"#hide\");\n            if (hide) hide.addEventListener('click', this.handleHide.bind(this));\n\n            const img = this.catCatch.querySelector(\"img\");\n            if (img) img.addEventListener('click', this.handleHide.bind(this));\n\n            const close = this.catCatch.querySelector(\"#close\");\n            if (close) close.addEventListener('click', this.handleClose.bind(this));\n\n            const restart = this.catCatch.querySelector(\"#restart\");\n            if (restart) restart.addEventListener('click', this.handleRestart.bind(this));\n\n            const setFileName = this.catCatch.querySelector(\"#setFileName\");\n            if (setFileName) setFileName.addEventListener('click', this.handleSetFileName.bind(this));\n\n            const test = this.catCatch.querySelector(\"#test\");\n            if (test) test.addEventListener('click', this.handleTest.bind(this));\n\n            const summary = this.catCatch.querySelector(\"#summary\");\n            if (summary) summary.addEventListener('click', this.getFileName.bind(this));\n\n            const completeClearCache = this.catCatch.querySelector(\"#completeClearCache\");\n            if (completeClearCache) completeClearCache.addEventListener('click', this.handleCompleteClearCache.bind(this));\n\n            // 自动跳转到缓冲节点\n            // this.autoToBufferedFlag = true;\n            const autoToBuffered = this.catCatch.querySelector(\"#autoToBuffered\");\n            if (autoToBuffered) autoToBuffered.addEventListener('click', this.handleAutoToBuffered.bind(this));\n\n            // 文件名设置相关事件\n            const setSelector = this.catCatch.querySelector(\"#setSelector\");\n            if (setSelector) setSelector.addEventListener('click', this.handleSetSelector.bind(this));\n\n            const setRegular = this.catCatch.querySelector(\"#setRegular\");\n            if (setRegular) setRegular.addEventListener('click', this.handleSetRegular.bind(this));\n\n            // i18n 处理\n            this.applyI18n();\n        }\n\n        /**\n         * 应用国际化文本\n         */\n        applyI18n() {\n            if (window.CatCatchI18n) {\n                this.catCatch.querySelectorAll('[data-i18n]').forEach((element) => {\n                    if (element && element.dataset && element.dataset.i18n) {\n                        element.innerHTML = window.CatCatchI18n[element.dataset.i18n][this.language] || element.innerHTML;\n                    }\n                });\n                this.catCatch.querySelectorAll('[data-i18n-outer]').forEach((element) => {\n                    if (element && element.dataset && element.dataset.i18nOuter) {\n                        element.outerHTML = window.CatCatchI18n[element.dataset.i18nOuter][this.language] || element.outerHTML;\n                    }\n                });\n            }\n        }\n\n        /**\n         * 翻译函数\n         * @param {String} key \n         * @param {String|null} original 原始文本\n         * @returns 翻译后的文本\n         */\n        i18n(key, original = \"\") {\n            if (!window.CatCatchI18n || !key || !window.CatCatchI18n[key]) { return original; }\n            return window.CatCatchI18n[key][this.language] || original;\n        }\n\n        /**\n         * 处理面板拖动事件\n         * @param {MouseEvent} event\n         */\n        handleDragStart(event) {\n            this.x = event.pageX - this.catCatch.offsetLeft;\n            this.y = event.pageY - this.catCatch.offsetTop;\n\n            const moveHandler = this.handleMove.bind(this);\n            document.addEventListener('mousemove', moveHandler);\n\n            document.addEventListener('mouseup', () => {\n                document.removeEventListener('mousemove', moveHandler);\n            }, { once: true });\n        }\n\n        /**\n         * 处理面板移动事件\n         * 通过鼠标事件更新面板位置\n         * @param {MouseEvent} event \n         */\n        handleMove(event) {\n            if (!this.catCatch) return;\n            this.catCatch.style.left = (event.pageX - this.x) + 'px';\n            this.catCatch.style.top = (event.pageY - this.y) + 'px';\n        }\n\n        handleAutoDownChange(event) {\n            localStorage.setItem(\"CatCatchCatch_autoDown\", event.target.checked ? \"checked\" : \"\");\n        }\n\n        handleFfmpegChange(event) {\n            localStorage.setItem(\"CatCatchCatch_ffmpeg\", event.target.checked ? \"checked\" : \"\");\n        }\n\n        handleRestartAlwaysChange(event) {\n            localStorage.setItem(\"CatCatchCatch_restart\", event.target.checked ? \"checked\" : \"\");\n        }\n\n        /**\n         * 处理清理缓存事件\n         * @param {MouseEvent} event \n         */\n        handleClean(event) {\n            if (window.confirm(this.i18n(\"clearCacheConfirmation\", \"确认清除缓存?\"))) {\n                this.clearCache();\n                const $clean = this.catCatch.querySelector(\"#clean\");\n                if (!$clean) return;\n\n                $clean.innerHTML = this.i18n(\"cleanupCompleted\", \"清理完成!\");\n                setTimeout(() => {\n                    if ($clean) $clean.innerHTML = this.i18n(\"clearCache\", \"清理缓存\");\n                }, 1000);\n            }\n        }\n\n        /**\n         * 处理下载事件\n         * @param {MouseEvent} event \n         */\n        handleDownload(event) {\n            try {\n                if (this.isComplete || window.confirm(this.i18n(\"downloadConfirmation\", \"提前下载可能会造成数据混乱.确认？\"))) {\n                    this.catchDownload();\n                }\n            } catch (error) {\n                console.error(\"下载处理失败:\", error);\n                alert(this.i18n(\"downloadError\", \"下载过程中出错，请查看控制台\"));\n            }\n        }\n\n        handleHide(event) {\n            const catCatchElement = this.catCatch.querySelector('#catCatch');\n            if (catCatchElement.style.display === \"none\") {\n                catCatchElement.style.display = \"flex\";\n                this.catCatch.style.opacity = \"\";\n            } else {\n                catCatchElement.style.display = \"none\";\n                this.catCatch.style.opacity = \"0.5\";\n            }\n        }\n\n        handleClose(event) {\n            if (this.isComplete || window.confirm(this.i18n(\"closeConfirmation\", \"确认关闭?\"))) {\n                this.clearCache();\n                this.enable = false;\n                this.catCatch.style.display = \"none\";\n                window.postMessage({ action: \"catCatchToBackground\", Message: \"script\", script: \"catch.js\", refresh: false });\n            }\n        }\n\n        /**\n         * 从头捕获\n         * @param {MouseEvent} event \n         */\n        handleRestart(event) {\n            const checkHead = this.catCatch.querySelector(\"#checkHead\");\n            if (checkHead) checkHead.checked = true;\n\n            this.clearCache();\n            document.querySelectorAll(\"video\").forEach((element) => {\n                element.currentTime = 0;\n                element.play();\n            });\n        }\n\n        handleSetFileName(event) {\n            this.setFileName = window.prompt(this.i18n(\"fileName\", \"输入文件名, 不包含扩展名\"), this.setFileName ?? \"\");\n            this.getFileName();\n        }\n\n        handleTest(event) {\n            console.log(\"捕获的媒体数据:\", this.catchMedia);\n        }\n\n        handleCompleteClearCache(event) {\n            localStorage.setItem(\"CatCatchCatch_completeClearCache\", event.target.checked ? \"checked\" : \"\");\n        }\n\n        /**\n         * 自动缓冲尾\n         * @param {MouseEvent} event \n         */\n        handleAutoToBuffered(event) {\n            // if (!this.autoToBufferedFlag) return;\n            // this.autoToBufferedFlag = false;\n\n            const $autoToBuffered = this.catCatch.querySelector(\"#autoToBuffered\");\n            if (!$autoToBuffered) return;\n\n            localStorage.setItem(\"CatCatchCatch_autoToBuffered\", event.target.checked ? \"checked\" : \"\");\n\n            const videos = document.querySelectorAll(\"video\");\n            for (let video of videos) {\n                video.addEventListener(\"progress\", (event) => {\n                    try {\n                        if (video.buffered && video.buffered.length > 0) {\n                            const bufferedEnd = video.buffered.end(0);\n                            if ($autoToBuffered.checked && bufferedEnd < video.duration) {\n                                video.currentTime = bufferedEnd - 5;\n                            }\n                        }\n                    } catch (error) {\n                        console.error(\"处理缓冲进度失败:\", error);\n                    }\n                });\n\n                video.addEventListener(\"ended\", () => {\n                    $autoToBuffered.checked = false;\n                });\n            }\n        }\n\n        /**\n         * CSS选择器 提取文件名\n         * @param {MouseEvent} event \n         */\n        handleSetSelector(event) {\n            const result = window.prompt(\"Selector\", localStorage.getItem(\"CatCatchCatch_selector\") ?? \"\");\n            if (result == null) return;\n\n            if (result == \"\") {\n                this.clearFileName(\"selector\");\n                return;\n            }\n\n            let title;\n            try {\n                title = document.querySelector(result);\n            } catch (e) {\n                this.clearFileName(\"selector\", this.i18n(\"fileNameError\", \"选择器语法错误!\"));\n                return;\n            }\n\n            if (title && title.innerHTML) {\n                this.selector.innerHTML = this.stringModify(result);\n                localStorage.setItem(\"CatCatchCatch_selector\", result);\n                this.getFileName();\n            } else {\n                this.clearFileName(\"selector\", this.i18n(\"fileNameError\", \"表达式错误, 无法获取或内容为空!\"));\n            }\n        }\n        /**\n         * 正则 提取文件名\n         * @param {MouseEvent} event \n         */\n        handleSetRegular(event) {\n            let result = window.prompt(this.i18n(\"regular\", \"文件名获取正则\"), localStorage.getItem(\"CatCatchCatch_regular\") ?? \"\");\n            if (result == null) return;\n\n            if (result == \"\") {\n                this.clearFileName(\"regular\");\n                return;\n            }\n\n            try {\n                new RegExp(result);\n                this.regular.innerHTML = this.stringModify(result);\n                localStorage.setItem(\"CatCatchCatch_regular\", result);\n                this.getFileName();\n            } catch (e) {\n                this.clearFileName(\"regular\", this.i18n(\"fileNameError\", \"正则表达式错误\"));\n                console.log(e);\n            }\n        }\n\n        /**\n         * 核心函数 代理MediaSource方法\n         */\n        proxyMediaSourceMethods() {\n            // 代理 addSourceBuffer 方法\n            window.MediaSource.prototype.addSourceBuffer = new Proxy(window.MediaSource.prototype.addSourceBuffer, {\n                apply: (target, thisArg, argumentsList) => {\n                    try {\n                        const result = Reflect.apply(target, thisArg, argumentsList);\n\n                        // 标题获取\n                        setTimeout(() => { this.getFileName(); }, 2000);\n                        this.tips.innerHTML = this.i18n(\"capturingData\", \"捕获数据中...\");\n\n                        this.catchMedia.push({ mimeType: argumentsList[0], bufferList: [] });\n                        const index = this.catchMedia.length - 1;\n\n                        // 代理 appendBuffer 方法\n                        result.appendBuffer = new Proxy(result.appendBuffer, {\n                            apply: (target, thisArg, argumentsList) => {\n                                Reflect.apply(target, thisArg, argumentsList);\n\n                                if (this.enable && argumentsList[0]) {\n                                    this.mediaSize += argumentsList[0].byteLength || 0;\n                                    if (this.tips) {\n                                        this.tips.innerHTML = this.i18n(\"capturingData\", \"捕获数据中...\") + \": \" + this.byteToSize(this.mediaSize);\n                                    }\n                                    this.catchMedia[index].bufferList.push(argumentsList[0]);\n                                }\n                            }\n                        });\n\n                        return result;\n                    } catch (error) {\n                        console.error(\"addSourceBuffer 代理错误:\", error);\n                        return Reflect.apply(target, thisArg, argumentsList);\n                    }\n                }\n            });\n\n            // 代理 endOfStream 方法\n            window.MediaSource.prototype.endOfStream = new Proxy(window.MediaSource.prototype.endOfStream, {\n                apply: (target, thisArg, argumentsList) => {\n                    try {\n                        Reflect.apply(target, thisArg, argumentsList);\n\n                        if (this.enable) {\n                            this.isComplete = true;\n                            if (this.tips) {\n                                this.tips.innerHTML = this.i18n(\"captureCompleted\", \"捕获完成\");\n                            }\n\n                            if (localStorage.getItem(\"CatCatchCatch_autoDown\") == \"checked\") {\n                                setTimeout(() => this.catchDownload(), 500);\n                            }\n                        }\n                    } catch (error) {\n                        console.error(\"endOfStream 代理错误:\", error);\n                        return Reflect.apply(target, thisArg, argumentsList);\n                    }\n                }\n            });\n        }\n\n        /**\n         * 自动从头捕获\n         * 监控DOM变化，自动重置视频播放位置\n         */\n        setupAutoRestart() {\n            document.addEventListener('DOMContentLoaded', () => {\n                document.querySelectorAll('video').forEach((video) => this.resetVideoPlayback(video));\n\n                // 监控 DOM\n                const observer = new MutationObserver(mutations => {\n                    mutations.forEach(mutation => {\n                        mutation.addedNodes.forEach(node => {\n                            try {\n                                if (node.tagName === 'VIDEO') {\n                                    this.resetVideoPlayback(node);\n                                } else if (node.querySelectorAll) {\n                                    node.querySelectorAll('video').forEach(video => this.resetVideoPlayback(video));\n                                }\n                            } catch (error) {\n                                console.error(\"处理新添加的视频节点失败:\", error);\n                            }\n                        });\n                    });\n                });\n\n                observer.observe(document.body || document.documentElement, { childList: true, subtree: true });\n            });\n        }\n\n        /**\n         * 重置视频播放位置\n         * @param {Object} video \n         */\n        resetVideoPlayback(video) {\n            if (!video) return;\n            const timer = setInterval(() => {\n                if (!video.paused) {\n                    video.currentTime = 0;\n                    const checkHead = this.catCatch.querySelector(\"#checkHead\");\n                    if (checkHead) checkHead.checked = true;\n                    this.clearCache();\n                    clearInterval(timer);\n                }\n            }, 500);\n\n            // 5秒后如果还没有检测到播放，就清除定时器\n            setTimeout(() => clearInterval(timer), 5000);\n\n            video.addEventListener('play', () => {\n                if (!video.isResetCatCatch) {\n                    video.isResetCatCatch = true;\n                    video.currentTime = 0;\n                    const checkHead = this.catCatch.querySelector(\"#checkHead\");\n                    if (checkHead) checkHead.checked = true;\n                    this.clearCache();\n                }\n            }, { once: true });\n        }\n\n        /**\n         * 下载捕获的数据\n         */\n        catchDownload() {\n            if (this.catchMedia.length == 0) {\n                alert(this.i18n(\"noData\", \"没抓到有效数据\"));\n                return;\n            }\n\n            let downloadWithFFmpeg = this.catchMedia.length >= 2 && localStorage.getItem(\"CatCatchCatch_ffmpeg\") == \"checked\";\n\n            /**\n             * 检查文件\n             * 检查是否有头部文件 没有头部文件则提示 不使用ffmpeg合并\n             * 检查是否有多个头部文件 根据用户选项 是否清理多于头部数据\n             */\n            const checkHead = this.catCatch.querySelector(\"#checkHead\");\n            // 仅确认一次是否清除多余头部数据\n            let userConfirmedHeadChoice = false;\n\n            for (let key in this.catchMedia) {\n                if (!this.catchMedia[key]?.bufferList || this.catchMedia[key].bufferList.length <= 1) continue;\n                let lastHeaderIndex = -1;\n\n                // 遍历所有 buffer 寻找最后一个头部\n                for (let i = 0; i < this.catchMedia[key].bufferList.length; i++) {\n                    const data = new Uint8Array(this.catchMedia[key].bufferList[i]);\n\n                    // 检查MP4格式的头部 (ftyp)\n                    if (data.length > 8 &&\n                        data[4] === 0x66 && // 'f'\n                        data[5] === 0x74 && // 't'\n                        data[6] === 0x79 && // 'y'\n                        data[7] === 0x70)   // 'p'\n                    {\n                        lastHeaderIndex = i; // 持续更新直到找到最后一个头部\n                    }\n                    // 检查WebM格式的头部 (1A 45 DF A3)\n                    else if (data.length > 4 &&\n                        data[0] === 0x1A &&\n                        data[1] === 0x45 &&\n                        data[2] === 0xDF &&\n                        data[3] === 0xA3) {\n                        lastHeaderIndex = i; // 持续更新直到找到最后一个WebM头部\n                    }\n                }\n                if (lastHeaderIndex == -1) {\n                    alert(this.i18n(\"noHead\", \"没有检测到视频头部数据, 请使用本地工具处理\"));\n                    downloadWithFFmpeg = false; // 没有头部数据则不使用ffmpeg合并\n                }\n                if (lastHeaderIndex > 0) {\n                    // 只有第一次遇到多余头部且用户尚未选择时才提示\n                    if (!userConfirmedHeadChoice && !checkHead.checked) {\n                        checkHead.checked = window.confirm(this.i18n(\"headData\", \"检测到多余头部数据, 是否清除?\"));\n                        userConfirmedHeadChoice = true; // 标记已经询问过用户\n                    }\n\n                    if (checkHead.checked) {\n                        this.catchMedia[key].bufferList.splice(0, lastHeaderIndex); // 移除最后一个头部之前的所有元素\n                    }\n                }\n            }\n\n            downloadWithFFmpeg ? this.downloadWithFFmpeg() : this.downloadDirect();\n\n            if (this.isComplete) {\n                if (localStorage.getItem(\"CatCatchCatch_completeClearCache\") == \"checked\") { this.clearCache(); }\n                if (this.tips) {\n                    this.tips.innerHTML = this.i18n(\"downloadCompleted\", \"下载完毕...\");\n                }\n            }\n        }\n\n        /**\n         * 使用FFmpeg合并下载捕获的数据\n         */\n        downloadWithFFmpeg() {\n            const media = [];\n            for (let item of this.catchMedia) {\n                if (!item || !item.bufferList || item.bufferList.length === 0) continue;\n\n                const mime = (item.mimeType && item.mimeType.split(';')[0]) || 'video/mp4';\n                const fileBlob = new Blob(item.bufferList, { type: mime });\n                const type = mime.split('/')[0] || 'video';\n\n                media.push({\n                    data: (typeof chrome == \"object\") ? URL.createObjectURL(fileBlob) : fileBlob,\n                    type: type\n                });\n            }\n\n            if (media.length === 0) {\n                alert(this.i18n(\"noData\", \"没有有效数据可下载\"));\n                return;\n            }\n\n            const title = this.fileName ? this.fileName.innerHTML.trim() : document.title;\n\n            window.postMessage({\n                action: \"catCatchFFmpeg\",\n                use: \"catchMerge\",\n                files: media,\n                title: title,\n                output: title,\n                quantity: media.length\n            });\n        }\n        /**\n         * 直接下载捕获的数据\n         */\n        downloadDirect() {\n            const a = document.createElement('a');\n            let downloadCount = 0;\n\n            for (let item of this.catchMedia) {\n                if (!item || !item.bufferList || item.bufferList.length === 0) continue;\n\n                const mime = (item.mimeType && item.mimeType.split(';')[0]) || 'video/mp4';\n                const type = mime.split('/')[0] == \"video\" ? \"mp4\" : \"mp3\";\n                const fileBlob = new Blob(item.bufferList, { type: mime });\n\n                a.href = URL.createObjectURL(fileBlob);\n                a.download = `${this.fileName ? this.fileName.innerHTML.trim() : document.title}.${type}`;\n                a.click();\n\n                // 释放URL对象以避免内存泄漏\n                setTimeout(() => URL.revokeObjectURL(a.href), 100);\n                downloadCount++;\n            }\n\n            a.remove();\n\n            if (downloadCount === 0) {\n                alert(this.i18n(\"noData\", \"没有有效数据可下载\"));\n            }\n        }\n\n        clearFileName(obj = \"selector\", warning = \"\") {\n            localStorage.removeItem(\"CatCatchCatch_\" + obj);\n            const element = obj == \"selector\" ? this.selector : this.regular;\n            if (element) element.innerHTML = this.i18n(\"notSet\", \"未设置\");\n            this.getFileName();\n            if (warning) alert(warning);\n        }\n\n        /**\n         * 清理缓存\n         */\n        clearCache() {\n            this.mediaSize = 0;\n            if (this.isComplete) {\n                this.catchMedia = [];\n                this.isComplete = false;\n                return;\n            }\n\n            for (let key in this.catchMedia) {\n                const media = this.catchMedia[key];\n                if (media && media.bufferList && media.bufferList.length > 0) {\n                    // 保留第一个buffer块，清除其余的\n                    const firstBuffer = media.bufferList[0];\n                    media.bufferList = [firstBuffer];\n                    this.mediaSize += firstBuffer ? (firstBuffer.byteLength || 0) : 0;\n                }\n            }\n        }\n\n        byteToSize(byte) {\n            if (!byte || byte < 1024) return \"0KB\";\n            if (byte < 1024 * 1024) {\n                return (byte / 1024).toFixed(1) + \"KB\";\n            } else if (byte < 1024 * 1024 * 1024) {\n                return (byte / 1024 / 1024).toFixed(1) + \"MB\";\n            } else {\n                return (byte / 1024 / 1024 / 1024).toFixed(1) + \"GB\";\n            }\n        }\n\n        /**\n         * 获取文件名\n         */\n        getFileName() {\n            try {\n                if (!this.fileName) return;\n\n                if (this.setFileName) {\n                    this.fileName.innerHTML = this.stringModify(this.setFileName);\n                    return;\n                }\n\n                let name = \"\";\n                const selectorKey = localStorage.getItem(\"CatCatchCatch_selector\");\n                if (selectorKey) {\n                    const title = document.querySelector(selectorKey);\n                    if (title && title.innerHTML) {\n                        name = title.innerHTML;\n                    }\n                }\n\n                const regularKey = localStorage.getItem(\"CatCatchCatch_regular\");\n                if (regularKey) {\n                    const str = name == \"\" ? document.documentElement.outerHTML : name;\n                    const reg = new RegExp(regularKey, \"g\");\n                    let result = str.match(reg);\n                    if (result) {\n                        result = result.filter((item) => item !== \"\");\n                        name = result.join(\"_\");\n                    }\n                }\n\n                this.fileName.innerHTML = name ? this.stringModify(name) : this.stringModify(document.title);\n            } catch (error) {\n                console.error(\"获取文件名失败:\", error);\n                if (this.fileName) this.fileName.innerHTML = this.stringModify(document.title);\n            }\n        }\n\n        stringModify(str) {\n            if (!str) return \"untitled\";\n\n            return str.replace(/['\\\\:\\*\\?\"<\\/>\\|~]/g, function (m) {\n                return {\n                    \"'\": '&#39;',\n                    '\\\\': '&#92;',\n                    '/': '&#47;',\n                    ':': '&#58;',\n                    '*': '&#42;',\n                    '?': '&#63;',\n                    '\"': '&quot;',\n                    '<': '&lt;',\n                    '>': '&gt;',\n                    '|': '&#124;',\n                    '~': '_'\n                }[m];\n            });\n        }\n    }\n\n    // 创建并启动CatCatcher实例\n    const catCatcher = new CatCatcher();\n})();"
  },
  {
    "path": "catch-script/i18n.js",
    "content": "(function () {\n    if (window.CatCatchI18n) { return; }\n    window.CatCatchI18n = {\n    languages: [\"en\", \"es\", \"zh\"],\n        downloadCapturedData: {\n            en: \"Download the captured data\",\n            es: \"Descargar datos captura\",\n            zh: \"下载已捕获的数据\"\n        },\n        deleteCapturedData: {\n            en: \"Delete the captured data\",\n            es: \"Borrar datos captura\",\n            zh: \"删除已捕获数据\"\n        },\n        capturedBeginning: {\n            en: \"Capture from the beginning\",\n            es: \"Capturar desde inicio\",\n            zh: \"从头捕获\"\n        },\n        alwaysCapturedBeginning: {\n            en: \"Always Capture from the beginning\",\n            es: \"Siempre desde Inicio\",\n            zh: \"始终从头捕获\"\n        },\n        hide: {\n            en: \"Hide\",\n            es: \"Ocultar\",\n            zh: \"隐藏\"\n        },\n        close: {\n            en: \"Close\",\n            es: \"Cerrar\",\n            zh: \"关闭\"\n        },\n        save: {\n            en: \"Save\",\n            es: \"Guardar\",\n            zh: \"保存\"\n        },\n\tcheckHead: {\n        en: \"Clean up unnecessary header data\",\n        es: \"Borrar datos cabecera innecesarios\",\n        zh: \"清理多余头部数据\"\t\n\t},\t\n        automaticDownload: {\n            en: \"Automatic download\",\n            es: \"Descarga automática\",\n            zh: \"完成捕获自动下载\"\n        },\n        ffmpeg: {\n        en: \"using ffmpeg\",\n        es: \"Usar ffmpeg\",\n        zh: \"使用ffmpeg\"\n    },\n    fileName: {\n        en: \"File name\",\n        es: \"Nombre archivo\",\n        zh: \"文件名\"\n    },\n    selector: {\n        en: \"Selector\",\n        es: \"Selector\",\n        zh: \"表达式\"\n    },\n    regular: {\n        en: \"Regular\",\n        es: \"Regular\",\n        zh: \"正则\"\n    },\n    notSet: {\n        en: \"Not set\",\n        es: \"No puesto\",\n        zh: \"未设置\"\n    },\n    usingSelector: {\n        en: \"selector\",\n        es: \"selector\",\n        zh: \"表达式提取\"\n    },\n    usingRegular: {\n        en: \"regular\",\n        es: \"regular\",\n        zh: \"正则提取\"\n    },\n    customize: {\n        en: \"Customize\",\n        es: \"Personalizar\",\n        zh: \"自定义\"\n    },\n\t    cleanHead: {\n        en: \"Clean up redundant header data\",\n        es: \"Limpiar datos cabecera redundantes\",\n        zh: \"清理多余头部数据\"\n    },\n    clearCache: {\n        en: \"Clear cache\",\n        es: \"Borrar cache\",\n        zh: \"清理缓存\"\n    },\n    cleanupCompleted: {\n        en: \"Cleanup completed\",\n        es: \"Limpieza finalizada\",\n        zh: \"清理完成\"\n    },\n    downloadConfirmation: {\n        en: \"Downloading in advance may cause data confusion. Confirm?\",\n        es: \"La descarga anticipada puede causar confusión de datos. ¿Confirmar?\",\n        zh: \"提前下载可能会造成数据混乱.确认？\"\n    },\n    fileNameError: {\n        en: \"Unable to fetch or the content is empty!\",\n        es: \"No se puede recuperar o el contenido está vacío.\",\n        zh: \"无法获取或内容为空!\"\n    },\n    noData: {\n        en: \"No data\",\n        es: \"Sin datos\",\n        zh: \"没抓到有效数据!\"\n    },\n    waiting: {\n        en: \"Waiting for video to play\",\n        es: \"Esperando reproducir vídeo\",\n        zh: \"等待视频播放\"\n    },\n    capturingData: {\n        en: \"Capturing data\",\n        es: \"Capturando datos\",\n        zh: \"捕获数据中\"\n    },\n    captureCompleted: {\n        en: \"Capture completed\",\n        es: \"Captura finalizada\",\n        zh: \"捕获完成\"\n    },\n    downloadCompleted: {\n        en: \"Download completed\",\n        es: \"Descarga completada\",\t\n        zh: \"下载完毕\"\n    },\n    selectVideo: {\n        en: \"Select Video\",\n        es: \"Seleccionar vídeo\",\n        zh: \"选择视频\"\n    },\n    selectAudio: {\n        en: \"Select Audio\",\n        es: \"Seleccionar audio\",\n        zh: \"选择音频\"\n    },\n    recordEncoding: {\n        en: \"Record Encoding\",\n        es: \"Codificando grabación\",\n        zh: \"录制编码\"\n    },\n    readVideo: {\n        en: \"Read Video\",\n        es: \"Leer vídeo\",\n        zh: \"读取视频\"\n    },\n    startRecording: {\n        en: \"Start Recording\",\n        es: \"Iniciar grabación\",\n        zh: \"开始录制\"\n    },\n    stopRecording: {\n        en: \"Stop Recording\",\n        es: \"Detener grabación\",\n        zh: \"停止录制\"\n    },\n    noVideoDetected: {\n        en: \"No video detected, Please read again\",\n        es: \"No se ha detectado ningún vídeo, leer vídeo\",\n        zh: \"没有检测到视频, 请重新读取\"\n    },\n    recording: {\n        en: \"Recording\",\n        es: \"Grabando\",\n        zh: \"视频录制中\"\n    },\n    recordingNotSupported: {\n        en: \"recording Not Supported\",\n        es: \"grabación no compatible\",\n        zh: \"不支持录制\"\n    },\n    formatNotSupported: {\n        en: \"Format not supported\",\n        es: \"Formato no compatible\",\n        zh: \"不支持此格式\"\n    },\n    clickToStartRecording: {\n        en: \"Click to start recording\",\n        es: \"Clic para iniciar la grabación\",\n        zh: \"请点击开始录制\"\n    },\n    sentToFfmpeg: {\n        en: \"Sent to ffmpeg\",\n        es: \"Enviar a ffmpeg\",\n        zh: \"发送到ffmpeg\"\n    },\n    recordingFailed: {\n        en: \"Recording failed\",\n        es: \"Error al grabar\",\n        zh: \"录制失败\"\n    },\n    scriptNotSupported: {\n        en: \"This script is not supported\",\n        es: \"Este script no es compatible\",\n        zh: \"当前网页不支持此脚本\"\n    },\n    dragWindow: {\n        en: \"Drag window\",\n        es: \"Arrastrar ventana\",\n        zh: \"拖动窗口\"\n    },\n    autoToBuffered: {\n        en: \"Automatically jump to buffer\",\n        es: \"Ir al buffer\",\n        zh: \"自动跳转到缓冲尾\"\n    },\n    save1hour: {\n        en: \"Save once every hour\",\n        es: \"Guardar una vez cada hora\",\n        zh: \"1小时保存一次\"\n    },\n    recordingChangeEncoding: {\n        en: \"Cannot change encoding during recording\",\n        es: \"No se puede cambiar la codificación durante la grabación\",\n        zh: \"录制中不能更改编码\"\n    },\n    streamEmpty: {\n        en: \"Media stream is empty\",\n        es: \"El stream multimedia está vacío\",\n        zh: \"媒体流为空\"\n    },\n    notStream: {\n        en: \"Not a media stream object\",\n        es: \"No es un objeto stream multimedia\",\n        zh: \"非媒体流对象\"\n    },\n    notStream: {\n        en: \"Not a media stream object\",\n        es: \"No es un objeto stream multimedia\",\n        zh: \"非媒体流对象\"\n    },\n    streamAdded: {\n        en: \"Stream added\",\n        es: \"Añadido stream\",\n        zh: \"流已添加\"\n    },\n    videoAndAudio: {\n        en: \"Includes both audio and video streams\",\n            es: \"Incluir streams de audio y vídeo\",\n        zh: \"已包含音频和视频流\"\n    },\n    audioBits: {\n        en: \"Audio bit\",\n        es: \"Tasa audio\",\n        zh: \"音频码率\"\n    },\n    videoBits: {\n        en: \"Video bits\",\n        es: \"Tasa vídeo\",\n        zh: \"视频码率\"\n    },\n    frameRate: {\n        en: \"frame Rate\",\n        es: \"cuadros/seg\",\n        zh: \"帧率\"\n    },\n    noHeader: {\n        en: \"No header data detected, please process with local tools\",\n        es: \"No se han detectado datos de cabecera, procesar con herramientas locales\",\n        zh: \"没有检测到视频头部数据, 请使用本地工具处理\"\n    },\n    headData: {\n        en: \"Multiple header data found in media file, Clear it?\",\n        es: \"Múltiples datos de cabecera encontrados en el archivo multimedia, ¿Borrar?\",\n        zh: \"检测到多余头部数据, 是否清除?\"\n    },\n    clearCacheConfirmation: {\n        en: \"Are you sure you want to clear the cache?\",\n        es: \"¿Seguro que quieres borrar el caché?\",\n        zh: \"确定要清除缓存吗?\"\n    },\n    closeConfirmation: {\n        en: \"Are you sure you want to close?\",\n        es: \"¿Seguro que quieres cerrar?\",\n            zh: \"确定要关闭吗?\"\n    },\n   completeClearCache: {\n        en: \"Clear data after downloading\",\n        es: \"Borrar datos después de descargar\",\n        zh: \"下载完成后清空数据\"\n        }\n    };\n})();"
  },
  {
    "path": "catch-script/recorder.js",
    "content": "(function () {\n    console.log(\"recorder.js Start\");\n    if (document.getElementById(\"catCatchRecorder\")) { return; }\n    const isFirefox = navigator.userAgent.toLowerCase().includes('firefox');\n\n    // let language = \"en\";\n    let language = navigator.language.replace(\"-\", \"_\");\n    if (window.CatCatchI18n) {\n        if (!window.CatCatchI18n.languages.includes(language)) {\n            language = language.split(\"_\")[0];\n            if (!window.CatCatchI18n.languages.includes(language)) {\n                language = \"en\";\n            }\n        }\n    }\n\n    const buttonStyle = 'style=\"border:solid 1px #000;margin:2px;padding:2px;background:#fff;border-radius:4px;border:solid 1px #c7c7c780;color:#000;\"';\n    const checkboxStyle = 'style=\"-webkit-appearance: auto;\"';\n\n    const CatCatch = document.createElement(\"div\");\n    CatCatch.setAttribute(\"id\", \"catCatchRecorder\");\n    CatCatch.innerHTML = `<img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYBAMAAAASWSDLAAAAKlBMVEUAAADLlROxbBlRAD16GS5oAjWWQiOCIytgADidUx/95gHqwwTx0gDZqwT6kfLuAAAACnRSTlMA/vUejV7kuzi8za0PswAAANpJREFUGNNjwA1YSxkYTEqhnKZLLi6F1w0gnKA1shdvHYNxdq1atWobjLMKCOAyC3etlVrUAOH4HtNZmLgoAMKpXX37zO1FwcZAwMDguGq1zKpFmTNnzqx0Bpp2WvrU7ttn9py+I8JgLn1R8Pad22vurNkjwsBReHv33junzuyRnOnMwNCSeFH27K5dq1SNgcZxFMnuWrNq1W5VkNntihdv7ToteGcT0C7mIkE1qbWCYjJnM4CqEoWKdoslChXuUgXJqIcLebiphSgCZRhaPDhcDFhdmUMCGIgEAFA+Uc02aZg9AAAAAElFTkSuQmCC\" style=\"-webkit-user-drag: none;width: 20px;\">\n    <div id=\"tips\"></div>\n    <span data-i18n=\"selectVideo\">选择视频</span> <select id=\"videoList\" style=\"max-width: 200px;\"></select>\n    <span data-i18n=\"recordEncoding\">录制编码</span> <select id=\"mimeTypeList\" style=\"max-width: 200px;\"></select>\n    <label><input type=\"checkbox\" id=\"ffmpeg\" ${checkboxStyle}><span data-i18n=\"ffmpeg\">使用ffmpeg转码</span></label>\n    <label>\n        <select id=\"videoBits\">\n            <option value=\"2500000\" data-i18n=\"videoBits\">视频码率</option>\n            <option value=\"2500000\">2.5 Mbps</option>\n            <option value=\"5000000\">5 Mbps</option>\n            <option value=\"8000000\">8 Mbps</option>\n            <option value=\"16000000\">16 Mbps</option>\n        </select>\n        <select id=\"audioBits\">\n            <option value=\"128000\" data-i18n=\"audioBits\">视频码率</option>\n            <option value=\"128000\">128 kbps</option>\n            <option value=\"256000\">256 kbps</option>\n        </select>\n        <select id=\"frameRate\">\n            <option value=\"0\" data-i18n=\"frameRate\">帧率</option>\n            <option value=\"25\">25 FPS</option>\n            <option value=\"30\">30 FPS</option>\n            <option value=\"60\">60 FPS</option>\n            <option value=\"120\">120 FPS</option>\n        </select>\n    </label>\n    <div>\n        <button id=\"getVideo\" ${buttonStyle} data-i18n=\"readVideo\">读取视频</button>\n        <button id=\"start\" ${buttonStyle} data-i18n=\"startRecording\">开始录制</button>\n        <button id=\"stop\" ${buttonStyle} data-i18n=\"stopRecording\">停止录制</button>\n        <button id=\"hide\" ${buttonStyle} data-i18n=\"hide\">隐藏</button>\n        <button id=\"close\" ${buttonStyle} data-i18n=\"close\">关闭</button>\n    </div>`;\n    CatCatch.style = `\n        position: fixed;\n        z-index: 999999;\n        top: 10%;\n        left: 80%;\n        background: rgb(255 255 255 / 85%);\n        border: solid 1px #c7c7c7;\n        border-radius: 4px;\n        color: rgb(26, 115, 232);\n        padding: 5px 5px 5px 5px;\n        font-size: 12px;\n        font-family: \"Microsoft YaHei\", \"Helvetica\", \"Arial\", sans-serif;\n        user-select: none;\n        display: flex;\n        align-items: flex-start;\n        justify-content: space-evenly;\n        flex-direction: column;\n        line-height: 20px;`;\n\n    // 创建 Shadow DOM 放入CatCatch\n    const divShadow = document.createElement('div');\n    const shadowRoot = divShadow.attachShadow({ mode: 'closed' });\n    shadowRoot.appendChild(CatCatch);\n    // 页面插入Shadow DOM\n    document.getElementsByTagName('html')[0].appendChild(divShadow);\n\n\n    // 处理 sandbox iframe\n    setupIframeProcessing = () => {\n        document.addEventListener('DOMContentLoaded', () => {\n            const processIframe = (iframe) => {\n                if (iframe && iframe.hasAttribute && iframe.hasAttribute('sandbox')) {\n                    const clonedIframe = iframe.cloneNode(true);\n                    clonedIframe.removeAttribute('sandbox');\n                    if (iframe.parentNode) {\n                        iframe.parentNode.replaceChild(clonedIframe, iframe);\n                    }\n                }\n            };\n\n            document.querySelectorAll('iframe').forEach(processIframe);\n\n            const observer = new MutationObserver((mutationsList) => {\n                for (const mutation of mutationsList) {\n                    if (mutation.type === 'childList') {\n                        mutation.addedNodes.forEach(node => {\n                            if (node.nodeName === 'IFRAME') {\n                                processIframe(node);\n                            } else if (node.querySelectorAll) {\n                                node.querySelectorAll('iframe').forEach(processIframe);\n                            }\n                        });\n                    }\n                }\n            });\n            observer.observe(document.body || document.documentElement, { childList: true, subtree: true });\n        });\n    }\n    setupIframeProcessing();\n\n    const $tips = CatCatch.querySelector(\"#tips\");\n    const $videoList = CatCatch.querySelector(\"#videoList\");\n    const $mimeTypeList = CatCatch.querySelector(\"#mimeTypeList\");\n    const $start = CatCatch.querySelector(\"#start\");\n    const $stop = CatCatch.querySelector(\"#stop\");\n    let videoList = [];\n    $tips.innerHTML = i18n(\"noVideoDetected\", \"没有检测到视频, 请重新读取\");\n    let recorder = {};\n    let option = { mimeType: 'video/webm;codecs=vp9,opus' };\n\n    CatCatch.querySelector(\"#hide\").addEventListener('click', function (event) {\n        CatCatch.style.display = \"none\";\n    });\n    CatCatch.querySelector(\"#close\").addEventListener('click', function (event) {\n        recorder?.state && recorder.stop();\n        CatCatch.style.display = \"none\";\n        window.postMessage({ action: \"catCatchToBackground\", Message: \"script\", script: \"recorder.js\", refresh: false });\n    });\n\n    function init() {\n        getVideo();\n        $start.style.display = 'inline';\n        $stop.style.display = 'none';\n    }\n    setTimeout(init, 500);\n\n    // #region 视频编码选择\n    function setMimeType() {\n        function getSupportedMimeTypes(media, types, codecs) {\n            const supported = [];\n            types.forEach((type) => {\n                const mimeType = `${media}/${type}`;\n                codecs.forEach((codec) => [`${mimeType};codecs=${codec}`].forEach(variation => {\n                    if (MediaRecorder.isTypeSupported(variation)) {\n                        supported.push(variation);\n                    }\n                }));\n                if (MediaRecorder.isTypeSupported(mimeType)) {\n                    supported.push(mimeType);\n                }\n            });\n            return supported;\n        };\n        const videoTypes = [\"webm\", \"ogg\", \"mp4\", \"x-matroska\"];\n        const codecs = [\"should-not-be-supported\", \"vp9\", \"vp8\", \"avc1\", \"av1\", \"h265\", \"h.265\", \"h264\", \"h.264\", \"opus\", \"pcm\", \"aac\", \"mpeg\", \"mp4a\"];\n        const supportedVideos = getSupportedMimeTypes(\"video\", videoTypes, codecs);\n        supportedVideos.forEach(function (type) {\n            $mimeTypeList.options.add(new Option(type, type));\n        });\n        option.mimeType = supportedVideos[0];\n\n        $mimeTypeList.addEventListener('change', function (event) {\n            if (recorder && recorder.state && recorder.state === 'recording') {\n                $tips.innerHTML = i18n(\"recordingChangeEncoding\", \"录制中不能更改编码\");\n                return;\n            }\n            if (MediaRecorder.isTypeSupported(event.target.value)) {\n                option.mimeType = event.target.value;\n                $tips.innerHTML = event.target.value;\n            } else {\n                $tips.innerHTML = i18n(\"formatNotSupported\", \"不支持此格式\");\n            }\n        });\n    }\n    setMimeType();\n    // #endregion 视频编码选择\n\n    // #region 获取视频列表\n    function getVideo() {\n        videoList = [];\n        $videoList.options.length = 0;\n        document.querySelectorAll(\"video, audio\").forEach(function (video, index) {\n            if (video.currentSrc) {\n                const src = video.currentSrc.split(\"/\").pop();\n                videoList.push(video);\n                $videoList.options.add(new Option(src, index));\n            }\n        });\n        $tips.innerHTML = videoList.length ? i18n(\"clickToStartRecording\", \"请点击开始录制\") : i18n(\"noVideoDetected\", \"没有检测到视频, 请重新读取\");\n    }\n    CatCatch.querySelector(\"#getVideo\").addEventListener('click', getVideo);\n    CatCatch.querySelector(\"#stop\").addEventListener('click', function () {\n        recorder.stop();\n    });\n    // #endregion 获取视频列表\n\n    // 获取兼容的 captureStream 方法\n    let isMozCaptureStream = false;\n    function getCaptureStreamMethod(element) {\n        if (element.captureStream) {\n            return element.captureStream.bind(element);\n        }\n        if (element.mozCaptureStream) {\n            isMozCaptureStream = true;\n            return element.mozCaptureStream.bind(element);\n        }\n        if (element.webkitCaptureStream) {\n            return element.webkitCaptureStream.bind(element);\n        }\n        return null;\n    }\n\n    CatCatch.querySelector(\"#start\").addEventListener('click', function (event) {\n        if (!MediaRecorder.isTypeSupported(option.mimeType)) {\n            $tips.innerHTML = i18n(\"formatNotSupported\", \"不支持此格式\");\n            return;\n        }\n        init();\n        const index = $videoList.value;\n        if (index && videoList[index]) {\n            let stream = null;\n            try {\n                const frameRate = +CatCatch.querySelector(\"#frameRate\").value;\n                const captureStream = getCaptureStreamMethod(videoList[index]);\n                if (!captureStream) {\n                    throw new Error(i18n(\"recordingNotSupported\", \"不支持录制\"));\n                }\n                stream = frameRate ? captureStream(frameRate) : captureStream();\n\n                // Firefox 的 captureStream 录制时没有声音，这里使用 Web Audio API 绕过修补问题\n                if (isMozCaptureStream) {\n                    const audioCtx = new (window.AudioContext || window.webkitAudioContext)();\n                    const source = audioCtx.createMediaStreamSource(stream);\n                    source.connect(audioCtx.destination);\n                }\n            } catch (e) {\n                console.log(e);\n                $tips.innerHTML = i18n(\"recordingNotSupported\", \"不支持录制\");\n                return;\n            }\n            // 码率\n            option.audioBitsPerSecond = +CatCatch.querySelector(\"#audioBits\").value;\n            option.videoBitsPerSecond = +CatCatch.querySelector(\"#videoBits\").value;\n\n            recorder = new MediaRecorder(stream, option);\n            recorder.ondataavailable = function (event) {\n                if (CatCatch.querySelector(\"#ffmpeg\").checked) {\n                    window.postMessage({\n                        action: \"catCatchFFmpeg\",\n                        use: \"transcode\",\n                        files: [{ data: URL.createObjectURL(event.data), type: option.mimeType }],\n                        title: document.title.trim()\n                    });\n                    $tips.innerHTML = i18n(\"clickToStartRecording\", \"请点击开始录制\");\n                    return;\n                }\n                const a = document.createElement('a');\n                a.href = URL.createObjectURL(event.data);\n                a.download = `${document.title}`;\n                a.click();\n                a.remove();\n                $tips.innerHTML = i18n(\"downloadCompleted\", \"下载完成\");;\n            }\n            recorder.onstart = function (event) {\n                $stop.style.display = 'inline';\n                $start.style.display = 'none';\n                $tips.innerHTML = i18n(\"recording\", \"视频录制中\");\n            }\n            recorder.onstop = function (event) {\n                $tips.innerHTML = i18n(\"stopRecording\", \"停止录制\");\n                init();\n            }\n            recorder.onerror = function (event) {\n                init();\n                $tips.innerHTML = i18n(\"recordingFailed\", \"录制失败\");;\n                console.log(event);\n            };\n            try {\n                recorder.start();\n            } catch (e) {\n                $tips.innerHTML = i18n(\"recordingFailed\", \"录制失败\");\n                console.log(e);\n                return;\n            }\n            videoList[index].play();\n            setTimeout(() => {\n                if (recorder.state === 'recording') {\n                    $stop.style.display = 'inline';\n                    $start.style.display = 'none';\n                    $tips.innerHTML = i18n(\"recording\", \"视频录制中\");\n                }\n            }, 500);\n        } else {\n            $tips.innerHTML = i18n(\"noVideoDetected\", \"请确认视频是否存在\");\n        }\n    });\n\n    // #region 移动逻辑\n    let x, y;\n    function move(event) {\n        CatCatch.style.left = event.pageX - x + 'px';\n        CatCatch.style.top = event.pageY - y + 'px';\n    }\n    CatCatch.addEventListener('mousedown', function (event) {\n        x = event.pageX - CatCatch.offsetLeft;\n        y = event.pageY - CatCatch.offsetTop;\n        document.addEventListener('mousemove', move);\n        document.addEventListener('mouseup', function () {\n            document.removeEventListener('mousemove', move);\n        });\n    });\n    // #endregion 移动逻辑\n\n    // i18n\n    if (window.CatCatchI18n) {\n        CatCatch.querySelectorAll('[data-i18n]').forEach(function (element) {\n            element.innerHTML = window.CatCatchI18n[element.dataset.i18n][language];\n        });\n        CatCatch.querySelectorAll('[data-i18n-outer]').forEach(function (element) {\n            element.outerHTML = window.CatCatchI18n[element.dataset.i18nOuter][language];\n        });\n    }\n    function i18n(key, original = \"\") {\n        if (!window.CatCatchI18n) { return original };\n        return window.CatCatchI18n[key][language];\n    }\n})();"
  },
  {
    "path": "catch-script/recorder2.js",
    "content": "(function () {\n    console.log(\"recorder2.js Start\");\n    if (document.getElementById(\"catCatchRecorder2\")) {\n        return;\n    }\n    if (!navigator.mediaDevices) {\n        alert(\"当前网页不支持屏幕分享\");\n        return;\n    }\n\n    let language = navigator.language.replace(\"-\", \"_\");\n    if (window.CatCatchI18n) {\n        if (!window.CatCatchI18n.languages.includes(language)) {\n            language = language.split(\"_\")[0];\n            if (!window.CatCatchI18n.languages.includes(language)) {\n                language = \"en\";\n            }\n        }\n    }\n\n    // 添加style\n    const style = document.createElement(\"style\");\n    style.innerHTML = `\n        @keyframes color-change{\n            0% { outline: 4px solid rgb(26, 115, 232); }\n            50% { outline: 4px solid red; }\n            100% { outline: 4px solid rgb(26, 115, 232); }\n        }\n        #catCatchRecorder2 {\n            font-weight: bold;\n            position: absolute;\n            cursor: move;\n            z-index: 999999999;\n            outline: 4px solid rgb(26, 115, 232);\n            resize: both;\n            overflow: hidden;\n            height: 720px;\n            width: 1024px;\n            top: 30%;\n            left: 30%;\n            pointer-events: none;\n            font-size: 10px;\n        }\n        #catCatchRecorderHeader {\n            background: rgb(26, 115, 232);\n            color: white;\n            text-align: center;\n            height: 20px;\n            cursor: pointer;\n            display: flex;\n            justify-content: space-evenly;\n            align-items: center;\n            pointer-events: auto;\n        }\n        #catCatchRecorderTitle {\n            cursor: move;\n            user-select: none;\n            width: 45%;\n        }\n        #catCatchRecorderinnerCropArea {\n            height: calc(100% - 20px);\n            width: 100%;\n        }\n        .animation {\n            animation: color-change 5s infinite;\n        }\n        .input-group {\n            display: flex;\n            align-items: center;\n        }\n        .input-group label {\n            margin-right: 5px;\n        }\n        #videoBitrate, #audioBitrate {\n            width: 4rem;\n        }\n        .input-group label{\n            width: 5rem;\n        }`;\n\n    // 添加div\n    let cat = document.createElement(\"div\");\n    cat.setAttribute(\"id\", \"catCatchRecorder2\");\n    cat.innerHTML = `<div id=\"catCatchRecorderinnerCropArea\"></div>\n        <div id=\"catCatchRecorderHeader\">\n            <div class=\"input-group\">\n                <select id=\"videoBits\">\n                    <option value=\"2500000\" data-i18n=\"videoBits\">视频码率</option>\n                    <option value=\"2500000\">2.5 Mbps</option>\n                    <option value=\"5000000\">5 Mbps</option>\n                    <option value=\"8000000\">8 Mbps</option>\n                    <option value=\"16000000\">16 Mbps</option>\n                </select>\n            </div>\n            <div class=\"input-group\">\n                <select id=\"audioBits\">\n                    <option value=\"128000\" data-i18n=\"audioBits\">视频码率</option>\n                    <option value=\"128000\">128 kbps</option>\n                    <option value=\"256000\">256 kbps</option>\n                </select>\n            </div>\n            <div id=\"catCatchRecorderStart\" data-i18n=\"startRecording\">开始录制</div>\n            <div id=\"catCatchRecorderTitle\" data-i18n=\"dragWindow\">拖动窗口</div>\n            <div id=\"catCatchRecorderClose\" data-i18n=\"close\">关闭</div>\n        </div>`;\n\n    // 创建 Shadow DOM 放入CatCatch\n    const divShadow = document.createElement('div');\n    const shadowRoot = divShadow.attachShadow({ mode: 'closed' });\n    shadowRoot.appendChild(cat);\n    shadowRoot.appendChild(style);\n    document.getElementsByTagName('html')[0].appendChild(divShadow);\n\n    // 事件绑定\n    const catCatchRecorderStart = cat.querySelector(\"#catCatchRecorderStart\");\n    catCatchRecorderStart.onclick = function () {\n        if (recorder) {\n            recorder.stop();\n            return;\n        }\n        try { startRecording(); } catch (e) { console.log(e); return; }\n    }\n    cat.querySelector(\"#catCatchRecorderClose\").onclick = function () {\n        recorder && recorder.stop();\n        cat.remove();\n    }\n\n    // 拖动div\n    const catCatchRecorderinnerCropArea = cat.querySelector(\"#catCatchRecorderinnerCropArea\");\n    cat.querySelector(\"#catCatchRecorderTitle\").onpointerdown = (e) => {\n        let pos1, pos2, pos3, pos4;\n        pos3 = e.clientX;\n        pos4 = e.clientY;\n        if (pos3 - cat.offsetWidth - cat.offsetLeft > - 20 &&\n            pos4 - cat.offsetHeight - cat.offsetTop > - 20) {\n            return;\n        }\n        document.onpointermove = (e) => {\n            pos1 = pos3 - e.clientX;\n            pos2 = pos4 - e.clientY;\n            pos3 = e.clientX;\n            pos4 = e.clientY;\n            cat.style.top = cat.offsetTop - pos2 + \"px\";\n            cat.style.left = cat.offsetLeft - pos1 + \"px\";\n        }\n        document.onpointerup = () => {\n            document.onpointerup = null;\n            document.onpointermove = null;\n        }\n    }\n    // document.getElementsByTagName('html')[0].appendChild(cat);\n\n    // 初始化位置\n    const video = document.querySelector(\"video\");\n    if (video) {\n        // 调整和video一样大小\n        if (video.clientHeight >= 0 && video.clientWidth >= 0) {\n            cat.style.height = video.clientHeight + 20 + \"px\";\n            cat.style.width = video.clientWidth + \"px\";\n        }\n        // 调整到video的位置\n        const videoOffset = getElementOffset(video);\n        if (videoOffset.top >= 0 && videoOffset.left >= 0) {\n            cat.style.top = videoOffset.top + \"px\";\n            cat.style.left = videoOffset.left + \"px\";\n        }\n        // 防止遮挡菜单\n        let catAttr = cat.getBoundingClientRect();\n        if (document.documentElement.scrollTop + catAttr.bottom > document.documentElement.scrollTop + window.innerHeight) {\n            cat.style.top = document.documentElement.scrollTop + window.innerHeight - catAttr.height + \"px\";\n        }\n    }\n\n    // 录制\n    var recorder;\n    async function startRecording() {\n        const buffer = [];\n        let option = {\n            mimeType: 'video/webm;codecs=vp8,opus',\n            videoBitsPerSecond: +cat.querySelector(\"#videoBits\").value,\n            audioBitsPerSecond: +cat.querySelector(\"#audioBits\").value\n        };\n\n        if (MediaRecorder.isTypeSupported('video/webm;codecs=vp9,opus')) {\n            option.mimeType = 'video/webm;codecs=vp9,opus';\n        } else if (MediaRecorder.isTypeSupported('video/webm;codecs=h264')) {\n            option.mimeType = 'video/webm;codecs=h264';\n        }\n        const cropTarget = await CropTarget.fromElement(catCatchRecorderinnerCropArea);\n        const stream = await navigator.mediaDevices\n            .getDisplayMedia({\n                preferCurrentTab: true,\n                video: {\n                    cursor: \"never\"\n                },\n                audio: {\n                    sampleRate: 48000,\n                    sampleSize: 16,\n                    channelCount: 2\n                }\n            });\n        const [track] = stream.getVideoTracks();\n        await track.cropTo(cropTarget);\n        recorder = new MediaRecorder(stream, option);\n        recorder.start();\n        recorder.onstart = function (e) {\n            buffer.slice(0);\n            catCatchRecorderStart.innerHTML = i18n(\"stopRecording\", \"停止录制\");\n            cat.classList.add(\"animation\");\n        }\n        recorder.ondataavailable = function (e) {\n            buffer.push(e.data);\n        }\n        recorder.onstop = function () {\n            const fileBlob = new Blob(buffer, { type: option });\n            const a = document.createElement('a');\n            a.href = URL.createObjectURL(fileBlob);\n            a.download = `${document.title}.webm`;\n            a.click();\n            a.remove();\n            buffer.slice(0);\n            stream.getTracks().forEach(track => track.stop());\n            recorder = undefined;\n            catCatchRecorderStart.innerHTML = i18n(\"startRecording\", \"开始录制\");\n            cat.classList.remove(\"animation\");\n        }\n    }\n    function getElementOffset(el) {\n        const rect = el.getBoundingClientRect();\n        const scrollTop = window.pageYOffset || document.documentElement.scrollTop;\n        const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;\n        return {\n            top: rect.top + scrollTop,\n            left: rect.left + scrollLeft\n        };\n    }\n\n    // i18n\n    if (window.CatCatchI18n) {\n        CatCatch.querySelectorAll('[data-i18n]').forEach(function (element) {\n            const translation = window.CatCatchI18n[element.dataset.i18n]?.[language];\n            if (translation) {\n                element.innerHTML = translation;\n            }\n        });\n        CatCatch.querySelectorAll('[data-i18n-outer]').forEach(function (element) {\n            const outerTranslation = window.CatCatchI18n[element.dataset.i18nOuter]?.[language];\n            if (outerTranslation) {\n                element.outerHTML = outerTranslation;\n            }\n        });\n    }\n    function i18n(key, original = \"\") {\n        if (!window.CatCatchI18n) { return original };\n        return window.CatCatchI18n[key][language];\n    }\n})();"
  },
  {
    "path": "catch-script/search.js",
    "content": "// const CATCH_SEARCH_ONLY = true;\n(function __CAT_CATCH_CATCH_SCRIPT__() {\n    const isRunningInWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope;\n    const CATCH_SEARCH_DEBUG = false; // 开发调试日志\n    // 防止 console.log 被劫持\n    if (!isRunningInWorker && CATCH_SEARCH_DEBUG && console.log.toString() != 'function log() { [native code] }') {\n        const newIframe = top.document.createElement(\"iframe\");\n        newIframe.style.display = \"none\";\n        top.document.body.appendChild(newIframe);\n        window.console.log = newIframe.contentWindow.console.log;\n    }\n    // 防止 window.postMessage 被劫持\n    const _postMessage = self.postMessage;\n\n    // console.log(\"start search.js\");\n    const filter = new Set();\n    const reKeyURL = /URI=\"(.*)\"/;\n    const dataRE = /^data:(application|video|audio)\\//i;\n    const joinBaseUrlTask = [];\n    const baseUrl = new Set();\n    const regexVimeo = /^https:\\/\\/[^\\.]*\\.vimeocdn\\.com\\/exp=.*\\/playlist\\.json\\?/i;\n    const videoSet = new Set();\n    const base64Regex = /^[A-Za-z0-9+/]+={0,2}$/;\n    const hexRegex = /^[A-Fa-f0-9]+$/;\n    extractBaseUrl(location.href);\n\n    // Worker\n    const _Worker = Worker;\n    self.Worker = function (scriptURL, options) {\n        try {\n            const xhr = new XMLHttpRequest();\n            xhr.open('GET', scriptURL, false);\n            xhr.send();\n            if (xhr.status === 200) {\n                const blob = new Blob([`(${__CAT_CATCH_CATCH_SCRIPT__.toString()})();`, xhr.response], { type: 'text/javascript' });\n                const newWorker = new _Worker(URL.createObjectURL(blob), options);\n                newWorker.addEventListener(\"message\", function (event) {\n                    if (event.data?.action == \"catCatchAddKey\" || event.data?.action == \"catCatchAddMedia\") {\n                        postData(event.data);\n                    }\n                });\n                return newWorker;\n            }\n        } catch (error) {\n            return new _Worker(scriptURL, options);\n        }\n        return new _Worker(scriptURL, options);\n    }\n    self.Worker.toString = function () {\n        return _Worker.toString();\n    }\n\n    // JSON.parse\n    const _JSONparse = JSON.parse;\n    JSON.parse = function () {\n        let data = _JSONparse.apply(this, arguments);\n        findMedia(data);\n        return data;\n    }\n    JSON.parse.toString = function () {\n        return _JSONparse.toString();\n    }\n\n    async function findMedia(data, depth = 0) {\n        CATCH_SEARCH_DEBUG && console.log(data);\n        let index = 0;\n        if (!data) { return; }\n        if (data instanceof Array && data.length == 16) {\n            const isKey = data.every(function (value) {\n                return typeof value == 'number' && value <= 256\n            });\n            if (isKey) {\n                postData({ action: \"catCatchAddKey\", key: data, href: location.href, ext: \"key\" });\n                return;\n            }\n        }\n        if (data instanceof ArrayBuffer && data.byteLength == 16) {\n            postData({ action: \"catCatchAddKey\", key: data, href: location.href, ext: \"key\" });\n            return;\n        }\n        for (let key in data) {\n            if (index != 0) { depth = 0; } index++;\n            if (typeof data[key] == \"object\") {\n                // 查找疑似key\n                if (data[key] instanceof Array && data[key].length == 16) {\n                    const isKey = data[key].every(function (value) {\n                        return typeof value == 'number' && value <= 256\n                    });\n                    isKey && postData({ action: \"catCatchAddKey\", key: data[key], href: location.href, ext: \"key\" });\n                    continue;\n                }\n                if (depth > 10) { continue; }  // 防止死循环 最大深度\n                findMedia(data[key], ++depth);\n                continue;\n            }\n            if (typeof data[key] == \"string\") {\n                if (isUrl(data[key])) {\n                    const ext = getExtension(data[key]);\n                    if (ext) {\n                        const url = data[key].startsWith(\"//\") ? (location.protocol + data[key]) : data[key];\n                        extractBaseUrl(url);\n                        postData({ action: \"catCatchAddMedia\", url: url, href: location.href, ext: ext });\n                    }\n                    continue;\n                }\n                if (data[key].substring(0, 7).toUpperCase() == \"#EXTM3U\") {\n                    toUrl(data[key]);\n                    continue;\n                }\n                if (dataRE.test(data[key].substring(0, 17))) {\n                    const text = getDataM3U8(data[key]);\n                    text && toUrl(text);\n                    continue;\n                }\n                if (data[key].toLowerCase().includes(\"urn:mpeg:dash:schema:mpd\")) {\n                    toUrl(data[key], \"mpd\");\n                    continue;\n                }\n                if (CATCH_SEARCH_DEBUG && data[key].includes(\"manifest\")) {\n                    console.log(data);\n                }\n            }\n        }\n    }\n\n    // XHR\n    const _xhrOpen = XMLHttpRequest.prototype.open;\n    XMLHttpRequest.prototype.open = function (method) {\n        method = method.toUpperCase();\n        CATCH_SEARCH_DEBUG && console.log(this);\n        this.addEventListener(\"readystatechange\", function (event) {\n            CATCH_SEARCH_DEBUG && console.log(this);\n            if (this.status != 200) { return; }\n\n            // 处理viemo\n            this.responseURL.includes(\"vimeocdn.com\") && vimeo(this.responseURL, this.response);\n\n            // 查找疑似key\n            if (this.responseType === \"arraybuffer\" && this.response?.byteLength) {\n                if (this.response.byteLength === 16 || this.response.byteLength === 32) {\n                    postData({ action: \"catCatchAddKey\", key: this.response, href: location.href, ext: \"key\" });\n                }\n                if (this.responseURL.includes(\".ts\")) {\n                    extractBaseUrl(this.responseURL);\n                }\n            }\n            if (typeof this.response == \"object\") {\n                findMedia(this.response);\n                return;\n            }\n            if (this.response == \"\" || typeof this.response != \"string\") { return; }\n\n            if (dataRE.test(this.response)) {\n                const text = getDataM3U8(this.response);\n                text && toUrl(text);\n                return;\n            }\n            if (dataRE.test(this.responseURL)) {\n                const text = getDataM3U8(this.responseURL);\n                text && toUrl(text);\n                return;\n            }\n            if (isUrl(this.response)) {\n                const ext = getExtension(this.response);\n                ext && postData({ action: \"catCatchAddMedia\", url: this.response, href: location.href, ext: ext });\n                return;\n            }\n            const responseUpper = this.response.toUpperCase();\n            if (responseUpper.includes(\"#EXTM3U\")) {\n                if (responseUpper.substring(0, 7) == \"#EXTM3U\") {\n                    if (method == \"GET\") {\n                        toUrl(addBaseUrl(getBaseUrl(this.responseURL), this.response));\n                        postData({ action: \"catCatchAddMedia\", url: this.responseURL, href: location.href, ext: \"m3u8\" });\n                        return;\n                    }\n                    toUrl(this.response);\n                    return;\n                }\n                if (isJSON(this.response)) {\n                    if (method == \"GET\") {\n                        postData({ action: \"catCatchAddMedia\", url: this.responseURL, href: location.href, ext: \"json\" });\n                        return;\n                    }\n                    toUrl(this.response, \"json\");\n                    return;\n                }\n            }\n            // dash DRM\n            // if (responseUpper.includes(\"<MPD\") && responseUpper.includes(\"</MPD>\")) {\n            //     _postMessage({\n            //         action: \"catCatchDashDRMMedia\",\n            //         url: this.responseURL,\n            //         data: this.response,\n            //         href: location.href\n            //     });\n            //     return;\n            // }\n            const isJson = isJSON(this.response);\n            if (isJson) {\n                findMedia(isJson);\n                return;\n            }\n        });\n        _xhrOpen.apply(this, arguments);\n    }\n    XMLHttpRequest.prototype.open.toString = function () {\n        return _xhrOpen.toString();\n    }\n\n    // fetch\n    const _fetch = fetch;\n    fetch = async function (input, init) {\n        let response;\n        try {\n            response = await _fetch.apply(this, arguments);\n        } catch (error) {\n            console.error(\"Fetch error:\", error);\n            throw error; // Re-throw the error if necessary\n        }\n        const clone = response.clone();\n        CATCH_SEARCH_DEBUG && console.log(response);\n        response.arrayBuffer()\n            .then(arrayBuffer => {\n                CATCH_SEARCH_DEBUG && console.log({ arrayBuffer, input });\n                if (arrayBuffer.byteLength == 16) {\n                    postData({ action: \"catCatchAddKey\", key: arrayBuffer, href: location.href, ext: \"key\" });\n                    return;\n                }\n                let text = new TextDecoder().decode(arrayBuffer);\n                if (text == \"\") { return; }\n                if (typeof input == \"object\") { input = input.url; }\n                let isJson = isJSON(text);\n                if (isJson) {\n                    findMedia(isJson);\n                    return;\n                }\n                if (text.substring(0, 7).toUpperCase() == \"#EXTM3U\") {\n                    if (init?.method == undefined || (init.method && init.method.toUpperCase() == \"GET\")) {\n                        toUrl(addBaseUrl(getBaseUrl(input), text));\n                        postData({ action: \"catCatchAddMedia\", url: input, href: location.href, ext: \"m3u8\" });\n                        return;\n                    }\n                    toUrl(text);\n                    return;\n                }\n                if (dataRE.test(text.substring(0, 17))) {\n                    const data = getDataM3U8(text);\n                    data && toUrl(data);\n                    return;\n                }\n            });\n        return clone;\n    }\n    fetch.toString = function () {\n        return _fetch.toString();\n    }\n\n    // Array.prototype.slice\n    const _slice = Array.prototype.slice;\n    Array.prototype.slice = function (start, end) {\n        const data = _slice.apply(this, arguments);\n        if (end == 16 && this.length == 32) {\n            CATCH_SEARCH_DEBUG && console.log(this, start, end, data);\n            for (let item of data) {\n                if (typeof item != \"number\" || item > 255) { return data; }\n            }\n            postData({ action: \"catCatchAddKey\", key: data, href: location.href, ext: \"key\" });\n        }\n        return data;\n    }\n    Array.prototype.slice.toString = function () {\n        return _slice.toString();\n    }\n\n    //#region TypedArray.prototype.subarray\n    const createSubarrayWrapper = (originalSubarray) => {\n        return function (start, end) {\n            const data = originalSubarray.apply(this, arguments);\n            CATCH_SEARCH_DEBUG && console.log(this, start, end, data);\n            if (data.byteLength == 16) {\n                const uint8 = new _Uint8Array(data);\n                const isValid = Array.from(uint8).every(item => typeof item == \"number\" && item <= 255);\n                isValid && postData({ action: \"catCatchAddKey\", key: uint8.buffer, href: location.href, ext: \"key\" });\n            }\n            return data;\n        }\n    }\n    // Int8Array.prototype.subarray\n    const _Int8ArraySubarray = Int8Array.prototype.subarray;\n    Int8Array.prototype.subarray = createSubarrayWrapper(_Int8ArraySubarray);\n    Int8Array.prototype.subarray.toString = function () {\n        return _Int8ArraySubarray.toString();\n    }\n    // Uint8Array.prototype.subarray\n    const _Uint8ArraySubarray = Uint8Array.prototype.subarray;\n    Uint8Array.prototype.subarray = createSubarrayWrapper(_Uint8ArraySubarray);\n    Uint8Array.prototype.subarray.toString = function () {\n        return _Uint8ArraySubarray.toString();\n    }\n    //#endregion\n\n    // window.btoa / window.atob\n    const _btoa = btoa;\n    btoa = function (data) {\n        const base64 = _btoa.apply(this, arguments);\n        CATCH_SEARCH_DEBUG && console.log(base64, data, base64.length);\n        if (base64.length == 24 && base64.substring(22, 24) == \"==\") {\n            postData({ action: \"catCatchAddKey\", key: base64, href: location.href, ext: \"base64Key\" });\n        }\n        if (data.substring(0, 7).toUpperCase() == \"#EXTM3U\") {\n            toUrl(data);\n        }\n        return base64;\n    }\n    btoa.toString = function () {\n        return _btoa.toString();\n    }\n    const _atob = atob;\n    atob = function (base64) {\n        const data = _atob.apply(this, arguments);\n        CATCH_SEARCH_DEBUG && console.log(base64, data, base64.length);\n        if (base64.length == 24 && base64.substring(22, 24) == \"==\") {\n            postData({ action: \"catCatchAddKey\", key: base64, href: location.href, ext: \"base64Key\" });\n        }\n        if (data.substring(0, 7).toUpperCase() == \"#EXTM3U\") {\n            toUrl(data);\n        }\n        if (data.endsWith(\"</MPD>\")) {\n            toUrl(data, \"mpd\");\n        }\n        return data;\n    }\n    atob.toString = function () {\n        return _atob.toString();\n    }\n\n    // fromCharCode\n    const originalFromCharCode = String.fromCharCode;\n    const proxyFromCharCode = new Proxy(originalFromCharCode, {\n        apply(target, thisArg, argumentsList) {\n            const data = Reflect.apply(target, thisArg, argumentsList);\n            if (data.length < 7) { return data; }\n            CATCH_SEARCH_DEBUG && console.log(data, thisArg, argumentsList);\n            if (data.substring(0, 7) == \"#EXTM3U\" || data.includes(\"#EXTINF:\")) {\n                m3u8Text += data;\n                if (m3u8Text.includes(\"#EXT-X-ENDLIST\")) {\n                    toUrl(m3u8Text.split(\"#EXT-X-ENDLIST\")[0] + \"#EXT-X-ENDLIST\");\n                    m3u8Text = '';\n                }\n                return data;\n            }\n            const key = data.replaceAll(\"\\u0010\", \"\");\n            if (key.length == 32 && hexRegex.test(key)) {\n                postData({ action: \"catCatchAddKey\", key: key, href: location.href, ext: \"key\" });\n            }\n            return data;\n        }\n    });\n    String.fromCharCode = proxyFromCharCode;\n    String.fromCharCode.toString = function () {\n        return originalFromCharCode.toString();\n    };\n\n    // DataView\n    // const _DataView = DataView;\n    // DataView = new Proxy(_DataView, {\n    //     construct(target, args) {\n    //         let instance = new target(...args);\n    //         // 劫持常用的set方法\n    //         for (const methodName of ['setInt8', 'setUint8', 'setInt16', 'setUint16', 'setInt32', 'setUint32']) {\n    //             if (typeof instance[methodName] !== 'function') {\n    //                 continue;\n    //             }\n    //             instance[methodName] = new Proxy(instance[methodName], {\n    //                 apply(target, thisArg, argArray) {\n    //                     const result = Reflect.apply(target, thisArg, argArray);\n    //                     if (thisArg.byteLength == 16) {\n    //                         postData({ action: \"catCatchAddKey\", key: thisArg.buffer, href: location.href, ext: \"key\" });\n    //                     }\n    //                     return result;\n    //                 }\n    //             });\n    //         }\n    //         CATCH_SEARCH_DEBUG && console.log(target.name, args, instance);\n    //         if (instance.byteLength == 16 && instance.buffer.byteLength == 16) {\n    //             postData({ action: \"catCatchAddKey\", key: instance.buffer, href: location.href, ext: \"key\" });\n    //         }\n    //         if (instance.byteLength == 256 || instance.byteLength == 128 || instance.byteLength == 32) {\n    //             const _buffer = isRepeatedExpansion(instance.buffer, 16);\n    //             if (_buffer) {\n    //                 postData({ action: \"catCatchAddKey\", key: _buffer, href: location.href, ext: \"key\" });\n    //             }\n    //         }\n    //         if (instance.byteLength == 32) {\n    //             const key = instance.buffer.slice(0, 16);\n    //             postData({ action: \"catCatchAddKey\", key: key, href: location.href, ext: \"key\" });\n    //         }\n    //         return instance;\n    //     }\n    // });\n\n    const _DataView = DataView;\n    DataView = function () {\n        // 创建原始 DataView 实例\n        const instance = new _DataView(...arguments);\n        // 劫持常用的 set 方法\n        for (const methodName of ['setInt8', 'setUint8', 'setInt16', 'setUint16', 'setInt32', 'setUint32']) {\n            if (typeof instance[methodName] !== 'function') {\n                continue;\n            }\n            const originalMethod = instance[methodName];\n            instance[methodName] = function (...args) {\n                const result = originalMethod.apply(this, args);\n                // 在方法调用后检查条件\n                if (this.byteLength === 16) {\n                    postData({ action: \"catCatchAddKey\", key: this.buffer, href: location.href, ext: \"key\" });\n                }\n                return result;\n            };\n        }\n        CATCH_SEARCH_DEBUG && console.log(_DataView.name, arguments, instance);\n        // 根据 byteLength 条件发送数据\n        if (instance.byteLength === 16 && instance.buffer.byteLength === 16) {\n            postData({ action: \"catCatchAddKey\", key: instance.buffer, href: location.href, ext: \"key\" });\n        }\n        if (instance.byteLength === 256 || instance.byteLength === 128 || instance.byteLength === 32) {\n            const _buffer = isRepeatedExpansion(instance.buffer, 16);\n            if (_buffer) {\n                postData({ action: \"catCatchAddKey\", key: _buffer, href: location.href, ext: \"key\" });\n            }\n        }\n        if (instance.byteLength === 32) {\n            const key = instance.buffer.slice(0, 16);\n            postData({ action: \"catCatchAddKey\", key: key, href: location.href, ext: \"key\" });\n        }\n        return instance;\n    }\n    DataView.toString = function () {\n        return _DataView.toString();\n    }\n\n    // escape\n    const _escape = escape;\n    escape = function (str) {\n        CATCH_SEARCH_DEBUG && console.log(str);\n        if (str?.length && str.length == 24 && str.substring(22, 24) == \"==\") {\n            postData({ action: \"catCatchAddKey\", key: str, href: location.href, ext: \"base64Key\" });\n        }\n        return _escape(str);\n    }\n    escape.toString = function () {\n        return _escape.toString();\n    }\n\n    // indexOf\n    const _indexOf = String.prototype.indexOf;\n    String.prototype.indexOf = function (searchValue, fromIndex) {\n        const out = _indexOf.apply(this, arguments);\n        // CATCH_SEARCH_DEBUG && console.log(this, searchValue, fromIndex, out);\n        if (searchValue === '#EXTM3U' && out !== -1) {\n            const data = this.substring(fromIndex);\n            toUrl(data);\n        }\n        return out;\n    }\n    String.prototype.indexOf.toString = function () {\n        return _indexOf.toString();\n    }\n\n    const uint32ArrayToUint8Array_ = (array) => {\n        const newArray = new Uint8Array(16);\n        for (let i = 0; i < 4; i++) {\n            newArray[i * 4] = (array[i] >> 24) & 0xff;\n            newArray[i * 4 + 1] = (array[i] >> 16) & 0xff;\n            newArray[i * 4 + 2] = (array[i] >> 8) & 0xff;\n            newArray[i * 4 + 3] = array[i] & 0xff;\n        }\n        return newArray;\n    }\n    const uint16ArrayToUint8Array_ = (array) => {\n        const newArray = new Uint8Array(16);\n        for (let i = 0; i < 8; i++) {\n            newArray[i * 2] = (array[i] >> 8) & 0xff;\n            newArray[i * 2 + 1] = array[i] & 0xff;\n        }\n        return newArray;\n    }\n    // findTypedArray\n    const findTypedArray = (target, args) => {\n        const isArray = Array.isArray(args[0]) && args[0].length === 16;\n        const isArrayBuffer = args[0] instanceof ArrayBuffer && args[0].byteLength === 16;\n        const instance = new target(...args);\n        CATCH_SEARCH_DEBUG && console.log(target.name, args, instance);\n        if (isArray || isArrayBuffer) {\n            postData({ action: \"catCatchAddKey\", key: args[0], href: location.href, ext: \"key\" });\n        } else if (instance.buffer.byteLength === 16) {\n            if (target.name === 'Uint32Array') {\n                postData({ action: \"catCatchAddKey\", key: uint32ArrayToUint8Array_(instance).buffer, href: location.href, ext: \"key\" });\n            } else if (target.name === 'Uint16Array') {\n                postData({ action: \"catCatchAddKey\", key: uint16ArrayToUint8Array_(instance).buffer, href: location.href, ext: \"key\" });\n            } else {\n                postData({ action: \"catCatchAddKey\", key: instance.buffer, href: location.href, ext: \"key\" });\n            }\n        }\n        return instance;\n    }\n    // Uint8Array\n    const _Uint8Array = Uint8Array;\n    Uint8Array = new Proxy(_Uint8Array, {\n        construct(target, args) {\n            return findTypedArray(target, args);\n        }\n    });\n    // Uint16Array\n    const _Uint16Array = Uint16Array;\n    Uint16Array = new Proxy(_Uint16Array, {\n        construct(target, args) {\n            return findTypedArray(target, args);\n        }\n    });\n    // Uint32Array\n    const _Uint32Array = Uint32Array;\n    Uint32Array = new Proxy(_Uint32Array, {\n        construct(target, args) {\n            return findTypedArray(target, args);\n        }\n    });\n\n    // join\n    const _arrayJoin = Array.prototype.join;\n    Array.prototype.join = function () {\n        const data = _arrayJoin.apply(this, arguments);\n        // CATCH_SEARCH_DEBUG && console.log(data, this, arguments);\n        if (data.substring(0, 7).toUpperCase() == \"#EXTM3U\") {\n            toUrl(data);\n        }\n        if (data.length == 24) {\n            // 判断是否是base64\n            CATCH_SEARCH_DEBUG && console.log(data, this, arguments);\n            base64Regex.test(data) && postData({ action: \"catCatchAddKey\", key: data, href: location.href, ext: \"base64Key\" });\n        }\n        return data;\n    }\n    Array.prototype.join.toString = function () {\n        return _arrayJoin.toString();\n    }\n\n    function isUrl(str) {\n        return (str.startsWith(\"http://\") || str.startsWith(\"https://\") || str.startsWith(\"//\"));\n    }\n    function isFullM3u8(text) {\n        let tsLists = text.split(\"\\n\");\n        for (let ts of tsLists) {\n            if (ts[0] == \"#\") { continue; }\n            if (isUrl(ts)) { return true; }\n            return false;\n        }\n        return false;\n    }\n    function TsProtocol(text) {\n        let tsLists = text.split(\"\\n\");\n        for (let i in tsLists) {\n            if (tsLists[i][0] == \"#\") { continue; }\n            if (tsLists[i].startsWith(\"//\")) {\n                tsLists[i] = location.protocol + tsLists[i];\n            }\n        }\n        // return tsLists.join(\"\\n\");\n        return _arrayJoin.call(tsLists, \"\\n\");\n    }\n    function getBaseUrl(url) {\n        let bashUrl = url.split(\"/\");\n        bashUrl.pop();\n        // return baseUrl.join(\"/\") + \"/\";\n        return _arrayJoin.call(bashUrl, \"/\") + \"/\";\n    }\n    function addBaseUrl(baseUrl, m3u8Text) {\n        let m3u8_split = m3u8Text.split(\"\\n\");\n        m3u8Text = \"\";\n        for (let ts of m3u8_split) {\n            if (ts == \"\" || ts == \" \" || ts == \"\\n\") { continue; }\n            if (ts.includes(\"URI=\")) {\n                let KeyURL = reKeyURL.exec(ts);\n                if (KeyURL && KeyURL[1] && !isUrl(KeyURL[1])) {\n                    ts = ts.replace(reKeyURL, 'URI=\"' + baseUrl + KeyURL[1] + '\"');\n                }\n            }\n            if (ts[0] != \"#\" && !isUrl(ts)) {\n                if (ts.startsWith(\"/\")) {\n                    // url根目录\n                    const urlSplit = baseUrl.split(\"/\");\n                    ts = urlSplit[0] + \"//\" + urlSplit[2] + ts;\n                } else {\n                    ts = baseUrl + ts;\n                }\n            }\n            m3u8Text += ts + \"\\n\";\n        }\n        return m3u8Text;\n    }\n    function isJSON(str) {\n        if (typeof str == \"object\") {\n            return str;\n        }\n        if (typeof str == \"string\") {\n            try {\n                return _JSONparse(str);\n            } catch (e) { return false; }\n        }\n        return false;\n    }\n    function getExtension(str) {\n        let ext;\n        try {\n            if (str.startsWith(\"//\")) {\n                str = location.protocol + str;\n            }\n            ext = new URL(str);\n        } catch (e) { return undefined; }\n        ext = ext.pathname.split(\".\");\n        if (ext.length == 1) { return undefined; }\n        ext = ext[ext.length - 1].toLowerCase();\n        if (ext == \"m3u8\" ||\n            ext == \"m3u\" ||\n            ext == \"mpd\" ||\n            ext == \"mp4\" ||\n            ext == \"mp3\" ||\n            ext == \"flv\" ||\n            ext == \"key\"\n        ) { return ext; }\n        return false;\n    }\n    function toUrl(text, ext = \"m3u8\") {\n        if (!text) { return; }\n        // 处理ts地址无protocol\n        text = TsProtocol(text);\n        if (isFullM3u8(text)) {\n            let url = URL.createObjectURL(new Blob([new TextEncoder(\"utf-8\").encode(text)]));\n            postData({ action: \"catCatchAddMedia\", url: url, href: location.href, ext: ext });\n            return;\n        }\n        baseUrl.forEach((url) => {\n            url = URL.createObjectURL(new Blob([new TextEncoder(\"utf-8\").encode(addBaseUrl(url, text))]));\n            postData({ action: \"catCatchAddMedia\", url: url, href: location.href, ext: ext });\n        });\n        joinBaseUrlTask.push((url) => {\n            url = URL.createObjectURL(new Blob([new TextEncoder(\"utf-8\").encode(addBaseUrl(url, text))]));\n            postData({ action: \"catCatchAddMedia\", url: url, href: location.href, ext: ext });\n        });\n    }\n    function getDataM3U8(text) {\n        text = text.substring(text.indexOf('/') + 1);\n        const mimeTypes = [\"vnd.apple.mpegurl\", \"x-mpegurl\", \"mpegurl\"];\n\n        const matchedType = mimeTypes.find(type =>\n            text.toLowerCase().startsWith(type)\n        );\n\n        if (!matchedType) return false;\n        const remainingText = text.slice(matchedType.length + 1);\n        const [prefix, data] = remainingText.split(/,(.+)/);\n\n        return prefix.toLowerCase() === 'base64'\n            ? _atob(data)\n            : remainingText;\n    }\n    function postData(data) {\n        let value = data.url ? data.url : data.key;\n        if (value instanceof ArrayBuffer || value instanceof Array) {\n            if (value.byteLength == 0) { return; }\n            if (data.action == \"catCatchAddKey\") {\n                // 判断是否ftyp\n                const uint8 = new _Uint8Array(value);\n                if ((uint8[4] === 0x73 || uint8[4] === 0x66) && uint8[5] == 0x74 && uint8[6] == 0x79 && uint8[7] == 0x70) {\n                    return;\n                }\n            }\n            data.key = ArrayToBase64(value);\n            if (data.key === false) { return; }\n            value = data.key;\n        }\n        /**\n         * AAAAAAAA... 空数据\n         */\n        if (data.action == \"catCatchAddKey\" && (data.key && data.key.startsWith(\"AAAAAAAAAAAAAAAAAAAA\"))) {\n            return;\n        }\n        if (filter.has(value)) { return false; }\n        filter.add(value);\n        data.requestId = Date.now().toString() + filter.size;\n        _postMessage(data);\n    }\n    function ArrayToBase64(data) {\n        try {\n            let bytes = new _Uint8Array(data);\n            let binary = \"\";\n            for (let i = 0; i < bytes.byteLength; i++) {\n                binary += _fromCharCode(bytes[i]);\n            }\n            if (typeof _btoa == \"function\") {\n                return _btoa(binary);\n            }\n            return _btoa(binary);\n        } catch (e) {\n            return false;\n        }\n    }\n    function isRepeatedExpansion(array, expansionLength) {\n        let _buffer = new _Uint8Array(expansionLength);\n        array = new _Uint8Array(array);\n        for (let i = 0; i < expansionLength; i++) {\n            _buffer[i] = array[i];\n            for (let j = i + expansionLength; j < array.byteLength; j += expansionLength) {\n                if (array[i] !== array[j]) {\n                    return false;\n                }\n            }\n        }\n        return _buffer.buffer;\n    }\n    function extractBaseUrl(url) {\n        let urlSplit = url.split(\"/\");\n        urlSplit.pop();\n        urlSplit = urlSplit.join(\"/\") + \"/\";\n        if (!baseUrl.has(urlSplit)) {\n            joinBaseUrlTask.forEach(fn => fn(urlSplit));\n            baseUrl.add(urlSplit);\n        }\n    }\n\n    // vimeo json 翻译为 m3u8\n    async function vimeo(originalUrl, json) {\n        if (!json || !regexVimeo.test(originalUrl) || videoSet.has(originalUrl)) return;\n\n        const data = isJSON(json);\n        if (!data?.base_url || !data?.video) return;\n\n        videoSet.add(originalUrl);\n\n        try {\n            const url = new URL(originalUrl);\n            const pathBase = url.pathname.substring(0, url.pathname.lastIndexOf('/')) + \"/\";\n            const baseURL = new URL(url.origin + pathBase + data.base_url).href;\n\n            let M3U8List = [\"#EXTM3U\", \"#EXT-X-INDEPENDENT-SEGMENTS\", \"#EXT-X-VERSION:3\"];\n\n            const toM3U8 = (stream) => {\n                if (!stream.segments || stream.segments.length == 0) return null;\n                let M3U8 = [\n                    \"#EXTM3U\",\n                    \"#EXT-X-VERSION:3\",\n                    `#EXT-X-TARGETDURATION:${stream.duration}`,\n                    \"#EXT-X-MEDIA-SEQUENCE:0\",\n                    \"#EXT-X-PLAYLIST-TYPE:VOD\"\n                ];\n                if (stream.init_segment) {\n                    M3U8.push(`#EXT-X-MAP:URI=\"data:application/octet-stream;base64,${stream.init_segment}\"`);\n                } else if (stream.init_segment_url) {\n                    M3U8.push(`#EXT-X-MAP:URI=\"${baseURL}${stream.base_url}${stream.init_segment_url}\"`);\n                }\n                for (const segment of stream.segments) {\n                    M3U8.push(`#EXTINF:${segment.end - segment.start},`);\n                    M3U8.push(`${baseURL}${stream.base_url}${segment.url}`);\n                }\n                M3U8.push(\"#EXT-X-ENDLIST\");\n                return URL.createObjectURL(\n                    new Blob([new TextEncoder(\"utf-8\").encode(_arrayJoin.call(M3U8, \"\\n\"))])\n                );\n            }\n\n            if (data.video) {\n                for (const stream of data.video) {\n                    const blobUrl = toM3U8(stream);\n                    if (!blobUrl) continue;\n                    M3U8List.push(`#EXT-X-STREAM-INF:BANDWIDTH=${stream.bitrate},RESOLUTION=${stream.width}x${stream.height},CODECS=\"${stream.codecs}\"`);\n                    M3U8List.push(blobUrl);\n                }\n            }\n            if (data.audio) {\n                for (const stream of data.audio) {\n                    const blobUrl = toM3U8(stream);\n                    if (!blobUrl) continue;\n                    M3U8List.push(`#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"${stream.id}\",NAME=\"${stream.bitrate}\",URI=\"${blobUrl}\"`);\n                }\n            }\n            const blobUrl = URL.createObjectURL(\n                new Blob([new TextEncoder(\"utf-8\").encode(_arrayJoin.call(M3U8List, \"\\n\"))])\n            );\n            postData({ action: \"catCatchAddMedia\", url: blobUrl, href: location.href, ext: \"m3u8\" });\n\n        } catch (e) {\n            CATCH_SEARCH_DEBUG && console.error(\"Error processing Vimeo stream:\", e);\n        }\n    }\n\n\n    // 等待页面加载完毕 读取网页中的脚本\n    if (!isRunningInWorker && typeof document !== \"undefined\") {\n        document.addEventListener(\"DOMContentLoaded\", async function () {\n            const patterns = [\n                /[\"']((?:(?:https?:)?\\/\\/)?[^\"'\\s]*?\\.(?:m3u8|mp4|flv)(?:\\?[^\"'\\s]*)?)[\"']/gi\n            ];\n            document.querySelectorAll('script:not([src])').forEach((script) => {\n                if (script.textContent) {\n                    patterns.forEach((pattern) => {\n                        let match;\n                        while ((match = pattern.exec(script.textContent)) !== null) {\n                            let url = match[1] || match[0];\n                            // 清理URL\n                            url = url.replace(/['\"]/g, '').trim();\n                            if (url && !url.startsWith('http')) {\n                                // 补全协议\n                                url = window.location.protocol + '//' + url.replace(/^\\/\\//, '');\n                            }\n                            if (url && isUrl(url)) {\n                                postData({ action: \"catCatchAddMedia\", url: url, href: location.href, ext: \"m3u8\" });\n                            }\n                        }\n                    });\n                }\n            });\n\n        });\n    }\n})();"
  },
  {
    "path": "catch-script/webrtc.js",
    "content": "(function () {\n    console.log(\"webrtc.js Start\");\n    if (document.getElementById(\"catCatchWebRTC\")) { return; }\n\n    // 多语言\n    let language = navigator.language.replace(\"-\", \"_\");\n    if (window.CatCatchI18n) {\n        if (!window.CatCatchI18n.languages.includes(language)) {\n            language = language.split(\"_\")[0];\n            if (!window.CatCatchI18n.languages.includes(language)) {\n                language = \"en\";\n            }\n        }\n    }\n\n    const buttonStyle = 'style=\"border:solid 1px #000;margin:2px;padding:2px;background:#fff;border-radius:4px;border:solid 1px #c7c7c780;color:#000;\"';\n    const checkboxStyle = 'style=\"-webkit-appearance: auto;\"';\n    const CatCatch = document.createElement(\"div\");\n    CatCatch.innerHTML = `<img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYBAMAAAASWSDLAAAAKlBMVEUAAADLlROxbBlRAD16GS5oAjWWQiOCIytgADidUx/95gHqwwTx0gDZqwT6kfLuAAAACnRSTlMA/vUejV7kuzi8za0PswAAANpJREFUGNNjwA1YSxkYTEqhnKZLLi6F1w0gnKA1shdvHYNxdq1atWobjLMKCOAyC3etlVrUAOH4HtNZmLgoAMKpXX37zO1FwcZAwMDguGq1zKpFmTNnzqx0Bpp2WvrU7ttn9py+I8JgLn1R8Pad22vurNkjwsBReHv33junzuyRnOnMwNCSeFH27K5dq1SNgcZxFMnuWrNq1W5VkNntihdv7ToteGcT0C7mIkE1qbWCYjJnM4CqEoWKdoslChXuUgXJqIcLebiphSgCZRhaPDhcDFhdmUMCGIgEAFA+Uc02aZg9AAAAAElFTkSuQmCC\" style=\"-webkit-user-drag: none;width: 20px;\">\n    <div id=\"tips\" data-i18n=\"waiting\">正在等待视频流...\"</div>\n    <div id=\"time\"></div>\n    ${i18n(\"selectVideo\", \"选择视频\")}:\n        <select id=\"videoTrack\">\n            <option value=\"-1\">${i18n(\"selectVideo\", \"选择视频\")}</option>\n        </select>\n    ${i18n(\"selectAudio\", \"选择音频\")}:\n        <select id=\"audioTrack\">\n            <option value=\"-1\">${i18n(\"selectAudio\", \"选择视频\")}</option>\n        </select>\n    ${i18n(\"recordEncoding\", \"录制编码\")}: <select id=\"mimeTypeList\" style=\"max-width: 200px;\"></select>\n    <label><input type=\"checkbox\" id=\"autoSave1\"} ${checkboxStyle} data-i18n=\"save1hour\">1小时保存一次</label>\n    <label>\n        <select id=\"videoBits\">\n            <option value=\"2500000\" data-i18n=\"videoBits\">视频码率</option>\n            <option value=\"2500000\">2.5 Mbps</option>\n            <option value=\"5000000\">5 Mbps</option>\n            <option value=\"8000000\">8 Mbps</option>\n            <option value=\"16000000\">16 Mbps</option>\n        </select>\n        <select id=\"audioBits\">\n            <option value=\"128000\" data-i18n=\"audioBits\">音频码率</option>\n            <option value=\"128000\">128 kbps</option>\n            <option value=\"256000\">256 kbps</option>\n        </select>\n    </label>\n    <div>\n        <button id=\"start\" ${buttonStyle} data-i18n=\"startRecording\">开始录制</button>\n        <button id=\"stop\" ${buttonStyle} data-i18n=\"stopRecording\">停止录制</button>\n        <button id=\"save\" ${buttonStyle} data-i18n=\"save\">保存</button>\n        <button id=\"hide\" ${buttonStyle} data-i18n=\"hide\">隐藏</button>\n        <button id=\"close\" ${buttonStyle} data-i18n=\"close\">关闭</button>\n    </div>`;\n    CatCatch.style = `\n        position: fixed;\n        z-index: 999999;\n        top: 10%;\n        left: 80%;\n        background: rgb(255 255 255 / 85%);\n        border: solid 1px #c7c7c7;\n        border-radius: 4px;\n        color: rgb(26, 115, 232);\n        padding: 5px 5px 5px 5px;\n        font-size: 12px;\n        font-family: \"Microsoft YaHei\", \"Helvetica\", \"Arial\", sans-serif;\n        user-select: none;\n        display: flex;\n        align-items: flex-start;\n        justify-content: space-evenly;\n        flex-direction: column;\n        line-height: 20px;`;\n\n    // 创建 Shadow DOM 放入CatCatch\n    const divShadow = document.createElement('div');\n    const shadowRoot = divShadow.attachShadow({ mode: 'closed' });\n    shadowRoot.appendChild(CatCatch);\n    // 页面插入Shadow DOM\n    document.getElementsByTagName('html')[0].appendChild(divShadow);\n\n    // 提示\n    const $tips = CatCatch.querySelector(\"#tips\");\n    const tips = (text) => {\n        $tips.innerHTML = text;\n    }\n\n    // 开始 结束 按钮切换\n    const $start = CatCatch.querySelector(\"#start\");\n    const $stop = CatCatch.querySelector(\"#stop\");\n    const buttonState = (state = true) => {\n        $start.style.display = state ? 'inline' : 'none';\n        $stop.style.display = state ? 'none' : 'inline';\n    }\n    $start.style.display = 'inline';\n    $stop.style.display = 'none';\n\n    // 关闭\n    CatCatch.querySelector(\"#close\").addEventListener('click', function (event) {\n        recorder?.state && recorder.stop();\n        CatCatch.style.display = \"none\";\n        window.postMessage({ action: \"catCatchToBackground\", Message: \"script\", script: \"webrtc.js\", refresh: true });\n    });\n\n    // 隐藏\n    CatCatch.querySelector(\"#hide\").addEventListener('click', function (event) {\n        CatCatch.style.display = \"none\";\n    });\n\n    const tracks = { video: [], audio: [] };\n    const $tracks = { video: CatCatch.querySelector('#videoTrack'), audio: CatCatch.querySelector('#audioTrack') };\n\n    /* 核心变量 */\n    let recorder = null;    // 录制器\n    let autoSave1Timer = null;    // 1小时保存一次\n\n    // #region 编码选择\n    let option = { mimeType: 'video/webm;codecs=vp9,opus' };\n    function getSupportedMimeTypes(media, types, codecs) {\n        const supported = [];\n        types.forEach((type) => {\n            const mimeType = `${media}/${type}`;\n            codecs.forEach((codec) => [`${mimeType};codecs=${codec}`].forEach(variation => {\n                if (MediaRecorder.isTypeSupported(variation)) {\n                    supported.push(variation);\n                }\n            }));\n            if (MediaRecorder.isTypeSupported(mimeType)) {\n                supported.push(mimeType);\n            }\n        });\n        return supported;\n    };\n    const $mimeTypeList = CatCatch.querySelector(\"#mimeTypeList\");\n    const videoTypes = [\"webm\", \"ogg\", \"mp4\", \"x-matroska\"];\n    const codecs = [\"should-not-be-supported\", \"vp9\", \"vp8\", \"avc1\", \"av1\", \"h265\", \"h.265\", \"h264\", \"h.264\", \"opus\", \"pcm\", \"aac\", \"mpeg\", \"mp4a\"];\n    const supportedVideos = getSupportedMimeTypes(\"video\", videoTypes, codecs);\n    supportedVideos.forEach(function (type) {\n        $mimeTypeList.options.add(new Option(type, type));\n    });\n    option.mimeType = supportedVideos[0];\n    $mimeTypeList.addEventListener('change', function (event) {\n        if (recorder && recorder.state && recorder.state === 'recording') {\n            tips(i18n(\"recordingChangeEncoding\", \"录制中不能更改编码\"));\n            return;\n        }\n        if (MediaRecorder.isTypeSupported(event.target.value)) {\n            option.mimeType = event.target.value;\n            tips(`${i18n(\"recordEncoding\", \"录制编码\")}:` + event.target.value);\n        } else {\n            tips(i18n(\"formatNotSupported\", \"不支持此格式\"));\n        }\n    });\n    // #endregion 编码选择\n\n    // 录制\n    $time = CatCatch.querySelector(\"#time\");\n    CatCatch.querySelector(\"#start\").addEventListener('click', function () {\n        if (!tracks.video.length && !tracks.audio.length) {\n            tips(i18n(\"streamEmpty\", \"媒体流为空\"));\n            return;\n        }\n        let recorderTime = 0;\n        let recorderTimeer = undefined;\n        let chunks = [];\n\n        // 音频 视频 选择\n        const videoTrack = +CatCatch.querySelector(\"#videoTrack\").value;\n        const audioTrack = +CatCatch.querySelector(\"#audioTrack\").value;\n        const streamTrack = [];\n        if (videoTrack !== -1 && tracks.video[videoTrack]) {\n            streamTrack.push(tracks.video[videoTrack]);\n        }\n        if (audioTrack !== -1 && tracks.audio[audioTrack]) {\n            streamTrack.push(tracks.audio[audioTrack]);\n        }\n\n        // 码率\n        option.audioBitsPerSecond = +CatCatch.querySelector(\"#audioBits\").value;\n        option.videoBitsPerSecond = +CatCatch.querySelector(\"#videoBits\").value;\n\n        const mediaStream = new MediaStream(streamTrack);\n        recorder = new MediaRecorder(mediaStream, option);\n        recorder.ondataavailable = event => {\n            chunks.push(event.data)\n        };\n        recorder.onstop = () => {\n            recorderTime = 0;\n            clearInterval(recorderTimeer);\n            clearInterval(autoSave1Timer);\n            $time.innerHTML = \"\";\n            tips(i18n(\"stopRecording\", \"已停止录制!\"));\n            download(chunks);\n            buttonState();\n        }\n        recorder.onstart = () => {\n            chunks = [];\n            tips(i18n(\"recording\", \"视频录制中\"));\n            $time.innerHTML = \"00:00\";\n            recorderTimeer = setInterval(function () {\n                recorderTime++;\n                $time.innerHTML = secToTime(recorderTime);\n            }, 1000);\n            buttonState(false);\n        }\n        recorder.onerror = (msg) => {\n            console.error(msg);\n        }\n        recorder.start(60000);\n    });\n    // 停止录制\n    CatCatch.querySelector(\"#stop\").addEventListener('click', function () {\n        if (recorder) {\n            recorder.stop();\n            recorder = undefined;\n        }\n    });\n    // 保存\n    CatCatch.querySelector(\"#save\").addEventListener('click', function () {\n        if (recorder) {\n            recorder.stop();\n            recorder.start();\n        }\n    });\n    // 每1小时 保存一次\n    CatCatch.querySelector(\"#autoSave1\").addEventListener('click', function () {\n        clearInterval(autoSave1Timer);\n        if (CatCatch.querySelector(\"#autoSave1\").checked) {\n            autoSave1Timer = setInterval(function () {\n                if (recorder) {\n                    recorder.stop();\n                    recorder.start();\n                }\n            }, 3600000);\n        }\n    });\n\n    // 获取webRTC流\n    window.RTCPeerConnection = new Proxy(window.RTCPeerConnection, {\n        construct(target, args) {\n            const pc = new target(...args);\n            pc.addEventListener('track', (event) => {\n                const track = event.track;\n                if (track.kind === 'video' || track.kind === 'audio') {\n                    tips(`${track.kind} ${i18n(\"streamAdded\", \"流已添加\")}`);\n                    $tracks[track.kind].appendChild(new Option(track.label, tracks[track.kind].length));\n                    $tracks[track.kind].value = tracks[track.kind].length;\n                    tracks[track.kind].push(track);\n                    if (tracks.video.length && tracks.audio.length) {\n                        tips(i18n(\"videoAndAudio\", \"已包含音频和视频流\"));\n                    }\n                }\n            });\n            pc.addEventListener('iceconnectionstatechange', (event) => {\n                if (pc.iceConnectionState === 'disconnected' && recorder?.state === 'recording') {\n                    recorder.stop();\n                    tips(i18n(\"stopRecording\", \"连接已断开，录制已停止\"));\n                }\n            });\n            return pc;\n        }\n    });\n\n    // #region 移动逻辑\n    let x, y;\n    const move = (event) => {\n        CatCatch.style.left = event.pageX - x + 'px';\n        CatCatch.style.top = event.pageY - y + 'px';\n    }\n    CatCatch.addEventListener('mousedown', function (event) {\n        x = event.pageX - CatCatch.offsetLeft;\n        y = event.pageY - CatCatch.offsetTop;\n        document.addEventListener('mousemove', move);\n        document.addEventListener('mouseup', function () {\n            document.removeEventListener('mousemove', move);\n        });\n    });\n    // #endregion 移动逻辑\n\n    function download(chunks) {\n        const blob = new Blob(chunks, { type: option.mimeType });\n        const url = URL.createObjectURL(blob);\n        const a = document.createElement('a');\n        a.style.display = 'none';\n        a.href = url;\n        a.download = 'recorded-video.mp4';\n        document.body.appendChild(a);\n        a.click();\n        window.URL.revokeObjectURL(url);\n        document.body.removeChild(a);\n    }\n\n    // 秒转换成时间\n    function secToTime(sec) {\n        let hour = (sec / 3600) | 0;\n        let min = ((sec % 3600) / 60) | 0;\n        sec = (sec % 60) | 0;\n        let time = hour > 0 ? hour + \":\" : \"\";\n        time += min.toString().padStart(2, '0') + \":\";\n        time += sec.toString().padStart(2, '0');\n        return time;\n    }\n\n    // 防止网页意外关闭跳转\n    window.addEventListener('beforeunload', function (e) {\n        recorder && recorder.stop();\n        return true;\n    });\n\n    // i18n\n    if (window.CatCatchI18n) {\n        CatCatch.querySelectorAll('[data-i18n]').forEach(function (element) {\n            element.innerHTML = window.CatCatchI18n[element.dataset.i18n][language];\n        });\n        CatCatch.querySelectorAll('[data-i18n-outer]').forEach(function (element) {\n            element.outerHTML = window.CatCatchI18n[element.dataset.i18nOuter][language];\n        });\n    }\n    function i18n(key, original = \"\") {\n        if (!window.CatCatchI18n) { return original };\n        return window.CatCatchI18n[key][language];\n    }\n})();\n"
  },
  {
    "path": "css/install.css",
    "content": "* {\n  margin: 0;\n  padding: 0;\n  box-sizing: border-box;\n  font-family: \"Segoe UI\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif;\n}\n\n:root {\n  --primary: #4361ee;\n  --primary-light: #4895ef;\n  --secondary: #06d6a0;\n  --danger: #ef476f;\n  --light: #f8f9fa;\n  --dark: #212529;\n  --gray: #6c757d;\n  --border-radius: 16px;\n  --shadow: 0 10px 30px rgba(0, 0, 0, 0.1);\n  --transition: all 0.3s ease;\n\n  /* 新增深色模式变量 */\n  --bg-gradient-start: #f0f4ff;\n  --bg-gradient-end: #e6f7ff;\n  --card-bg: white;\n  --card-header-bg: linear-gradient(to right, #f8f9fa, #e9ecef);\n  --text-color: #212529;\n  --subtitle-color: #6c757d;\n  --content-bg: #f8f9fa;\n  --agreement-bg: #f8f9fa;\n  --agreement-border: rgba(0, 0, 0, 0.1);\n  --agreement-color: #6c757d;\n}\n\nbody {\n  background: linear-gradient(\n    135deg,\n    var(--bg-gradient-start) 0%,\n    var(--bg-gradient-end) 100%\n  );\n  min-height: 100vh;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  padding: 20px;\n  color: var(--text-color);\n  line-height: 1.6;\n}\n\n.container {\n  width: 100%;\n  max-width: 800px;\n  position: relative;\n}\n\n/* 语言切换按钮 */\n.lang-switch {\n  position: absolute;\n  top: 20px;\n  right: 20px;\n  z-index: 10;\n  background: var(--card-bg);\n  border-radius: 50px;\n  padding: 10px 18px;\n  display: flex;\n  align-items: center;\n  gap: 10px;\n  box-shadow: var(--shadow);\n  cursor: pointer;\n  transition: var(--transition);\n  font-weight: 600;\n  font-size: 1rem;\n}\n\n.lang-switch:hover {\n  transform: translateY(-3px);\n  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);\n  background: var(--primary);\n  color: white;\n}\n\n.lang-emoji {\n  font-size: 1.3rem;\n}\n\n/* 头部样式 */\n.header {\n  text-align: center;\n  margin-bottom: 30px;\n  position: relative;\n  padding-top: 20px;\n}\n\n.logo-container {\n  display: flex;\n  justify-content: center;\n  margin-bottom: 20px;\n}\n\n.logo {\n  width: 120px;\n  height: 120px;\n  background: linear-gradient(135deg, var(--primary), var(--primary-light));\n  border-radius: 50%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  box-shadow: var(--shadow);\n  animation: float 3s ease-in-out infinite;\n  font-size: 3.5rem;\n}\n\nh1 {\n  font-size: 2.5rem;\n  font-weight: 800;\n  margin-bottom: 10px;\n  background: linear-gradient(to right, var(--primary), var(--primary-light));\n  -webkit-background-clip: text;\n  -webkit-text-fill-color: transparent;\n}\n\n.subtitle {\n  font-size: 1.2rem;\n  color: var(--subtitle-color);\n  font-weight: 500;\n  max-width: 600px;\n  margin: 0 auto;\n}\n\n/* 卡片样式 */\n.card {\n  background: var(--card-bg);\n  border-radius: var(--border-radius);\n  box-shadow: var(--shadow);\n  overflow: hidden;\n  margin-bottom: 30px;\n  transform: translateY(0);\n  transition: var(--transition);\n}\n\n.card:hover {\n  transform: translateY(-5px);\n  box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15);\n}\n\n.card-header {\n  padding: 20px;\n  background: var(--card-header-bg);\n  border-bottom: 1px solid rgba(0, 0, 0, 0.05);\n}\n\n.card-title {\n  font-size: 1.5rem;\n  font-weight: 600;\n  color: var(--primary);\n  display: flex;\n  align-items: center;\n  gap: 10px;\n}\n\n.card-title .emoji {\n  font-size: 1.5rem;\n}\n\n.card-body {\n  padding: 25px;\n}\n\n.policy-section {\n  margin-top: 30px;\n}\n\n.section-title {\n  font-size: 1.25rem;\n  font-weight: 600;\n  margin-bottom: 15px;\n  color: var(--primary);\n  display: flex;\n  align-items: center;\n  gap: 10px;\n}\n\n.section-title .emoji {\n  font-size: 1.3rem;\n}\n\n.content-box {\n  background-color: var(--content-bg);\n  border-radius: 12px;\n  padding: 20px;\n  margin-top: 10px;\n  border: 1px solid rgba(0, 0, 0, 0.05);\n  line-height: 1.7;\n  display: flex;\n  justify-content: center;\n}\n\n.content-box a {\n  color: var(--primary);\n  font-weight: 600;\n  text-decoration: none;\n  transition: var(--transition);\n  position: relative;\n  font-size: 1.1rem;\n  padding: 10px 20px;\n  border-radius: 30px;\n  background-color: rgba(67, 97, 238, 0.1);\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n\n.content-box a:hover {\n  background-color: rgba(67, 97, 238, 0.2);\n  transform: translateY(-2px);\n  box-shadow: 0 4px 10px rgba(67, 97, 238, 0.2);\n}\n\n.content-box a .emoji {\n  font-size: 1.2rem;\n}\n\n.content-box p {\n  font-size: 1rem;\n}\n\n.agreement {\n  text-align: center;\n  padding: 20px;\n  background-color: var(--agreement-bg);\n  border-radius: var(--border-radius);\n  margin: 25px 0;\n  font-style: italic;\n  color: var(--agreement-color);\n  border: 1px dashed var(--agreement-border);\n  display: flex;\n  flex-direction: column;\n  gap: 10px;\n}\n\n.agreement .emoji {\n  font-size: 1.5rem;\n}\n\n.buttons {\n  display: flex;\n  gap: 20px;\n  margin-top: 30px;\n}\n\n.btn {\n  flex: 1;\n  padding: 16px 20px;\n  border: none;\n  border-radius: 50px;\n  font-size: 1.1rem;\n  font-weight: 600;\n  cursor: pointer;\n  transition: var(--transition);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 10px;\n}\n\n.btn-primary {\n  background: linear-gradient(to right, var(--primary), var(--primary-light));\n  color: white;\n  box-shadow: 0 4px 15px rgba(67, 97, 238, 0.3);\n}\n\n.btn-primary:hover {\n  transform: translateY(-3px);\n  box-shadow: 0 8px 20px rgba(67, 97, 238, 0.4);\n  background: linear-gradient(to right, var(--primary-light), var(--primary));\n}\n\n.btn-outline {\n  background: transparent;\n  color: var(--danger);\n  border: 2px solid var(--danger);\n}\n\n.btn-outline:hover {\n  background-color: rgba(239, 71, 111, 0.05);\n  transform: translateY(-3px);\n}\n\n/* 多语言内容控制 */\n.lang-zh,\n.lang-en {\n  display: none;\n}\n\n.lang-zh.active,\n.lang-en.active {\n  display: block;\n}\n\n/* 动画效果 */\n@keyframes float {\n  0% {\n    transform: translateY(0px);\n  }\n\n  50% {\n    transform: translateY(-10px);\n  }\n\n  100% {\n    transform: translateY(0px);\n  }\n}\n\n.fade-in {\n  animation: fadeIn 0.5s ease-in-out;\n}\n\n@keyframes fadeIn {\n  from {\n    opacity: 0;\n    transform: translateY(10px);\n  }\n\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n/* 响应式设计 */\n@media (max-width: 768px) {\n  h1 {\n    font-size: 2rem;\n  }\n\n  .buttons {\n    flex-direction: column;\n    gap: 12px;\n  }\n\n  .card-body {\n    padding: 20px;\n  }\n\n  .subtitle {\n    font-size: 1.1rem;\n  }\n}\n\n@media (max-width: 480px) {\n  h1 {\n    font-size: 1.8rem;\n  }\n\n  .subtitle {\n    font-size: 1rem;\n  }\n\n  .logo {\n    width: 90px;\n    height: 90px;\n    font-size: 2.8rem;\n  }\n\n  .lang-switch {\n    top: 10px;\n    right: 10px;\n    padding: 8px 14px;\n    font-size: 0.9rem;\n  }\n\n  .content-box a {\n    font-size: 0.95rem;\n    padding: 8px 15px;\n  }\n}\n\n/* 深色模式支持 */\n@media (prefers-color-scheme: dark) {\n  :root {\n    --bg-gradient-start: #0f172a;\n    --bg-gradient-end: #1e293b;\n    --card-bg: #1e293b;\n    --card-header-bg: linear-gradient(to right, #334155, #1e293b);\n    --text-color: #e2e8f0;\n    --subtitle-color: #94a3b8;\n    --content-bg: #334155;\n    --agreement-bg: #334155;\n    --agreement-border: rgba(255, 255, 255, 0.1);\n    --agreement-color: #cbd5e1;\n    --shadow: 0 10px 30px rgba(0, 0, 0, 0.3);\n  }\n\n  .lang-switch {\n    background: #334155;\n    color: #e2e8f0;\n  }\n\n  .content-box a {\n    background-color: rgba(67, 97, 238, 0.15);\n    color: #93c5fd;\n  }\n\n  .content-box a:hover {\n    background-color: rgba(67, 97, 238, 0.25);\n    box-shadow: 0 4px 10px rgba(67, 97, 238, 0.2);\n  }\n\n  .btn-outline:hover {\n    background-color: rgba(239, 71, 111, 0.15);\n  }\n}\n"
  },
  {
    "path": "css/mobile.css",
    "content": ".popupBody {\n  width: 100%;\n}\n\n.wrapper.options {\n  margin-right: 10px;\n}\n\n.m3u8_wrapper #mergeTs {\n  font-size: 2rem;\n}\n\n.newDownload {\n  width: 100%;\n  padding: 0 2rem;\n}\n"
  },
  {
    "path": "css/options.css",
    "content": "body {\n  background: var(--background-color);\n  font-size: 13px;\n  font-family: \"Microsoft YaHei\", \"Helvetica\", \"Arial\", sans-serif;\n  margin: 0;\n}\n\n.wrapper {\n  margin: 0 auto;\n  width: 45rem;\n}\n\n.error {\n  color: var(--text-error-color);\n}\n\nh1 {\n  font-size: 1.125em;\n  font-weight: normal;\n  margin: 0;\n}\n\nh2 {\n  font-size: 1.125em;\n  font-weight: normal;\n  margin: 0;\n}\n\np {\n  margin: auto;\n}\n\n.optionBox {\n  background: var(--optionBox-color);\n  border-radius: 4px;\n  box-shadow: 0 1px 2px 0 rgb(60 64 67 / 30%), 0 1px 3px 1px rgb(60 64 67 / 15%);\n  padding: 0.75em 1.25em;\n  margin-top: 5px;\n}\n\ntable {\n  width: 100%;\n  text-align: center;\n}\n\ninput,\ntextarea {\n  padding: 5px 5px;\n}\n\ninput.ext {\n  width: 100px;\n  text-align: center;\n}\n\ninput.type {\n  width: 200px;\n  text-align: center;\n}\n\ninput.size {\n  width: 100px;\n  text-align: center;\n}\n\ninput.regexType {\n  width: 20px;\n  text-align: center;\n}\n\ninput.regexExt {\n  width: 35px;\n  text-align: center;\n}\n\ninput.regex {\n  width: 320px;\n  text-align: center;\n}\n\n/* input#OtherAutoClear {\n  margin-left: 250px;\n  width: 45px;\n} */\n/* 滑动开关 组件 */\n.switch {\n  height: 22px;\n  width: 50px;\n  margin: auto;\n}\n\n.switch .switchRound {\n  position: relative;\n  display: block;\n  width: 100%;\n  height: 100%;\n  background-color: var(--switch-off-color);\n  transition: all 0.2s ease-in-out;\n}\n\n.switch .switchRoundBtn {\n  display: block;\n  position: absolute;\n  top: 2px;\n  left: 3px;\n  bottom: 3px;\n  width: 18px;\n  height: 18px;\n  background-color: var(--switch-round-color);\n  transition: all 0.2s ease-in-out;\n}\n\n.switch .switchInput {\n  display: none;\n}\n\n.switch .switchInput:checked + .switchRound {\n  background-color: var(--switch-on-color);\n}\n\n.switch .switchInput:checked + .switchRound > .switchRoundBtn {\n  left: 29px;\n}\n\n.switch .switchRadius {\n  border-radius: 50px;\n}\n\n/* 滑动开关 组件 END */\n.list {\n  padding-left: 10px;\n  padding-top: 5px;\n}\n\n.item {\n  align-items: center;\n  display: flex;\n  min-height: 30px;\n  border-bottom: solid 1px rgba(0, 0, 0, 0.06);\n  flex-wrap: wrap;\n  align-items: flex-end;\n  align-content: space-around;\n}\n\n.item .switch {\n  margin-right: 50px;\n}\n\n.item .switchSelect {\n  margin-right: 85px;\n}\n\n.optionsTitle {\n  margin-top: 20px;\n}\n\n.RemoveButton {\n  fill: var(--text-color);\n  height: 20px;\n  cursor: pointer;\n}\n\nbutton,\n.button,\n.button2 {\n  padding: calc(0.5em - 1px) 1em;\n  margin: 5px 5px 5px 5px;\n  /* font-size: 13px; */\n}\n\n.flex-end {\n  display: flex;\n  justify-content: flex-end;\n}\n\n.explain {\n  color: #6c6c6c;\n}\n\n#typeList,\n#extList {\n  margin-top: 10px;\n}\n\n.loose .item {\n  margin-bottom: 5px;\n  min-height: 35px;\n}\n\n#m3u8_url,\n#mpd_url,\n.test_url {\n  overflow: hidden;\n  display: block;\n  text-overflow: ellipsis;\n  word-break: break-all;\n  color: var(--text2-color);\n}\n\n.block {\n  border-bottom: solid 1px rgba(0, 0, 0, 0.06);\n  padding-bottom: 5px;\n  margin-bottom: 5px;\n}\n\n.m3u8_wrapper .block {\n  border-bottom: 0px;\n}\n\n.wrapper1024 {\n  margin: 0 auto;\n  width: 1024px;\n}\n\n.wrapper1080 {\n  margin: 0 auto;\n  width: 1080px;\n}\n\ntextarea {\n  font-size: 12px;\n  font-family: \"Microsoft YaHei\", \"Helvetica\", \"Arial\", sans-serif;\n}\n\n#textarea {\n  text-align: center;\n}\n\n.m3u8_wrapper video {\n  max-height: 80vh;\n  max-width: 100%;\n}\n\n#media_file {\n  word-break: break-all;\n}\n\n#media_file,\n#jsonText,\n#m3u8Text {\n  height: 55vh;\n}\n\n/* #media_file {\n  font-size: 12px;\n  font-family: \"Microsoft YaHei\", \"Helvetica\", \"Arial\", sans-serif;\n  height: 700px;\n  overflow-y: auto;\n  border: solid 1.5px rgb(0 0 0 / 50%);\n  word-break: break-all;\n} */\n#formatStr {\n  width: 145px;\n}\n\n#tips input {\n  color: var(--text2-color);\n}\n\n.keyUrl {\n  width: 1034px;\n}\n\n.fullInput {\n  /* width: 975px; */\n  width: 100%;\n  margin: 5px 0 5px 0;\n}\n\n.select {\n  appearance: none;\n  background: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIxMiIgZmlsbD0iIzVGNjM2OCI+PHBhdGggZD0iTTAgMGgyNEwxMiAxMnoiLz48L3N2Zz4=)\n    calc(100% - 8px) center no-repeat;\n  /* background-color: rgb(241, 243, 244); */\n  background-color: var(--background-color-two);\n  background-size: 10px;\n  border: none;\n  border-radius: 4px;\n  cursor: pointer;\n  padding: 5px 10px;\n}\n\n.select {\n  width: 8rem;\n}\n\n.m3u8Key {\n  width: 300px;\n}\n\n#PlayerTemplate {\n  width: 200px;\n}\n\n#errorTsList p {\n  color: red;\n  word-break: break-all;\n}\n\n.progress-bar {\n  height: 15px;\n  background-color: rgb(189, 193, 198);\n  border-radius: 3px;\n  margin: 3px;\n  margin-bottom: 10px;\n}\n\n.progress {\n  width: 0px;\n  height: 100%;\n  background-color: var(--text2-color);\n  border-radius: 3px;\n}\n\n#fileSize,\n#fileDuration {\n  margin-left: 20px;\n}\n\n.not-allowed {\n  cursor: not-allowed;\n  background-color: #ccc;\n  color: #fff;\n}\n\n.not-allowed:hover {\n  background: #ccc;\n}\n\n.not-allowed:active {\n  background: #ccc;\n}\n\n#showM3u8Help {\n  margin-left: 10px;\n  margin-top: 1px;\n  margin-right: 0px;\n  padding: 2px;\n}\n\n.m3u8checkbox {\n  display: flex;\n  cursor: pointer;\n  flex-direction: column;\n  user-select: none;\n  margin: 0 5px 0 5px;\n}\n\n.merge {\n  display: flex;\n  justify-content: flex-start;\n  margin-top: 5px;\n  align-items: center;\n}\n\n.customKey input {\n  margin-right: 5px;\n}\n\n/* .wrapper .button {\n  margin-top: 5px;\n} */\n.rangeDown {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  margin-right: 10px;\n}\n\n.rangeDown .merge {\n  margin-top: 0;\n}\n\n#rangeStart,\n#rangeEnd {\n  width: 55px;\n  /* text-align:center;\n  vertical-align:middle; */\n  margin-left: 2px;\n  margin-right: 2px;\n  padding-top: 3px;\n  padding-bottom: 3px;\n}\n\n#loading a {\n  word-break: break-all;\n}\n\n#next_m3u8 a {\n  word-break: break-all;\n}\n\n.key {\n  align-items: flex-end;\n}\n\n.key div {\n  display: flex;\n  flex-direction: column;\n  margin-right: 10px;\n}\n\n.key input {\n  width: 265px;\n}\n\n.method input {\n  width: 100px;\n}\n\n.offset {\n  width: 256px;\n}\n\n.videoInfo div {\n  margin-right: 5px;\n}\n\n.flex {\n  display: flex;\n}\n\n.m3u8dlArg {\n  margin-top: 10px;\n  height: 100px;\n  word-break: break-all;\n  width: 100%;\n}\n\n.m3u8DL {\n  margin-right: 70px !important;\n}\n\n/* .m3u8DL #m3u8dl{\n  width: 8rem;\n} */\n.break-all {\n  word-break: break-all;\n}\n\n/* MPD*/\n.dash .select {\n  padding-right: 20px;\n  margin-bottom: 10px;\n}\n\n/* JSON格式化 */\n.json-document {\n  margin-top: 0px;\n}\n\nul.json-dict,\nol.json-array {\n  list-style-type: none;\n  margin: 0 0 0 1px;\n  border-left: 1px dotted #ccc;\n  padding-left: 2em;\n}\n\n.json-string {\n  color: #0b7500;\n  word-break: break-all;\n  white-space: break-spaces;\n}\n\n.json-literal {\n  color: #1a01cc;\n  font-weight: bold;\n}\n\na.json-toggle {\n  position: relative;\n  color: inherit;\n  text-decoration: none;\n}\n\na.json-toggle:focus {\n  outline: none;\n}\n\na.json-toggle:before {\n  font-size: 1.1em;\n  color: #c0c0c0;\n  content: \"\\25BC\";\n  position: absolute;\n  display: inline-block;\n  width: 1em;\n  text-align: center;\n  line-height: 1em;\n  left: -1.2em;\n}\n\na.json-toggle:hover:before {\n  color: #aaa;\n}\n\na.json-toggle.collapsed:before {\n  transform: rotate(-90deg);\n}\n\na.json-placeholder {\n  color: #aaa;\n  padding: 0 1em;\n  text-decoration: none;\n}\n\na.json-placeholder:hover {\n  text-decoration: underline;\n}\n\n#downList a {\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  display: block;\n  color: var(--text2-color);\n}\n\n#downList {\n  overflow: scroll;\n  height: 60vh;\n  text-align: left;\n  display: none;\n  width: 100%;\n  border: solid 1px var(--text-color);\n}\n\n.width3rem {\n  width: 3rem;\n}\n\n.popupAttr {\n  margin-left: 0.5rem;\n}\n\n.progress-container {\n  display: flex;\n  align-items: center;\n  gap: 10px;\n}\n\n.progress-wrapper {\n  flex: 1;\n}\n\n.newDownload .downItem {\n  margin-bottom: 1rem;\n}\n\n.newDownload .downItem .progress-bar {\n  margin-bottom: 0;\n  height: 20px;\n}\n\n.newDownload .downItem button {\n  margin: 0;\n}\n\n.newDownload .downItem .progress {\n  color: var(--background-color-two);\n  text-align: center;\n  transition: width 0.2s;\n}\n\n/** 导航条 **/\n.sidebar {\n  position: fixed;\n  top: 0;\n  left: 0;\n  width: 10rem;\n  height: 100%;\n  padding: 10px;\n  background-color: var(--background-color-two);\n  box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);\n  overflow-y: auto;\n  text-align: center;\n  margin-right: 0;\n}\n.sidebar ul {\n  list-style-type: none;\n  padding: 0;\n}\n.sidebar li {\n  margin: 10px 0;\n}\n.sidebar a {\n  text-decoration: none;\n  color: var(--text-color);\n  display: block;\n  padding: 5px;\n  border-radius: 4px;\n}\n.sidebar a:hover {\n  background-color: var(--button-hover-color);\n}\n\n.item .send2localType {\n  margin-right: 196px;\n}\n.item .send2localType select {\n  width: 15rem;\n}\n"
  },
  {
    "path": "css/popup.css",
    "content": "a {\n  text-decoration: none;\n  word-break: break-all;\n}\na:hover {\n  text-decoration: underline;\n}\nbody {\n  font-family: arial, sans-serif;\n  font-size: 0.8rem;\n  width: 40rem;\n  overflow-x: hidden;\n  background: var(--background-color);\n  margin: 0;\n}\n.fixFirefoxRight {\n  margin-right: 5px;\n}\n.panel {\n  border: 1px solid #ddd0;\n  margin-bottom: 1px;\n}\n.panel-heading {\n  padding: 5px 5px 5px 5px;\n  background-color: var(--background-color-two);\n  cursor: pointer;\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n}\n.panel-heading .name {\n  flex: auto;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n  overflow: hidden;\n  margin-right: 0.2rem;\n}\n.panel .url,\n.panel .confirm {\n  padding: 5px;\n}\n.icon,\n.favicon {\n  transition: all 0.1s;\n  width: 1.5rem;\n  height: 1.5rem;\n  cursor: pointer;\n}\n.faviconFlag {\n  display: none;\n}\n.icon:hover {\n  transform: scale(1.1);\n}\n.icon:active {\n  transform: scale(0.9);\n}\n.icon.mqtt-sending {\n  opacity: 0.5;\n  cursor: not-allowed;\n}\n.icon.mqtt-sending:hover {\n  transform: none;\n}\n.panel-heading .icon {\n  padding-left: 2px;\n}\n.favicon {\n  padding-right: 2px;\n}\n.panel-heading .size {\n  float: right;\n  font-weight: bold;\n}\n#Tips,\n#TipsFixed {\n  left: 0;\n  right: 0;\n  text-align: center;\n  z-index: 9999;\n  pointer-events: none;\n  color: var(--text2-color);\n  font-weight: bold;\n  border: 1px solid #cdcdcd12;\n  border-radius: 2px;\n  background: var(--background-color-two);\n  padding: 0 10px;\n  margin-bottom: 1px;\n}\n#TipsFixed {\n  position: fixed;\n  display: none;\n}\n#preview {\n  max-height: 300px;\n  max-width: 100%;\n  text-align: center;\n}\nbutton,\n.button2 {\n  padding: 3px 3px 3px 3px;\n  /* font-size: 0.9rem; */\n}\n.Tabs {\n  display: flex;\n}\n.TabButton {\n  text-align: center;\n  border: solid 1px #c7c7c700;\n  color: var(--text2-color);\n  border-radius: 5px 5px 0 0;\n  cursor: pointer;\n  width: 50%;\n  /* display: flex; */\n  padding: 3px;\n  margin: 1px 2px 0 2px;\n  flex-direction: row;\n  align-items: baseline;\n  justify-content: center;\n  user-select: none;\n}\n.flex {\n  display: flex;\n}\n.TabButton.Active {\n  background-color: var(--background-color-two);\n  border-bottom-color: transparent;\n  font-weight: bold;\n}\n.TabButton.Active div {\n  font-weight: bold;\n}\n.DownCheck {\n  margin: 0 2px 0 0;\n  width: 1.2rem;\n  height: 1.2rem;\n  flex: 0 0 auto;\n}\n.TabShow {\n  display: block !important;\n}\n#down,\n.more {\n  display: flex;\n  flex-wrap: wrap;\n  position: fixed;\n  width: 100%;\n  z-index: 999;\n  background-color: var(--background-color-opacity);\n}\n#down {\n  bottom: 0;\n  justify-content: space-evenly;\n}\n.more {\n  display: none;\n  bottom: 26px;\n  justify-content: flex-start;\n  padding-bottom: 2px;\n  padding-top: 2px;\n  z-index: 9999;\n}\n.more button {\n  margin-left: 0.1rem;\n  font-size: 12px;\n}\n#filter {\n  flex-wrap: wrap;\n}\n#filter #regular button {\n  margin-left: 0px;\n}\n#filter #regular input {\n  width: 98%;\n}\n#filter .regular {\n  margin-left: 5px;\n}\n#filter #ext {\n  display: flex;\n  color: var(--text-color);\n}\n#filter div {\n  margin-left: 5px;\n}\n.flexFilter {\n  display: flex;\n  flex-wrap: wrap;\n  align-items: center;\n}\n.container {\n  margin-bottom: 30px;\n}\n#screenshots {\n  max-width: 100%;\n  max-height: 260px;\n  cursor: pointer;\n  margin: auto;\n}\n.flex-end {\n  justify-content: flex-end;\n}\n#otherOptions {\n  margin: 5px;\n}\n#PlayControl {\n  display: flex;\n  align-items: center;\n  flex-wrap: wrap;\n  justify-content: space-evenly;\n}\n#PlayControl .button2,\n#PlayControl .button {\n  margin-left: 2px;\n}\n#PlayControl #playbackRate {\n  width: 3em;\n  height: 20px;\n}\n#otherOptions select {\n  margin-top: 2px;\n  margin-bottom: 2px;\n  width: 100%;\n}\n#PlayControl .loop {\n  margin: 0 5px 0 5px;\n}\nlabel {\n  cursor: pointer;\n  user-select: none;\n}\n#PlayControl .volume {\n  width: 100px;\n}\n.flexColumn {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n}\n.flexRow {\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n}\n.nowrap {\n  word-break: keep-all;\n}\n.otherScript .button2,\n.otherFeat .button2 {\n  width: 100%;\n  margin-right: 10px;\n  text-align: center;\n}\n.otherTips {\n  text-align: center;\n  color: var(--text2-color);\n  font-weight: bold;\n}\n.moreButton {\n  display: flex;\n}\n.moreButton div {\n  margin-right: 3px;\n}\n.panel .confirm {\n  text-align: center;\n}\n"
  },
  {
    "path": "css/preview.css",
    "content": "/* 基础样式 */\nbody {\n  margin: 0;\n  padding: 0;\n  height: 100vh;\n  user-select: none;\n}\n\n/* .container {\n  padding: 10px;\n  margin: 0 auto;\n} */\n\n/* 筛选区域 */\n.filters {\n  display: grid;\n  gap: 5px;\n  /* margin-bottom: 10px; */\n  background: var(--background-color-two);\n  padding: 10px;\n  border-radius: 8px;\n  /* position: sticky; */\n  /* top: 0; */\n  /* z-index: 2; */\n}\n\n.filter-row {\n  display: flex;\n  align-items: center;\n  gap: 10px;\n  flex-wrap: wrap;\n}\n\n.sort-options {\n  display: flex;\n  gap: 15px;\n  align-items: center;\n}\n\n.sort-group,\n.sort-order {\n  display: flex;\n  gap: 8px;\n}\n\n#regular {\n  width: 512px;\n}\n\ninput[type=\"radio\"] {\n  vertical-align: bottom;\n}\ninput[type=\"checkbox\"] {\n  vertical-align: middle;\n}\n\n/* 文件列表 */\n.file-grid {\n  display: grid;\n  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));\n  gap: 10px;\n  padding: 10px;\n}\n\n.file-item {\n  display: flex;\n  flex-direction: column;\n  min-height: 150px;\n  padding: 8px;\n  border: 3px solid transparent;\n  border-radius: 8px;\n  cursor: pointer;\n  box-shadow: 0 0 3px var(--button2-color);\n  max-height: 233px;\n  transition: all 0.2s;\n}\n.file-item:hover {\n  box-shadow: 0 0 10px var(--button2-color);\n}\n\n.file-item.selected {\n  border-color: var(--button2-color);\n  background-color: var(--button-hover-color);\n  /* box-shadow: 0 0 8px var(--button2-color); */\n}\n\n.file-name {\n  font-weight: bold;\n  color: var(--text2-color);\n  word-break: break-all;\n  display: -webkit-box;\n  -webkit-line-clamp: 2;\n  line-clamp: 2;\n  -webkit-box-orient: vertical;\n  overflow: hidden;\n}\n\n/* 预览容器 */\n.preview-container {\n  margin: auto 0;\n  text-align: center;\n}\n\n.preview-container .icon {\n  /* height: 150px; */\n  max-height: 150px;\n  max-width: 233px;\n}\n\n/* .preview-image {\n  max-width: 100%;\n  max-height: 200px;\n  object-fit: contain;\n} */\n\n.video-preview {\n  width: 100%;\n  max-height: 150px;\n}\n\n.video-preview video {\n  max-width: 100%;\n  max-height: 100%;\n}\n\n/* 底部信息栏 */\n.bottom-row {\n  /* margin-top: auto; */\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  gap: 2px;\n}\n\n.file-info {\n  margin: 0 auto;\n  flex-shrink: 0;\n}\n\n/* 操作图标 */\n.actions {\n  display: flex;\n  gap: 2px;\n  justify-content: center;\n  margin-bottom: -5px;\n}\n\n.actions .icon {\n  width: 23px;\n  transition: all 0.1s;\n  opacity: 0.5;\n}\n\n.actions .icon:hover {\n  /* transform: scale(1.1); */\n  opacity: 1;\n}\n\n.actions .icon:active {\n  transform: scale(0.9);\n}\n\n/* 全屏预览 */\n.play-container,\n.image-container {\n  position: fixed;\n  top: 0;\n  left: 0;\n  width: 100vw;\n  height: 100vh;\n  background: rgba(0, 0, 0, 0.8);\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  z-index: 4;\n}\n\n.play-container.hide,\n.image-container.hide,\n.video-preview.hide {\n  display: none;\n}\n\n#video-player,\n#image-player {\n  max-width: 90vw;\n  max-height: 90vh;\n  width: auto;\n  height: auto;\n  object-fit: contain;\n}\n\n/* 框选 */\n#selection-box {\n  position: absolute;\n  border: 1px solid var(--button2-color);\n  background-color: var(--button-active-color);\n  pointer-events: none;\n  z-index: 3;\n  display: none;\n}\n\n/* 提示框 */\n.alert-box {\n  position: fixed;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n  background: rgba(0, 0, 0, 0.8);\n  color: white;\n  padding: 20px 40px;\n  border-radius: 8px;\n  opacity: 0;\n  visibility: hidden;\n  transition: all 0.3s ease;\n  z-index: 1000;\n}\n.alert-box.active {\n  opacity: 1;\n  visibility: visible;\n}\n\n/* 分页组件样式 */\n.pagination {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  gap: 8px;\n  /* margin-top: 20px; */\n  padding: 15px;\n  background: var(--background-color-two);\n  border-radius: 8px;\n}\n.pagination.hide {\n  display: none;\n}\n.page-numbers {\n  display: flex;\n  gap: 5px;\n  flex-wrap: wrap;\n}\n"
  },
  {
    "path": "css/public.css",
    "content": ":root {\n  /* 两个背景色 两个文字以及链接文字配色 */\n  --background-color: #fff;\n  --background-color-opacity: #ffffffea;\n  --background-color-two: #f5f5f5;\n  --text-color: #000;\n  --text-error-color: #ff0000;\n  --text2-color: rgb(26, 115, 232);\n  --link-color: #3079ed;\n\n  /* 设置页面 设置box 背景色 */\n  --optionBox-color: var(--background-color);\n\n  /* 两个按钮 配色 边框 */\n  --button-color: #fff;\n  --button-text-color: rgb(26, 115, 232);\n  --button-border: solid 1px #c7c7c780;\n  --button-hover-color: rgb(66 133 244 / 4%);\n  --button-active-color: rgb(66 133 244 / 10%);\n\n  --button2-color: rgb(26, 115, 232);\n  --button2-text-color: #fff;\n  --button2-border: solid 1px #c7c7c780;\n  --button2-hover-color: rgb(26 115 232 / 90%);\n  --button2-active-color: rgb(26 115 232 / 50%);\n\n  /* 滚动条配色 */\n  --scrollbar-track-color: #f5f5f500;\n  --scrollbar-thumb-color: #1a73e8;\n\n  /* 设置页面 滑动开关配色 */\n  --switch-off-color: rgb(189, 193, 198);\n  --switch-on-color: rgb(26, 115, 232);\n  --switch-round-color: #fff;\n\n  /* input textarea select 边框配色 */\n  --input-border: solid 1px #000;\n}\nhtml {\n  color: var(--text-color);\n  background: var(--background-color);\n  scrollbar-width: thin;\n}\ninput,\ntextarea,\nselect {\n  color: var(--text-color);\n  background: var(--background-color);\n  scrollbar-width: thin;\n  border: var(--input-border);\n}\na,\na:link,\na:visited {\n  color: var(--link-color);\n}\nbutton,\n.button,\n.button2 {\n  border-radius: 4px;\n  cursor: pointer;\n  margin: 0 0 3px 0;\n  user-select: none;\n}\nbutton,\n.button {\n  background: var(--button-color);\n  border: var(--button-border);\n  color: var(--button-text-color);\n}\nbutton:hover,\n.button:hover {\n  background: var(--button-hover-color);\n}\nbutton:active,\n.button:active {\n  background: var(--button-active-color);\n}\n.button2 {\n  background: var(--button2-color);\n  border: var(--button2-border);\n  color: var(--button2-text-color);\n}\n.button2:hover {\n  background: var(--button2-hover-color);\n}\n.button2:active {\n  background: var(--button2-active-color);\n}\nbutton:disabled,\n.button:disabled,\n.button2:disabled,\n.disabled {\n  background-color: #ccc;\n  color: #666;\n  cursor: not-allowed;\n  opacity: 0.6;\n}\n.bold {\n  font-weight: bold;\n}\n.hide {\n  display: none;\n}\n.textColor {\n  color: var(--text2-color);\n}\n.width100 {\n  width: 100%;\n}\n.height100 {\n  height: 100%;\n}\n.line {\n  border-top: solid 1px rgb(0 0 0 / 50%);\n  margin: 10px 0 10px 0;\n}\n.no-drop {\n  background-color: #ccc !important;\n  cursor: no-drop;\n  color: var(--button2-text-color);\n}\n.icon {\n  -webkit-user-drag: none;\n}\n/*定义整个滚动条高宽及背景：高宽分别对应横竖滚动条的尺寸*/\n::-webkit-scrollbar {\n  width: 5px;\n}\n/*定义滚动条轨道：内阴影+圆角*/\n::-webkit-scrollbar-track {\n  background-color: var(--scrollbar-track-color);\n}\n/*定义滑块：内阴影+圆角*/\n::-webkit-scrollbar-thumb {\n  border-radius: 10px;\n  background-color: var(--scrollbar-thumb-color);\n}\n@media (prefers-color-scheme: dark) {\n  :root {\n    --background-color: #0f172a;\n    --background-color-opacity: #0f172aea;\n    --background-color-two: #1e293b;\n    --text-color: #fff;\n    --text-error-color: #ff0000;\n    --text2-color: #fff;\n    --link-color: #94a3b8;\n\n    --optionBox-color: var(--background-color-two);\n\n    --button-color: #161b22;\n    --button-border: solid 1px #c7c7c780;\n    --button-text-color: #fff;\n    --button-hover-color: rgb(66 133 244 / 4%);\n    --button-active-color: rgb(66 133 244 / 10%);\n\n    --button2-color: rgb(26 115 232 / 50%);\n    --button2-border: solid 1px #c7c7c780;\n    --button2-text-color: #fff;\n    --button2-hover-color: rgb(26 115 232 / 90%);\n    --button2-active-color: rgb(26 115 232 / 50%);\n\n    --scrollbar-track-color: #f5f5f500;\n    --scrollbar-thumb-color: #1a73e8;\n\n    --switch-off-color: rgb(189, 193, 198);\n    --switch-on-color: rgb(26 115 232 / 50%);\n    --switch-round-color: #fff;\n\n    --input-border: solid 1px #ffffffb6;\n  }\n  img.regex {\n    content: url(../img/regex-dark.png);\n  }\n  img.copy {\n    content: url(../img/copy-dark.png);\n  }\n  img.parsing {\n    content: url(../img/parsing-dark.png);\n  }\n  img.play {\n    content: url(../img/play-dark.png);\n  }\n  img.download {\n    content: url(../img/download-dark.svg);\n  }\n  img.qrcode {\n    content: url(../img/qrcode-dark.png);\n  }\n  img.cat-down {\n    content: url(../img/cat-down-dark.png);\n  }\n  img.aria2 {\n    content: url(../img/aria2-dark.png);\n  }\n  img.invoke {\n    content: url(../img/invoke-dark.svg);\n  }\n  img.send {\n    content: url(../img/send-dark.svg);\n  }\n  img.delete {\n    content: url(../img/delete-dark.svg);\n  }\n  img.mqtt {\n    content: url(../img/mqtt-dark.svg);\n  }\n  img.send2ffmpeg {\n    content: url(../img/send2ffmpeg-dark.svg);\n  }\n}\n"
  },
  {
    "path": "downloader.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n    <meta charset=\"UTF-8\" />\n    <title>titleDownload</title>\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"css/public.css\" media=\"all\" />\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"css/options.css\" media=\"all\" />\n    <script src=\"lib/jquery.min.js\"></script>\n    <script src=\"lib/StreamSaver.js\"></script>\n    <script src=\"js/init.js\"></script>\n    <script src=\"js/firefox.js\"></script>\n    <script src=\"js/function.js\"></script>\n</head>\n\n<body>\n    <div class=\"wrapper1024 hide\" id=\"getURL\">\n        <section>\n            <h1 class=\"optionsTitle\" data-i18n=\"titleDownload\"></h1>\n            <div class=\"optionBox\">\n                <input type=\"text\" id=\"url\" placeholder=\"URL\" class=\"fullInput\" />\n                <input type=\"text\" id=\"referer\" placeholder=\"referer\" class=\"fullInput\" />\n                <button id=\"getURL_btn\" type=\"button\" data-i18n=\"download\"></button>\n                <label class=\"textColor\"><input type=\"checkbox\" id=\"downStream\"><span\n                        data-i18n-outer=\"downloadWhileSaving\"></span></label>\n            </div>\n        </section>\n    </div>\n\n    <div class=\"wrapper1024 newDownload\">\n        <section id=\"downfile\">\n            <h1 class=\"optionsTitle\" data-i18n=\"titleDownload\"></h1>\n            <div class=\"optionBox\" id=\"downBox\"></div>\n        </section>\n        <section>\n            <div class=\"optionBox\">\n                <button class=\"openDir button2\" data-i18n=\"openDir\"></button>\n                <button id=\"ffmpeg\" class=\"button2 hide\" data-i18n=\"sendFfmpeg\"></button>\n                <button id=\"stopDownload\" class=\"button2\" data-i18n=\"stopDownload\"></button>\n                <button id=\"test\" class=\"button2 hide\">test</button>\n                <label class=\"textColor\"><input type=\"checkbox\" id=\"autoClose\"><span\n                        data-i18n-outer=\"autoCloserDownload\"></span></label>\n            </div>\n        </section>\n    </div>\n\n    <script src=\"js/m3u8.downloader.js\"></script>\n    <script src=\"js/downloader.js\"></script>\n    <script src=\"js/i18n.js\"></script>\n</body>\n\n</html>"
  },
  {
    "path": "install.html",
    "content": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>猫抓扩展安装成功</title>\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"css/install.css\" media=\"all\" />\n</head>\n\n<body>\n    <div class=\"container\">\n        <!-- 语言切换按钮 -->\n        <div class=\"lang-switch\" id=\"langToggle\">\n            <span class=\"lang-emoji\" id=\"langEmoji\">🌐</span>\n            <span class=\"lang-text\" id=\"langText\">English</span>\n        </div>\n\n        <div class=\"header\">\n            <div class=\"logo-container\">\n                <div class=\"logo\"><img src=\"img/icon.png\"></div>\n            </div>\n            <h1 id=\"main-title\">恭喜 猫抓 扩展已成功安装 !</h1>\n            <div class=\"subtitle\" id=\"subtitle\">Installation successful !</div>\n        </div>\n\n        <div class=\"card fade-in\">\n            <div class=\"card-header\">\n                <div class=\"card-title\">\n                    <span class=\"emoji\">🙌</span>\n                    <span id=\"welcome-title\">希望本扩展能帮助到你</span>\n                </div>\n            </div>\n            <div class=\"card-body\">\n                <div class=\"policy-section\">\n                    <div class=\"section-title\">\n                        <span class=\"emoji\">🔒</span>\n                        <span id=\"privacy-title\">隐私政策 / Privacy Policy</span>\n                    </div>\n                    <div class=\"content-box\">\n                        <div class=\"lang-zh active\">\n                            <p>本扩展收集所有信息都在本地储存处理，不会发送到远程服务器，不包含任何跟踪器。</p>\n                        </div>\n                        <div class=\"lang-en\">\n                            <p>The extension collects and processes all information locally without sending it to remote\n                                servers and does not include any trackers.</p>\n                        </div>\n                    </div>\n                </div>\n\n                <div class=\"policy-section\">\n                    <div class=\"section-title\">\n                        <span class=\"emoji\">⚠️</span>\n                        <span id=\"disclaimer-title\">免责声明 / Disclaimer</span>\n                    </div>\n                    <div class=\"content-box\">\n                        <div class=\"lang-zh active\">\n                            <p>本扩展仅供下载用户拥有版权或已获授权的视频，禁止用于下载受版权保护且未经授权的内容。用户需自行承担使用本工具的全部法律责任，开发者不对用户的任何行为负责。本工具按\"原样\"提供，开发者不承担任何直接或间接责任。\n                            </p>\n                        </div>\n                        <div class=\"lang-en\">\n                            <p>This extension is intended for downloading videos that you own or have authorized access\n                                to. It is prohibited to use this Tool for downloading copyrighted content without\n                                permission. Users are solely responsible for their actions, and the developer is not\n                                liable for any user behavior. This Tool is provided \"as-is,\" and the developer assumes\n                                no direct or indirect liability.</p>\n                        </div>\n                    </div>\n                </div>\n\n                <div class=\"policy-section\">\n                    <div class=\"section-title\">\n                        <span class=\"emoji\">🚨</span>\n                        <span id=\"issue-title\">问题提交 / Issue Submission</span>\n                    </div>\n                    <div class=\"content-box\">\n                        <div class=\"lang-zh active\">\n                            <a href=\"https://o2bmm.gitbook.io/cat-catch/issues\" target=\"_blank\">\n                                <span class=\"emoji\">🔗</span>\n                                https://o2bmm.gitbook.io/cat-catch/issues\n                            </a>\n                        </div>\n                        <div class=\"lang-en\">\n                            <a href=\"https://o2bmm.gitbook.io/cat-catch/issues\" target=\"_blank\">\n                                <span class=\"emoji\">🔗</span>\n                                https://o2bmm.gitbook.io/cat-catch/issues\n                            </a>\n                        </div>\n                    </div>\n                </div>\n\n                <div class=\"agreement\">\n                    <div class=\"lang-zh active\">\n                        <p>点击\"同意\"或\"关闭本页面\"即表示您已阅读并同意以上内容。</p>\n                    </div>\n                    <div class=\"lang-en\">\n                        <p>By clicking \"Agree\" or \"Close this page,\" you confirm that you have read and agree to the\n                            above terms.</p>\n                    </div>\n                </div>\n\n                <div class=\"buttons\">\n                    <button id=\"agreeBtn\" class=\"btn btn-primary\">\n                        <span class=\"emoji\">✅</span>\n                        <span id=\"agreeText\">同意</span>\n                    </button>\n                    <button id=\"uninstallBtn\" class=\"btn btn-outline\">\n                        <span class=\"emoji\">🗑️</span>\n                        <span id=\"uninstallText\">卸载扩展</span>\n                    </button>\n                </div>\n            </div>\n        </div>\n    </div>\n\n    <script src=\"js/function.js\"></script>\n    <script src=\"js/install.js\"></script>\n</body>\n\n</html>"
  },
  {
    "path": "js/background.js",
    "content": "importScripts(\"/js/function.js\", \"/js/init.js\");\n\n// Service Worker 5分钟后会强制终止扩展\n// https://bugs.chromium.org/p/chromium/issues/detail?id=1271154\n// https://stackoverflow.com/questions/66618136/persistent-service-worker-in-chrome-extension/70003493#70003493\nchrome.webNavigation.onBeforeNavigate.addListener(function () { return; });\nchrome.webNavigation.onHistoryStateUpdated.addListener(function () { return; });\nchrome.runtime.onConnect.addListener(function (Port) {\n    if (chrome.runtime.lastError || Port.name !== \"HeartBeat\") return;\n    Port.postMessage(\"HeartBeat\");\n    Port.onMessage.addListener(function (message, Port) { return; });\n    const interval = setInterval(function () {\n        clearInterval(interval);\n        Port.disconnect();\n    }, 250000);\n    Port.onDisconnect.addListener(function () {\n        interval && clearInterval(interval);\n        if (chrome.runtime.lastError) { return; }\n    });\n});\n\n/**\n *  定时任务\n *  nowClear clear 清理冗余数据\n *  save 保存数据\n */\nchrome.alarms.onAlarm.addListener(function (alarm) {\n    if (alarm.name === \"nowClear\" || alarm.name === \"clear\") {\n        clearRedundant();\n        return;\n    }\n    if (alarm.name === \"save\") {\n        (chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData });\n        return;\n    }\n});\n\n// onBeforeRequest 浏览器发送请求之前使用正则匹配发送请求的URL\n// chrome.webRequest.onBeforeRequest.addListener(\n//     function (data) {\n//         try { findMedia(data, true); } catch (e) { console.log(e); }\n//     }, { urls: [\"<all_urls>\"] }, [\"requestBody\"]\n// );\n// 保存requestHeaders\nchrome.webRequest.onSendHeaders.addListener(\n    function (data) {\n        if (G && G.initSyncComplete && !G.enable) { return; }\n        if (data.requestHeaders) {\n            G.requestHeaders.set(data.requestId, data.requestHeaders);\n            data.allRequestHeaders = data.requestHeaders;\n        }\n        try { findMedia(data, true); } catch (e) { console.log(e); }\n    }, { urls: [\"<all_urls>\"] }, ['requestHeaders',\n        chrome.webRequest.OnBeforeSendHeadersOptions.EXTRA_HEADERS].filter(Boolean)\n);\n// onResponseStarted 浏览器接收到第一个字节触发，保证有更多信息判断资源类型\nchrome.webRequest.onResponseStarted.addListener(\n    function (data) {\n        try {\n            data.allRequestHeaders = G.requestHeaders.get(data.requestId);\n            if (data.allRequestHeaders) {\n                G.requestHeaders.delete(data.requestId);\n            }\n            findMedia(data);\n        } catch (e) { console.log(e, data); }\n    }, { urls: [\"<all_urls>\"] }, [\"responseHeaders\"]\n);\n// 删除失败的requestHeadersData\nchrome.webRequest.onErrorOccurred.addListener(\n    function (data) {\n        G.requestHeaders.delete(data.requestId);\n        G.blackList.delete(data.requestId);\n    }, { urls: [\"<all_urls>\"] }\n);\n\nfunction findMedia(data, isRegex = false, filter = false, timer = false) {\n    // Service Worker被强行杀死之后重新自我唤醒，等待全局变量初始化完成。\n    if (!G || !G.initSyncComplete || !G.initLocalComplete || G.tabId == undefined || cacheData.init) {\n        if (timer) { return; }\n        setTimeout(() => {\n            findMedia(data, isRegex, filter, true);\n        }, 500);\n        return;\n    }\n\n    if (G.damn && G.damnUrlSet.has(data.tabId)) {\n        return;\n    }\n\n    // 检查 是否启用 是否在当前标签是否在屏蔽列表中\n    const blockUrlFlag = data.tabId && data.tabId > 0 && G.blockUrlSet.has(data.tabId);\n    if (!G.enable || (G.blockUrlWhite ? !blockUrlFlag : blockUrlFlag)) {\n        return;\n    }\n\n    data.getTime = Date.now();\n\n    if (!isRegex && G.blackList.has(data.requestId)) {\n        G.blackList.delete(data.requestId);\n        return;\n    }\n    // 屏蔽特殊页面发起的资源\n    if (data.initiator != \"null\" &&\n        data.initiator != undefined &&\n        isSpecialPage(data.initiator)) { return; }\n    if (G.isFirefox &&\n        data.originUrl &&\n        isSpecialPage(data.originUrl)) { return; }\n    // 屏蔽特殊页面的资源\n    if (isSpecialPage(data.url)) { return; }\n    const urlParsing = new URL(data.url);\n    let [name, ext] = fileNameParse(urlParsing.pathname);\n\n    //正则匹配\n    if (isRegex && !filter) {\n        for (let key in G.Regex) {\n            if (!G.Regex[key].state) { continue; }\n            G.Regex[key].regex.lastIndex = 0;\n            let result = G.Regex[key].regex.exec(data.url);\n            if (result == null) { continue; }\n            if (G.Regex[key].blackList) {\n                G.blackList.add(data.requestId);\n                return;\n            }\n            data.extraExt = G.Regex[key].ext ? G.Regex[key].ext : undefined;\n            if (result.length == 1) {\n                findMedia(data, true, true);\n                return;\n            }\n            result.shift();\n            result = result.map(str => decodeURIComponent(str));\n            if (!result[0].startsWith('https://') && !result[0].startsWith('http://')) {\n                result[0] = urlParsing.protocol + \"//\" + data.url;\n            }\n            data.url = result.join(\"\");\n            findMedia(data, true, true);\n            return;\n        }\n        return;\n    }\n\n    // 非正则匹配\n    if (!isRegex) {\n        // 获取头部信息\n        data.header = getResponseHeadersValue(data);\n        //检查后缀\n        if (!filter && ext != undefined) {\n            filter = CheckExtension(ext, data.header?.size);\n            if (filter == \"break\") { return; }\n        }\n        //检查类型\n        if (!filter && data.header?.type != undefined) {\n            filter = CheckType(data.header.type, data.header?.size);\n            if (filter == \"break\") { return; }\n        }\n        //查找附件\n        if (!filter && data.header?.attachment != undefined) {\n            const res = data.header.attachment.match(reFilename);\n            if (res && res[1]) {\n                [name, ext] = fileNameParse(decodeURIComponent(res[1]));\n                filter = CheckExtension(ext, 0);\n                if (filter == \"break\") { return; }\n            }\n        }\n        //放过类型为media的资源\n        if (data.type == \"media\") {\n            filter = true;\n        }\n    }\n\n    if (!filter) { return; }\n\n    // 谜之原因 获取得资源 tabId可能为 -1 firefox中则正常\n    // 检查是 -1 使用当前激活标签得tabID\n    data.tabId = data.tabId == -1 ? G.tabId : data.tabId;\n\n    cacheData[data.tabId] ??= [];\n    cacheData[G.tabId] ??= [];\n\n    // 缓存数据大于9999条 清空缓存 避免内存占用过多\n    if (cacheData[data.tabId].length > G.maxLength) {\n        cacheData[data.tabId] = [];\n        (chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData });\n        return;\n    }\n\n    // 查重 避免CPU占用 大于500 强制关闭查重\n    // if (G.checkDuplicates && cacheData[data.tabId].length <= 500) {\n    //     for (let item of cacheData[data.tabId]) {\n    //         if (item.url.length == data.url.length &&\n    //             item.cacheURL.pathname == urlParsing.pathname &&\n    //             item.cacheURL.host == urlParsing.host &&\n    //             item.cacheURL.search == urlParsing.search) { return; }\n    //     }\n    // }\n\n    if (G.checkDuplicates && cacheData[data.tabId].length <= 500) {\n        const tabFingerprints = G.urlMap.get(data.tabId) || new Set();\n        if (tabFingerprints.has(data.url)) {\n            return; // 找到重复，直接返回\n        }\n        tabFingerprints.add(data.url);\n        G.urlMap.set(data.tabId, tabFingerprints);\n        if (tabFingerprints.size >= 500) {\n            tabFingerprints.clear();\n        }\n    }\n\n    chrome.tabs.get(data.tabId, async function (webInfo) {\n        if (chrome.runtime.lastError) { return; }\n        data.requestHeaders = getRequestHeaders(data);\n        // requestHeaders 中cookie 单独列出来\n        if (data.requestHeaders?.cookie) {\n            data.cookie = data.requestHeaders.cookie;\n            data.requestHeaders.cookie = undefined;\n        }\n        const info = {\n            name: name,\n            url: data.url,\n            size: data.header?.size,\n            ext: ext,\n            type: data.mime ?? data.header?.type,\n            tabId: data.tabId,\n            isRegex: isRegex,\n            requestId: data.requestId ?? Date.now().toString(),\n            initiator: data.initiator,\n            requestHeaders: data.requestHeaders,\n            cookie: data.cookie,\n            // cacheURL: { host: urlParsing.host, search: urlParsing.search, pathname: urlParsing.pathname },\n            getTime: data.getTime\n        };\n        // 不存在扩展使用类型\n        if (info.ext === undefined && info.type !== undefined) {\n            info.ext = info.type.split(\"/\")[1];\n        }\n        // 正则匹配的备注扩展\n        if (data.extraExt) {\n            info.ext = data.extraExt;\n        }\n        // 不存在 initiator 和 referer 使用web url代替initiator\n        if (info.initiator == undefined || info.initiator == \"null\") {\n            info.initiator = info.requestHeaders?.referer ?? webInfo?.url;\n        }\n        // 装载页面信息\n        info.title = webInfo?.title ?? \"NULL\";\n        info.favIconUrl = webInfo?.favIconUrl;\n        info.webUrl = webInfo?.url;\n        // 屏蔽资源\n        if (!isRegex && G.blackList.has(data.requestId)) {\n            G.blackList.delete(data.requestId);\n            return;\n        }\n        // 发送到popup 并检查自动下载\n        chrome.runtime.sendMessage({ Message: \"popupAddData\", data: info }, function () {\n            if (G.featAutoDownTabId.size > 0 && G.featAutoDownTabId.has(info.tabId) && chrome.downloads?.State) {\n                try {\n                    const downDir = info.title == \"NULL\" ? \"CatCatch/\" : stringModify(info.title) + \"/\";\n                    let fileName = isEmpty(info.name) ? stringModify(info.title) + '.' + info.ext : decodeURIComponent(stringModify(info.name));\n                    if (G.TitleName) {\n                        fileName = filterFileName(templates(G.downFileName, info));\n                    } else {\n                        fileName = downDir + fileName;\n                    }\n                    chrome.downloads.download({\n                        url: info.url,\n                        filename: fileName\n                    });\n                } catch (e) { return; }\n            }\n            if (chrome.runtime.lastError) { return; }\n        });\n\n        // 数据发送\n        if (G.send2local) {\n            try { send2local(\"catch\", { ...info, requestHeaders: data.allRequestHeaders }, info.tabId); } catch (e) { console.log(e); }\n        }\n\n        // 储存数据\n        cacheData[info.tabId] ??= [];\n        cacheData[info.tabId].push(info);\n\n        // 当前标签媒体数量大于100 开启防抖 等待5秒储存 或 积累10个资源储存一次。\n        if (cacheData[info.tabId].length >= 100 && debounceCount <= 10) {\n            debounceCount++;\n            clearTimeout(debounce);\n            debounce = setTimeout(function () { save(info.tabId); }, 5000);\n            return;\n        }\n        // 时间间隔小于500毫秒 等待2秒储存\n        if (Date.now() - debounceTime <= 500) {\n            clearTimeout(debounce);\n            debounceTime = Date.now();\n            debounce = setTimeout(function () { save(info.tabId); }, 2000);\n            return;\n        }\n        save(info.tabId);\n    });\n}\n// cacheData数据 储存到 chrome.storage.local\nfunction save(tabId) {\n    clearTimeout(debounce);\n    debounceTime = Date.now();\n    debounceCount = 0;\n    if (cacheData[tabId]) {\n        // 单个标签数据超过99条 不再保存到storage\n        if (cacheData[tabId]?.length <= 99) {\n            (chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData }, function () {\n                chrome.runtime.lastError && console.log(chrome.runtime.lastError);\n            });\n        }\n        SetIcon({ number: cacheData[tabId].length, tabId: tabId });\n    }\n}\n\n/**\n * 监听 扩展 message 事件\n */\nchrome.runtime.onMessage.addListener(function (Message, sender, sendResponse) {\n    if (chrome.runtime.lastError) { return; }\n    if (!G.initLocalComplete || !G.initSyncComplete) {\n        sendResponse(\"error\");\n        return true;\n    }\n    // 以下检查是否有 tabId 不存在使用当前标签\n    Message.tabId = Message.tabId ?? G.tabId;\n\n    // 从缓存中保存数据到本地\n    if (Message.Message == \"pushData\") {\n        (chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData });\n        sendResponse(\"ok\");\n        return true;\n    }\n    // 获取所有数据\n    if (Message.Message == \"getAllData\") {\n        sendResponse(cacheData);\n        return true;\n    }\n    /**\n     * 设置扩展图标数字\n     * 提供 type 删除标签为 tabId 的数字\n     * 不提供type 删除所有标签的数字\n     */\n    if (Message.Message == \"ClearIcon\") {\n        Message.type ? SetIcon({ tabId: Message.tabId }) : SetIcon();\n        sendResponse(\"ok\");\n        return true;\n    }\n    // 启用/禁用扩展\n    if (Message.Message == \"enable\") {\n        G.enable = !G.enable;\n        chrome.storage.sync.set({ enable: G.enable });\n        chrome.action.setIcon({ path: G.enable ? \"/img/icon.png\" : \"/img/icon-disable.png\" });\n        sendResponse(G.enable);\n        return true;\n    }\n    /**\n     * 提供requestId数组 获取指定的数据\n     */\n    if (Message.Message == \"getData\" && Message.requestId) {\n        // 判断Message.requestId是否数组\n        if (!Array.isArray(Message.requestId)) {\n            Message.requestId = [Message.requestId];\n        }\n        const response = [];\n        if (Message.requestId.length) {\n            for (let item in cacheData) {\n                for (let data of cacheData[item]) {\n                    if (Message.requestId.includes(data.requestId)) {\n                        response.push(data);\n                    }\n                }\n            }\n        }\n        sendResponse(response.length ? response : \"error\");\n        return true;\n    }\n    /**\n     * 提供 tabId 获取该标签数据\n     */\n    if (Message.Message == \"getData\") {\n        sendResponse(cacheData[Message.tabId]);\n        return true;\n    }\n    /**\n     * 获取各按钮状态\n     * 模拟手机 自动下载 启用 以及各种脚本状态\n     */\n    if (Message.Message == \"getButtonState\") {\n        let state = {\n            MobileUserAgent: G.featMobileTabId.has(Message.tabId),\n            AutoDown: G.featAutoDownTabId.has(Message.tabId),\n            enable: G.enable,\n        }\n        G.scriptList.forEach(function (item, key) {\n            state[item.key] = item.tabId.has(Message.tabId);\n        });\n        sendResponse(state);\n        return true;\n    }\n    // 对tabId的标签 进行模拟手机操作\n    if (Message.Message == \"mobileUserAgent\") {\n        mobileUserAgent(Message.tabId, !G.featMobileTabId.has(Message.tabId));\n        chrome.tabs.reload(Message.tabId, { bypassCache: true });\n        sendResponse(\"ok\");\n        return true;\n    }\n    // 对tabId的标签 开启 关闭 自动下载\n    if (Message.Message == \"autoDown\") {\n        if (G.featAutoDownTabId.has(Message.tabId)) {\n            G.featAutoDownTabId.delete(Message.tabId);\n        } else {\n            G.featAutoDownTabId.add(Message.tabId);\n        }\n        (chrome.storage.session ?? chrome.storage.local).set({ featAutoDownTabId: Array.from(G.featAutoDownTabId) });\n        sendResponse(\"ok\");\n        return true;\n    }\n    // 对tabId的标签 脚本注入或删除\n    if (Message.Message == \"script\") {\n        if (G.damn && G.damnUrlSet.has(Message.tabId)) {\n            return;\n        }\n        if (!G.scriptList.has(Message.script)) {\n            sendResponse(\"error no exists\");\n            return false;\n        }\n        const script = G.scriptList.get(Message.script);\n        const scriptTabid = script.tabId;\n        const refresh = Message.refresh ?? script.refresh;\n        if (scriptTabid.has(Message.tabId)) {\n            scriptTabid.delete(Message.tabId);\n            if (Message.script == \"search.js\") {\n                G.deepSearchTemporarilyClose = Message.tabId;\n            }\n            refresh && chrome.tabs.reload(Message.tabId, { bypassCache: true });\n            sendResponse(\"ok\");\n            return true;\n        }\n        scriptTabid.add(Message.tabId);\n        if (refresh) {\n            chrome.tabs.reload(Message.tabId, { bypassCache: true });\n        } else {\n            const files = [`catch-script/${Message.script}`];\n            script.i18n && files.unshift(\"catch-script/i18n.js\");\n            chrome.scripting.executeScript({\n                target: { tabId: Message.tabId, allFrames: script.allFrames },\n                files: files,\n                injectImmediately: true,\n                world: script.world\n            });\n        }\n        sendResponse(\"ok\");\n        return true;\n    }\n    // 脚本注入 脚本申请多语言文件\n    if (Message.Message == \"scriptI18n\") {\n        chrome.scripting.executeScript({\n            target: { tabId: Message.tabId, allFrames: true },\n            files: [\"catch-script/i18n.js\"],\n            injectImmediately: true,\n            world: \"MAIN\"\n        });\n        sendResponse(\"ok\");\n        return true;\n    }\n    // Heart Beat\n    if (Message.Message == \"HeartBeat\") {\n        chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {\n            if (tabs[0] && tabs[0].id) {\n                G.tabId = tabs[0].id;\n            }\n        });\n        sendResponse(\"HeartBeat OK\");\n        return true;\n    }\n    // 清理数据\n    if (Message.Message == \"clearData\") {\n        // 当前标签\n        if (Message.type) {\n            delete cacheData[Message.tabId];\n            (chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData });\n            clearRedundant();\n            sendResponse(\"OK\");\n            return true;\n        }\n        // 其他标签\n        for (let item in cacheData) {\n            if (item == Message.tabId) { continue; }\n            delete cacheData[item];\n        }\n        (chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData });\n        clearRedundant();\n        sendResponse(\"OK\");\n        return true;\n    }\n    // 清理冗余数据\n    if (Message.Message == \"clearRedundant\") {\n        clearRedundant();\n        sendResponse(\"OK\");\n        return true;\n    }\n    // 从 content-script 或 catch-script 传来的媒体url\n    if (Message.Message == \"addMedia\") {\n        chrome.tabs.query({}, function (tabs) {\n            for (let item of tabs) {\n                if (item.url == Message.href) {\n                    findMedia({ url: Message.url, tabId: item.id, extraExt: Message.extraExt, mime: Message.mime, requestId: Message.requestId, requestHeaders: Message.requestHeaders }, true, true);\n                    return true;\n                }\n            }\n            findMedia({ url: Message.url, tabId: -1, extraExt: Message.extraExt, mime: Message.mime, requestId: Message.requestId, initiator: Message.href, requestHeaders: Message.requestHeaders }, true, true);\n        });\n        sendResponse(\"ok\");\n        return true;\n    }\n    // ffmpeg网页通信\n    if (Message.Message == \"catCatchFFmpeg\") {\n        const data = { ...Message, Message: \"ffmpeg\", tabId: Message.tabId ?? sender.tab.id, version: G.ffmpegConfig.version };\n        chrome.tabs.query({ url: G.ffmpegConfig.url + \"*\" }, function (tabs) {\n            if (chrome.runtime.lastError || !tabs.length) {\n                chrome.tabs.create({ url: G.ffmpegConfig.url, active: Message.active ?? true }, function (tab) {\n                    if (chrome.runtime.lastError) { return; }\n                    G.ffmpegConfig.tab = tab.id;\n                    G.ffmpegConfig.cacheData.push(data);\n                });\n                return true;\n            }\n            if (tabs[0].status == \"complete\") {\n                chrome.tabs.sendMessage(tabs[0].id, data);\n            } else {\n                G.ffmpegConfig.tab = tabs[0].id;\n                G.ffmpegConfig.cacheData.push(data);\n            }\n        });\n        sendResponse(\"ok\");\n        return true;\n    }\n    // 发送数据到本地\n    if (Message.Message == \"send2local\" && G.send2local) {\n        try { send2local(Message.action, Message.data, Message.tabId); } catch (e) { console.log(e); }\n        sendResponse(\"ok\");\n        return true;\n    }\n    if (Message.Message == \"damnUrlHas\") {\n        sendResponse(G.damnUrlSet.has(Message.tabId));\n        return true;\n    }\n});\n\n// 选定标签 更新G.tabId\n// chrome.tabs.onHighlighted.addListener(function (activeInfo) {\n//     if (activeInfo.windowId == -1 || !activeInfo.tabIds || !activeInfo.tabIds.length) { return; }\n//     G.tabId = activeInfo.tabIds[0];\n// });\n\n/**\n * 监听 切换标签\n * 更新全局变量 G.tabId 为当前标签\n */\nchrome.tabs.onActivated.addListener(function (activeInfo) {\n    G.tabId = activeInfo.tabId;\n    if (cacheData[G.tabId] !== undefined) {\n        SetIcon({ number: cacheData[G.tabId].length, tabId: G.tabId });\n        return;\n    }\n    SetIcon({ tabId: G.tabId });\n});\n\n// 切换窗口，更新全局变量G.tabId\nchrome.windows.onFocusChanged.addListener(function (activeInfo) {\n    if (activeInfo == -1) { return; }\n    chrome.tabs.query({ active: true, windowId: activeInfo }, function (tabs) {\n        if (tabs[0] && tabs[0].id) {\n            G.tabId = tabs[0].id;\n        } else {\n            G.tabId = -1;\n        }\n    });\n}, { filters: [\"normal\"] });\n\n/**\n * 监听 标签页面更新\n * 检查 清理数据\n * 检查 是否在屏蔽列表中\n */\nchrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {\n    if (isSpecialPage(tab.url) || tabId <= 0 || !G.initSyncComplete) { return; }\n    // console.log('onUpdated', tabId, changeInfo, tab);\n    if (changeInfo.status && changeInfo.status == \"loading\" && G.autoClearMode == 2) {\n        G.urlMap.delete(tabId);\n        chrome.alarms.get(\"save\", function (alarm) {\n            if (!alarm) {\n                delete cacheData[tabId];\n                SetIcon({ tabId: tabId });\n                chrome.alarms.create(\"save\", { when: Date.now() + 1000 });\n            }\n        });\n    }\n    // 检查当前标签是否在屏蔽列表中\n    if (changeInfo.url && tabId > 0) {\n        if (G.blockUrl.length) {\n            G.blockUrlSet.delete(tabId);\n            if (isLockUrl(changeInfo.url)) {\n                G.blockUrlSet.add(tabId);\n            }\n        }\n\n        G.damnUrlSet.delete(tabId);\n        if (isDamnUrl(changeInfo.url)) {\n            G.damnUrlSet.add(tabId);\n        }\n    }\n    chrome.sidePanel.setOptions({\n        tabId,\n        path: \"popup.html?tabId=\" + tabId\n    });\n});\n\n/**\n * 监听 frame 正在载入\n * 检查 是否在屏蔽列表中 (frameId == 0 为主框架)\n * 检查 自动清理 (frameId == 0 为主框架)\n * 检查 注入脚本\n */\nchrome.webNavigation.onCommitted.addListener(function (details) {\n    if (isSpecialPage(details.url) || details.tabId <= 0 || !G.initSyncComplete) { return; }\n    // console.log('onCommitted', details);\n\n    // 刷新页面 检查是否在屏蔽列表中\n    if (details.frameId == 0) {\n        G.blockUrlSet.delete(details.tabId);\n        if (isLockUrl(details.url)) {\n            G.blockUrlSet.add(details.tabId);\n        }\n\n        G.damnUrlSet.delete(details.tabId);\n        if (isDamnUrl(details.url)) {\n            G.damnUrlSet.add(details.tabId);\n        }\n    }\n\n    // 刷新清理角标数\n    if (details.frameId == 0 && (!['auto_subframe', 'manual_subframe', 'form_submit'].includes(details.transitionType)) && G.autoClearMode == 1) {\n        delete cacheData[details.tabId];\n        G.urlMap.delete(details.tabId);\n        (chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData });\n        SetIcon({ tabId: details.tabId });\n    }\n\n    // chrome内核版本 102 以下不支持 chrome.scripting.executeScript API\n    if (G.version < 102) { return; }\n\n    if (G.deepSearch && G.deepSearchTemporarilyClose != details.tabId) {\n        G.scriptList.get(\"search.js\").tabId.add(details.tabId);\n        G.deepSearchTemporarilyClose = null;\n    }\n\n    // catch-script 脚本\n    G.scriptList.forEach(function (item, script) {\n        if (!item.tabId.has(details.tabId) || !item.allFrames) { return true; }\n\n        const files = [`catch-script/${script}`];\n        item.i18n && files.unshift(\"catch-script/i18n.js\");\n        chrome.scripting.executeScript({\n            target: { tabId: details.tabId, frameIds: [details.frameId] },\n            files: files,\n            injectImmediately: true,\n            world: item.world\n        });\n    });\n\n    // 模拟手机\n    if (G.initLocalComplete && G.featMobileTabId.size > 0 && G.featMobileTabId.has(details.tabId)) {\n        chrome.scripting.executeScript({\n            args: [G.MobileUserAgent.toString()],\n            target: { tabId: details.tabId, frameIds: [details.frameId] },\n            func: function () {\n                Object.defineProperty(navigator, 'userAgent', { value: arguments[0], writable: false });\n            },\n            injectImmediately: true,\n            world: \"MAIN\"\n        });\n    }\n});\n\n/**\n * 监听 标签关闭 清理数据\n */\nchrome.tabs.onRemoved.addListener(function (tabId) {\n    // 清理缓存数据\n    chrome.alarms.get(\"nowClear\", function (alarm) {\n        !alarm && chrome.alarms.create(\"nowClear\", { when: Date.now() + 1000 });\n    });\n    if (G.initSyncComplete) {\n        G.blockUrlSet.has(tabId) && G.blockUrlSet.delete(tabId);\n        G.damnUrlSet.has(tabId) && G.damnUrlSet.delete(tabId);\n    }\n});\n\n/**\n * 浏览器 扩展快捷键\n */\nchrome.commands.onCommand.addListener(function (command) {\n    if (command == \"auto_down\") {\n        if (G.featAutoDownTabId.has(G.tabId)) {\n            G.featAutoDownTabId.delete(G.tabId);\n        } else {\n            G.featAutoDownTabId.add(G.tabId);\n        }\n        (chrome.storage.session ?? chrome.storage.local).set({ featAutoDownTabId: Array.from(G.featAutoDownTabId) });\n    } else if (command == \"catch\") {\n        const scriptTabid = G.scriptList.get(\"catch.js\").tabId;\n        scriptTabid.has(G.tabId) ? scriptTabid.delete(G.tabId) : scriptTabid.add(G.tabId);\n        chrome.tabs.reload(G.tabId, { bypassCache: true });\n    } else if (command == \"m3u8\") {\n        chrome.tabs.create({ url: \"m3u8.html\" });\n    } else if (command == \"clear\") {\n        delete cacheData[G.tabId];\n        (chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData });\n        clearRedundant();\n        SetIcon({ tabId: G.tabId });\n    } else if (command == \"enable\") {\n        G.enable = !G.enable;\n        chrome.storage.sync.set({ enable: G.enable });\n        chrome.action.setIcon({ path: G.enable ? \"/img/icon.png\" : \"/img/icon-disable.png\" });\n    } else if (command == \"reboot\") {\n        chrome.runtime.reload();\n    } else if (command == \"deepSearch\") {\n        const script = G.scriptList.get(\"search.js\");\n        const scriptTabid = script.tabId;\n        if (scriptTabid.has(G.tabId)) {\n            scriptTabid.delete(G.tabId);\n            G.deepSearchTemporarilyClose = G.tabId;\n            chrome.tabs.reload(G.tabId, { bypassCache: true });\n            return;\n        }\n        scriptTabid.add(G.tabId);\n        chrome.tabs.reload(G.tabId, { bypassCache: true });\n    }\n});\n\n/**\n * 监听 页面完全加载完成 判断是否在线ffmpeg页面\n * 如果是在线ffmpeg 则发送数据\n */\nchrome.webNavigation.onCompleted.addListener(function (details) {\n    if (G.ffmpegConfig.tab && details.tabId == G.ffmpegConfig.tab) {\n        setTimeout(() => {\n            G.ffmpegConfig.cacheData.forEach(data => {\n                chrome.tabs.sendMessage(details.tabId, data);\n            });\n            G.ffmpegConfig.cacheData = [];\n            G.ffmpegConfig.tab = 0;\n        }, 500);\n    }\n});\n\n// 操作符检查\nfunction operatorCheck(size, Obj) {\n    const targetSize = Obj.size * 1024;\n    switch (Obj.operator) {\n        case \"=\":\n            return size == targetSize;\n        case \"<\":\n            return size < targetSize;\n        case \">\":\n            return size > targetSize;\n        case \"<=\":\n            return size <= targetSize;\n        case \">=\":\n            return size >= targetSize;\n        case \"!=\":\n            return size != targetSize;\n        case \"~\":\n            return (Obj.min ? size >= Obj.min * 1024 : true) && (Obj.max ? size <= Obj.max * 1024 : true);\n        default:\n            return size <= targetSize;\n    }\n}\n\n/**\n * 检查扩展名和大小\n * @param {String} ext \n * @param {Number} size \n * @returns {Boolean|String}\n */\nfunction CheckExtension(ext, size) {\n    const Ext = G.Ext.get(ext);\n    if (!Ext) { return false; }\n    if (!Ext.state) { return \"break\"; }\n    // if (Ext.size != 0 && size != undefined && size <= Ext.size * 1024) { return \"break\"; }\n    if (Ext.size != 0 && size != undefined && !operatorCheck(size, Ext)) {\n        return \"break\";\n    }\n    return true;\n}\n\n/**\n * 检查类型和大小\n * @param {String} dataType \n * @param {Number} dataSize \n * @returns {Boolean|String}\n */\nfunction CheckType(dataType, dataSize) {\n    const typeInfo = G.Type.get(dataType.split(\"/\")[0] + \"/*\") || G.Type.get(dataType);\n    if (!typeInfo) { return false; }\n    if (!typeInfo.state) { return \"break\"; }\n    if (typeInfo.size != 0 && dataSize != undefined && !operatorCheck(dataSize, typeInfo)) {\n        return \"break\";\n    }\n    return true;\n}\n\n/**\n * 获取文件名及扩展名\n * @param {String} pathname \n * @returns {Array}\n */\nfunction fileNameParse(pathname) {\n    let fileName = decodeURI(pathname.split(\"/\").pop());\n    let ext = fileName.split(\".\");\n    ext = ext.length == 1 ? undefined : ext.pop().toLowerCase();\n    return [fileName, ext ? ext : undefined];\n}\n\n/**\n * 获取响应头信息\n * @param {Object} data \n * @returns {Object}\n */\nfunction getResponseHeadersValue(data) {\n    const header = {};\n    if (data.responseHeaders == undefined || data.responseHeaders.length == 0) { return header; }\n    for (let item of data.responseHeaders) {\n        item.name = item.name.toLowerCase();\n        if (item.name == \"content-length\") {\n            header.size ??= parseInt(item.value);\n        } else if (item.name == \"content-type\") {\n            header.type = item.value.split(\";\")[0].toLowerCase();\n        } else if (item.name == \"content-disposition\") {\n            header.attachment = item.value;\n        } else if (item.name == \"content-range\") {\n            let size = item.value.split('/')[1];\n            if (size !== '*') {\n                header.size = parseInt(size);\n            }\n        }\n    }\n    return header;\n}\n\n/**\n * 获取请求头\n * @param {Object} data \n * @returns {Object|Boolean}\n */\nfunction getRequestHeaders(data) {\n    if (data.allRequestHeaders == undefined || data.allRequestHeaders.length == 0) { return false; }\n    const header = {};\n    for (let item of data.allRequestHeaders) {\n        item.name = item.name.toLowerCase();\n        if (item.name == \"referer\") {\n            header.referer = item.value;\n        } else if (item.name == \"origin\") {\n            header.origin = item.value;\n        } else if (item.name == \"cookie\") {\n            header.cookie = item.value;\n        } else if (item.name == \"authorization\") {\n            header.authorization = item.value;\n        }\n    }\n    if (Object.keys(header).length) {\n        return header;\n    }\n    return false;\n}\n//设置扩展图标\nfunction SetIcon(obj) {\n    if (obj?.number == 0 || obj?.number == undefined) {\n        chrome.action.setBadgeText({ text: \"\", tabId: obj?.tabId ?? G.tabId }, function () { if (chrome.runtime.lastError) { return; } });\n        // chrome.action.setTitle({ title: \"还没闻到味儿~\", tabId: obj.tabId }, function () { if (chrome.runtime.lastError) { return; } });\n    } else if (G.badgeNumber) {\n        obj.number = obj.number > 999 ? \"999+\" : obj.number.toString();\n        chrome.action.setBadgeText({ text: obj.number, tabId: obj.tabId }, function () { if (chrome.runtime.lastError) { return; } });\n        // chrome.action.setTitle({ title: \"抓到 \" + obj.number + \" 条鱼\", tabId: obj.tabId }, function () { if (chrome.runtime.lastError) { return; } });\n    }\n}\n\n// 模拟手机端\nfunction mobileUserAgent(tabId, change = false) {\n    if (change) {\n        G.featMobileTabId.add(tabId);\n        (chrome.storage.session ?? chrome.storage.local).set({ featMobileTabId: Array.from(G.featMobileTabId) });\n        chrome.declarativeNetRequest.updateSessionRules({\n            removeRuleIds: [tabId],\n            addRules: [{\n                \"id\": tabId,\n                \"action\": {\n                    \"type\": \"modifyHeaders\",\n                    \"requestHeaders\": [{\n                        \"header\": \"User-Agent\",\n                        \"operation\": \"set\",\n                        \"value\": G.MobileUserAgent\n                    }]\n                },\n                \"condition\": {\n                    \"tabIds\": [tabId],\n                    \"resourceTypes\": Object.values(chrome.declarativeNetRequest.ResourceType)\n                }\n            }]\n        });\n        return true;\n    }\n    G.featMobileTabId.delete(tabId) && (chrome.storage.session ?? chrome.storage.local).set({ featMobileTabId: Array.from(G.featMobileTabId) });\n    chrome.declarativeNetRequest.updateSessionRules({\n        removeRuleIds: [tabId]\n    });\n}\n\n// 判断特殊页面\nfunction isSpecialPage(url) {\n    if (!url || url == \"null\") { return true; }\n    return !(url.startsWith(\"http://\") || url.startsWith(\"https://\") || url.startsWith(\"blob:\"));\n}\n\n// 测试\n// chrome.storage.local.get(function (data) { console.log(data.MediaData) });\n// chrome.declarativeNetRequest.getSessionRules(function (rules) { console.log(rules); });\n// chrome.tabs.query({}, function (tabs) { for (let item of tabs) { console.log(item.id); } });"
  },
  {
    "path": "js/content-script.js",
    "content": "(function () {\n    var _videoObj = [];\n    var _videoSrc = [];\n    var _key = new Set();\n    chrome.runtime.onMessage.addListener(function (Message, sender, sendResponse) {\n        if (chrome.runtime.lastError) { return; }\n        // 获取页面视频对象\n        if (Message.Message == \"getVideoState\") {\n            let videoObj = [];\n            let videoSrc = [];\n            document.querySelectorAll(\"video, audio\").forEach(function (video) {\n                if (video.currentSrc != \"\" && video.currentSrc != undefined) {\n                    videoObj.push(video);\n                    videoSrc.push(video.currentSrc);\n                }\n            });\n            const iframe = document.querySelectorAll(\"iframe\");\n            if (iframe.length > 0) {\n                iframe.forEach(function (iframe) {\n                    if (iframe.contentDocument == null) { return true; }\n                    iframe.contentDocument.querySelectorAll(\"video, audio\").forEach(function (video) {\n                        if (video.currentSrc != \"\" && video.currentSrc != undefined) {\n                            videoObj.push(video);\n                            videoSrc.push(video.currentSrc);\n                        }\n                    });\n                });\n            }\n            if (videoObj.length > 0) {\n                if (videoObj.length !== _videoObj.length || videoSrc.toString() !== _videoSrc.toString()) {\n                    _videoSrc = videoSrc;\n                    _videoObj = videoObj;\n                }\n                Message.index = Message.index == -1 ? 0 : Message.index;\n                const video = videoObj[Message.index];\n                const timePCT = video.currentTime / video.duration * 100;\n                sendResponse({\n                    time: timePCT,\n                    currentTime: video.currentTime,\n                    duration: video.duration,\n                    volume: video.volume,\n                    count: _videoObj.length,\n                    src: _videoSrc,\n                    paused: video.paused,\n                    loop: video.loop,\n                    speed: video.playbackRate,\n                    muted: video.muted,\n                    type: video.tagName.toLowerCase()\n                });\n                return true;\n            }\n            sendResponse({ count: 0 });\n            return true;\n        }\n        // 速度控制\n        if (Message.Message == \"speed\") {\n            _videoObj[Message.index].playbackRate = Message.speed;\n            return true;\n        }\n        // 画中画\n        if (Message.Message == \"pip\") {\n            if (document.pictureInPictureElement) {\n                try { document.exitPictureInPicture(); } catch (e) { return true; }\n                sendResponse({ state: false });\n                return true;\n            }\n            try { _videoObj[Message.index].requestPictureInPicture(); } catch (e) { return true; }\n            sendResponse({ state: true });\n            return true;\n        }\n        // 全屏\n        if (Message.Message == \"fullScreen\") {\n            if (document.fullscreenElement) {\n                try { document.exitFullscreen(); } catch (e) { return true; }\n                sendResponse({ state: false });\n                return true;\n            }\n            setTimeout(function () {\n                try { _videoObj[Message.index].requestFullscreen(); } catch (e) { return true; }\n            }, 500);\n            sendResponse({ state: true });\n            return true;\n        }\n        // 播放\n        if (Message.Message == \"play\") {\n            _videoObj[Message.index].play();\n            return true;\n        }\n        // 暂停\n        if (Message.Message == \"pause\") {\n            _videoObj[Message.index].pause();\n            return true;\n        }\n        // 循环播放\n        if (Message.Message == \"loop\") {\n            _videoObj[Message.index].loop = Message.action;\n            return true;\n        }\n        // 设置音量\n        if (Message.Message == \"setVolume\") {\n            _videoObj[Message.index].volume = Message.volume;\n            sendResponse(\"ok\");\n            return true;\n        }\n        // 静音\n        if (Message.Message == \"muted\") {\n            _videoObj[Message.index].muted = Message.action;\n            return true;\n        }\n        // 设置视频进度\n        if (Message.Message == \"setTime\") {\n            const time = Message.time * _videoObj[Message.index].duration / 100;\n            _videoObj[Message.index].currentTime = time;\n            sendResponse(\"ok\");\n            return true;\n        }\n        // 截图视频图片\n        if (Message.Message == \"screenshot\") {\n            try {\n                let video = _videoObj[Message.index];\n                let canvas = document.createElement(\"canvas\");\n                canvas.width = video.videoWidth;\n                canvas.height = video.videoHeight;\n                canvas.getContext(\"2d\").drawImage(video, 0, 0, canvas.width, canvas.height);\n                let link = document.createElement(\"a\");\n                link.href = canvas.toDataURL(\"image/jpeg\");\n                link.download = `${location.hostname}-${secToTime(video.currentTime)}.jpg`;\n                link.click();\n                canvas = null;\n                link = null;\n                sendResponse(\"ok\");\n                return true;\n            } catch (e) { console.log(e); return true; }\n        }\n        if (Message.Message == \"getKey\") {\n            sendResponse(Array.from(_key));\n            return true;\n        }\n        if (Message.Message == \"ffmpeg\") {\n            if (!Message.files) {\n                window.postMessage(Message);\n                sendResponse(\"ok\");\n                return true;\n            }\n            Message.quantity ??= Message.files.length;\n            for (let item of Message.files) {\n                const data = { ...Message, ...item };\n                data.type = item.type ?? \"video\";\n                if (data.data instanceof Blob) {\n                    window.postMessage(data);\n                } else {\n                    fetch(data.data)\n                        .then(response => response.blob())\n                        .then(blob => {\n                            data.data = blob;\n                            window.postMessage(data);\n                        });\n                }\n            }\n            sendResponse(\"ok\");\n            return true;\n        }\n        if (Message.Message == \"getPage\") {\n            if (Message.find) {\n                const DOM = document.querySelector(Message.find);\n                DOM ? sendResponse(DOM.innerHTML) : sendResponse(\"\");\n                return true;\n            }\n            sendResponse(document.documentElement.outerHTML);\n            return true;\n        }\n    });\n\n    // Heart Beat\n    var Port;\n    function connect() {\n        Port = chrome.runtime.connect(chrome.runtime.id, { name: \"HeartBeat\" });\n        Port.postMessage(\"HeartBeat\");\n        Port.onMessage.addListener(function (message, Port) { return true; });\n        Port.onDisconnect.addListener(connect);\n    }\n    connect();\n\n    function secToTime(sec) {\n        let time = \"\";\n        let hour = Math.floor(sec / 3600);\n        let min = Math.floor((sec % 3600) / 60);\n        sec = Math.floor(sec % 60);\n        if (hour > 0) { time = hour + \"'\"; }\n        if (min < 10) { time += \"0\"; }\n        time += min + \"'\";\n        if (sec < 10) { time += \"0\"; }\n        time += sec;\n        return time;\n    }\n    window.addEventListener(\"message\", (event) => {\n        if (!event.data || !event.data.action) { return; }\n        if (event.data.action == \"catCatchAddMedia\") {\n            if (!event.data.url) { return; }\n            chrome.runtime.sendMessage({\n                Message: \"addMedia\",\n                url: event.data.url,\n                href: event.data.href ?? event.source.location.href,\n                extraExt: event.data.ext,\n                mime: event.data.mime,\n                requestHeaders: { referer: event.data.referer },\n                requestId: event.data.requestId\n            });\n        }\n        if (event.data.action == \"catCatchAddKey\") {\n            let key = event.data.key;\n            if (key instanceof ArrayBuffer || key instanceof Array) {\n                key = ArrayToBase64(key);\n            }\n            if (_key.has(key)) { return; }\n            _key.add(key);\n            chrome.runtime.sendMessage({\n                Message: \"send2local\",\n                action: \"addKey\",\n                data: key,\n            });\n            chrome.runtime.sendMessage({\n                Message: \"popupAddKey\",\n                data: key,\n                url: event.data.url,\n            });\n        }\n        if (event.data.action == \"catCatchFFmpeg\") {\n            if (!event.data.use ||\n                !event.data.files ||\n                !event.data.files instanceof Array ||\n                event.data.files.length == 0\n            ) { return; }\n            event.data.title = event.data.title ?? document.title ?? new Date().getTime().toString();\n            event.data.title = event.data.title.replaceAll('\"', \"\").replaceAll(\"'\", \"\").replaceAll(\" \", \"\");\n            let data = {\n                Message: event.data.action,\n                action: event.data.use,\n                files: event.data.files,\n                url: event.data.href ?? event.source.location.href,\n            };\n            data = { ...event.data, ...data };\n            chrome.runtime.sendMessage(data);\n        }\n        if (event.data.action == \"catCatchFFmpegResult\") {\n            if (!event.data.state || !event.data.tabId) { return; }\n            chrome.runtime.sendMessage({ Message: \"catCatchFFmpegResult\", ...event.data });\n        }\n        if (event.data.action == \"catCatchToBackground\") {\n            delete event.data.action;\n            chrome.runtime.sendMessage(event.data);\n        }\n        // if (event.data.action == \"catCatchDashDRMMedia\") {\n        //     // TODO DRM Media\n        //     console.log(\"DRM Media\", event);\n        // }\n    }, false);\n\n    function ArrayToBase64(data) {\n        try {\n            let bytes = new Uint8Array(data);\n            let binary = \"\";\n            for (let i = 0; i < bytes.byteLength; i++) {\n                binary += String.fromCharCode(bytes[i]);\n            }\n            if (typeof _btoa == \"function\") {\n                return _btoa(binary);\n            }\n            return btoa(binary);\n        } catch (e) {\n            return false;\n        }\n    }\n})();"
  },
  {
    "path": "js/downloader.js",
    "content": "// url 参数解析\nconst params = new URL(location.href).searchParams;\nconst _requestId = params.get(\"requestId\") ? params.get(\"requestId\").split(\",\") : [];   // 要下载得资源ID\nconst _ffmpeg = params.get(\"ffmpeg\");   // 启用在线FFmpeg\nlet _downStream = params.get(\"downStream\"); // 启用边下边存 流式下载\nconst _data = [];   // 通过_requestId获取得到得数据\nconst _taskId = Date.parse(new Date()); // 配合ffmpeg使用的任务ID 以便在线ffmpeg通过ID知道文件属于哪些任务\nlet _tabId = null;  // 当前页面tab id\nlet _index = null;  // 当前页面 tab index\n\n// 是否表单提交下载 表单提交 不使用自定义文件名\nconst downloadData = localStorage.getItem('downloadData') ? JSON.parse(localStorage.getItem('downloadData')) : [];\n\nawaitG(() => {\n    loadCSS();\n    // 获取当前标签信息\n    chrome.tabs.getCurrent(function (tabs) {\n        _tabId = tabs.id;\n        _index = tabs.index;\n\n        // 如果没有requestId 显示 提交表单\n        if (!_requestId.length) {\n            $(\"#downStream\").prop(\"checked\", G.downStream);\n            $(\"#getURL, .newDownload\").toggle();\n            $(\"#getURL_btn\").click(function () {\n                const data = [{\n                    url: $(\"#getURL #url\").val().trim(),\n                    requestId: 1,\n                }];\n\n                // 处理请求头 如果是url直接放入referer 支持json格式\n                const referer = $(\"#getURL #referer\").val().trim();\n                if (referer) {\n                    if (referer.startsWith(\"http\")) {\n                        data[0].requestHeaders = { referer: referer };\n                    } else {\n                        data[0].requestHeaders = JSONparse(referer);\n                    }\n                }\n\n                _downStream = $(\"#downStream\").prop(\"checked\");\n                _data.push(...data);\n                setHeaders(data, start(), _tabId);\n                $(\"#getURL, .newDownload\").toggle();\n            });\n            return;\n        }\n\n        // 优先从downloadData 提取任务数据\n        for (let item of downloadData) {\n            if (_requestId.includes(item.requestId)) {\n                _data.push(item);\n                _requestId.splice(_requestId.indexOf(item.requestId), 1);\n            }\n        }\n        if (!_requestId.length) {\n            setHeaders(_data, start(), _tabId);\n            return;\n        }\n\n        // downloadData 不存在 从后台获取数据\n        chrome.runtime.sendMessage({ Message: \"getData\", requestId: _requestId }, function (data) {\n            if (data == \"error\" || !Array.isArray(data) || chrome.runtime.lastError || data.length == 0) {\n                alert(i18n.dataFetchFailed);\n                return;\n            }\n            _data.push(...data);\n            setHeaders(data, start(), _tabId);\n        });\n    });\n});\n\nfunction start() {\n\n    // 提前打开ffmpeg页面\n    if (_ffmpeg) {\n        chrome.runtime.sendMessage({\n            Message: \"catCatchFFmpeg\",\n            action: \"openFFmpeg\",\n            extra: i18n.waitingForMedia\n        });\n    }\n\n    $(\"#autoClose\").prop(\"checked\", G.downAutoClose);\n    streamSaver.mitm = G.streamSaverConfig.url;\n\n    const $downBox = $(\"#downBox\"); // 下载列表容器\n    const down = new Downloader(_data);  // 创建下载器 \n    const itemDOM = new Map();  // 提前储存需要平凡操作的dom对象 提高效率\n\n    $(\"#test\").click(() => console.log(down));\n\n    // 添加html\n    const addHtml = (fragment) => {\n        if (!fragment.downFileName) {\n            fragment.downFileName = getUrlFileName(fragment.url);\n        }\n        const html = $(`\n            <div class=\"downItem\">\n                <div class=\"explain\">${fragment.downFileName}</div>\n                <div id=\"downFilepProgress\"></div>\n                <div class=\"progress-container\">\n                    <div class=\"progress-wrapper\">\n                        <div class=\"progress-bar\">\n                            <div class=\"progress\"></div>\n                        </div>\n                    </div>\n                    <button class=\"cancel-btn\">${i18n.stopDownload}</button>\n                </div>\n            </div>`);\n\n        const $button = html.find(\"button\");\n        $button.data(\"action\", \"stop\");\n\n        // 操作对象放入itemDOM 提高效率\n        itemDOM.set(fragment.index, {\n            progressText: html.find(\"#downFilepProgress\"),\n            progress: html.find(\".progress\"),\n            button: $button\n        });\n\n        $button.click(function () {\n            const action = $(this).data(\"action\");\n            if (action == \"stop\") {\n                down.stop(fragment.index);\n                $(this).html(i18n.retryDownload).data(\"action\", \"start\");\n                if (fragment.fileStream) {\n                    fragment.fileStream.close();\n                }\n            } else if (action == \"start\") {\n                if (fragment.fileStream) {\n                    fragment.fileStream = streamSaver.createWriteStream(fragment.downFileName).getWriter();\n                }\n                down.state = \"waiting\";\n                down.downloader(fragment);\n                $(this).html(i18n.stopDownload).data(\"action\", \"stop\");\n            }\n        });\n        $downBox.append(html);\n\n        // 流式下载处理\n        if ((_downStream || G.downStream) && !_ffmpeg) {\n            fragment.fileStream = streamSaver.createWriteStream(fragment.downFileName).getWriter();\n        }\n    }\n\n    // 下载列表添加对应html\n    down.fragments.forEach(addHtml);\n\n    // 文件进程事件\n    let lastEmitted = Date.now();\n    down.on('itemProgress', function (fragment, state, receivedLength, contentLength, value) {\n        // 通过 lastEmitted 限制更新频率 避免疯狂dom操作\n        if (Date.now() - lastEmitted >= 100 && !state) {\n            const $dom = itemDOM.get(fragment.index);\n            if (contentLength) {\n                const progress = (receivedLength / contentLength * 100).toFixed(2) + \"%\";\n                $dom.progress.css(\"width\", progress).html(progress);\n                $dom.progressText.html(`${byteToSize(receivedLength)} / ${byteToSize(contentLength)}`);\n            } else {\n                $dom.progressText.html(`${byteToSize(receivedLength)}`);\n            }\n            if (down.total == 1) {\n                const title = contentLength ?\n                    `${byteToSize(receivedLength)} / ${byteToSize(contentLength)}` :\n                    `${byteToSize(receivedLength)}`;\n                document.title = title;\n            }\n            lastEmitted = Date.now();\n        }\n    });\n\n    // 单文件下载完成事件\n    down.on('completed', function (buffer, fragment) {\n\n        const $dom = itemDOM.get(fragment.index);\n        $dom.progress.css(\"width\", \"100%\").html(\"100%\");\n        $dom.progressText.html(i18n.downloadComplete);\n        $dom.button.html(i18n.sendFfmpeg).data(\"action\", \"sendFfmpeg\");\n        document.title = `${down.success}/${down.total}`;\n        $dom.button.hide();\n\n        // 是流式下载 停止写入\n        if (fragment.fileStream) {\n            fragment.fileStream.close();\n            fragment.fileStream = null;\n            return;\n        }\n\n        // 转为blob\n        const blob = ArrayBufferToBlob(buffer, { type: fragment.contentType });\n\n        // 发送到ffmpeg\n        if (_ffmpeg) {\n            sendFile(_ffmpeg, blob, fragment);\n            $dom.progressText.html(i18n.sendFfmpeg);\n            return;\n        }\n\n        $dom.progressText.html(i18n.saving);\n        // 直接下载\n        chrome.downloads.download({\n            url: URL.createObjectURL(blob),\n            filename: fragment.downFileName,\n            saveAs: G.saveAs\n        }, function (downloadId) {\n            fragment.downId = downloadId;\n        });\n    });\n\n    // 全部下载完成事件\n    down.on('allCompleted', function (buffer) {\n        $(\"#stopDownload\").hide();\n\n        // 检查 down.fragments 是否都为边下边存 检查自动关闭\n        if (down.fragments.every(item => item.fileStream) && $(\"#autoClose\").prop(\"checked\")) {\n            setTimeout(() => {\n                closeTab();\n            }, Math.ceil(Math.random() * 999));\n        }\n    });\n\n    // 错误处理\n    down.on('downloadError', function (fragment, error) {\n        // 添加range请求头 重新尝试下载\n        if (!fragment.retry?.Range && error?.cause == \"HTTPError\") {\n            fragment.retry = { \"Range\": \"bytes=0-\" };\n            down.stop(fragment.index);\n            down.downloader(fragment);\n            return;\n        }\n        // 添加sec-fetch 再次尝试下载\n        if (!fragment.retry?.sec && error?.cause == \"HTTPError\") {\n            fragment.retry.sec = true;\n            if (!fragment.requestHeaders) { fragment.requestHeaders = {}; }\n            fragment.requestHeaders = { ...fragment.requestHeaders, \"sec-fetch-mode\": \"no-cors\", \"sec-fetch-site\": \"same-site\" };\n            setHeaders(fragment, () => { down.stop(fragment.index); down.downloader(fragment); }, _tabId);\n            return;\n        }\n        itemDOM.get(fragment.index).progressText.html(error);\n        chrome.tabs.highlight({ tabs: _index });\n    });\n\n    // 开始下载事件 如果存在range重下标记 则添加 range 请求头\n    down.on('start', function (fragment, options) {\n        if (fragment.retry) {\n            options.headers = fragment.retry;\n            options.cache = \"no-cache\";\n        }\n    });\n\n    // 全部停止下载按钮\n    $(\"#stopDownload\").click(function () {\n        down.stop();\n        // 更新对应的按钮状态\n        itemDOM.forEach((item, index) => {\n            if (item.button.data(\"action\") == \"stop\") {\n                item.button.html(i18n.retryDownload).data(\"action\", \"start\");\n                if (down.fragments[index].fileStream) {\n                    down.fragments[index].fileStream.close();\n                    down.fragments[index].fileStream = null;\n                }\n            }\n        });\n    });\n\n    // 打开下载目录\n    $(\".openDir\").click(function () {\n        if (down.fragments[0].downId) {\n            chrome.downloads.show(down.fragments[0].downId);\n            return;\n        }\n        chrome.downloads.showDefaultFolder();\n    });\n\n    // 监听事件\n    chrome.runtime.onMessage.addListener(function (Message, sender, sendResponse) {\n        if (!Message.Message) { return; }\n\n        // 外部添加下载任务\n        if (Message.Message == \"catDownload\" && Message.data && Array.isArray(Message.data)) {\n            // ffmpeg任务的下载器 不允许添加新任务\n            if (_ffmpeg) {\n                sendResponse({ message: \"FFmpeg\", tabId: _tabId });\n                return;\n            }\n            setHeaders(Message.data, () => {\n                for (let fragment of Message.data) {\n                    // 检查fragment是否已经存在\n                    if (down.fragments.find(item => item.requestId == fragment.requestId)) {\n                        continue;\n                    }\n\n                    _data.push(fragment);\n                    down.push(fragment);\n                    addHtml(fragment);\n\n                    // 修改url requestId 参数\n                    const url = new URL(location.href);\n                    url.searchParams.set(\"requestId\", down.fragments.map(item => item.requestId).join(\",\"));\n                    history.replaceState(null, null, url);\n\n                    // 数据储存到localStorage\n                    downloadData.push(fragment);\n                    localStorage.setItem('downloadData', JSON.stringify(downloadData));\n\n                    // 正在运行的下载任务小于线程数 则开始下载\n                    if (down.running < down.thread) {\n                        // down.downloader(fragment.index);\n                        down.downloader();\n                    }\n                };\n            }, _tabId);\n            sendResponse({ message: \"OK\", tabId: _tabId });\n            return;\n        }\n\n        // 以下为在线ffmpeg返回结果\n        if (Message.Message != \"catCatchFFmpegResult\" || Message.state != \"ok\" || _tabId == 0 || Message.tabId != _tabId) { return; }\n\n        // 发送状态提示\n        const $dom = itemDOM.get(Message.index);\n        $dom && $dom.progressText.html(i18n.hasSent);\n        down.buffer[Message.index] = null; //清空buffer\n\n        // 全部发送完成 检查自动关闭\n        if (down.success == down.total) {\n            if ($(\"#autoClose\").prop(\"checked\")) {\n                setTimeout(() => {\n                    closeTab();\n                }, Math.ceil(Math.random() * 999));\n            }\n        }\n    });\n\n    // 监听下载事件 下载完成 关闭窗口\n    chrome.downloads.onChanged.addListener(function (downloadDelta) {\n        if (!downloadDelta.state || downloadDelta.state.current != \"complete\") { return; }\n\n        // 检查id是否本页面提交的下载\n        const fragment = down.fragments.find(item => item.downId == downloadDelta.id);\n        if (!fragment) { return; }\n\n        down.buffer[fragment.index] = null; //清空buffer\n\n        // 更新下载状态\n        itemDOM.get(fragment.index).progressText.html(i18n.downloadComplete);\n\n        // 完成下载 检查自动关闭\n        if (down.success == down.total) {\n            document.title = i18n.downloadComplete;\n            if ($(\"#autoClose\").prop(\"checked\")) {\n                setTimeout(() => {\n                    closeTab();\n                }, Math.ceil(Math.random() * 999));\n            }\n        }\n    });\n\n    // 关闭页面 检查关闭所有未完成的下载流\n    window.addEventListener('beforeunload', function (e) {\n        const fileStream = down.fragments.filter(item => item.fileStream);\n        if (fileStream.length) {\n            e.preventDefault();\n            fileStream.forEach((fragment) => {\n                fragment.fileStream.close();\n            });\n        }\n    });\n\n    document.title = `${down.success}/${down.total}`;\n    down.start();\n}\n\n/**\n * 发送数据到在线FFmpeg\n * @param {String} action 发送类型\n * @param {ArrayBuffer|Blob} data 数据内容\n * @param {Object} fragment 数据对象\n */\nlet isCreatingTab = false;\nfunction sendFile(action, data, fragment) {\n    // 转 blob\n    if (data instanceof ArrayBuffer) {\n        data = ArrayBufferToBlob(data, { type: fragment.contentType });\n    }\n    chrome.tabs.query({ url: G.ffmpegConfig.url + \"*\" }, function (tabs) {\n        // 等待ffmpeg 打开并且可用\n        if (tabs.length === 0) {\n            if (!isCreatingTab) {\n                isCreatingTab = true; // 设置创建标志位\n                chrome.tabs.create({ url: G.ffmpegConfig.url });\n            }\n            setTimeout(sendFile, 500, action, data, fragment);\n            return;\n        } else if (tabs[0].status !== \"complete\") {\n            setTimeout(sendFile, 233, action, data, fragment);\n            return;\n        }\n        isCreatingTab = false; // 重置创建标志位\n        /**\n         * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Chrome_incompatibilities#data_cloning_algorithm\n         * chrome.runtime.sendMessage API\n         * chrome 的对象参数需要序列化 无法传递Blob\n         * firefox 可以直接传递Blob\n         */\n        const baseData = {\n            Message: \"catCatchFFmpeg\",\n            action: action,\n            files: [{ data: G.isFirefox ? data : URL.createObjectURL(data), name: getUrlFileName(fragment.url), index: fragment.index }],\n            title: stringModify(fragment.title),\n            tabId: _tabId\n        };\n        if (action === \"merge\") {\n            baseData.taskId = _taskId;\n            baseData.quantity = _data.length;\n        }\n\n        chrome.runtime.sendMessage(baseData);\n    });\n}"
  },
  {
    "path": "js/firefox.js",
    "content": "// 兼容Firefox\nif (typeof (browser) == \"object\" && !(typeof (G) == \"object\" && !G.isFirefox)) {\n    function importScripts() {\n        for (let script of arguments) {\n            const js = document.createElement('script');\n            js.src = script;\n            document.head.appendChild(js);\n        }\n    }\n\n    // browser.windows.onFocusChanged.addListener 少一个参数\n    const _onFocusChanged = chrome.windows.onFocusChanged.addListener;\n    chrome.windows.onFocusChanged.addListener = function (listener, option) {\n        _onFocusChanged(listener);\n    };\n\n    browser.runtime.onInstalled.addListener(({ reason }) => {\n        if (reason == \"install\") {\n            browser.tabs.create({ url: \"install.html\" });\n        }\n    });\n}"
  },
  {
    "path": "js/function.js",
    "content": "/**\n * 小于10的数字前面加0\n * @param {Number} date \n * @returns {String|Number}\n */\nfunction appendZero(date) {\n    return parseInt(date) < 10 ? `0${date}` : date;\n}\n\n/**\n * 秒转格式化成时间\n * @param {Number} sec \n * @returns {String}\n */\nfunction secToTime(sec) {\n    let hour = (sec / 3600) | 0;\n    let min = ((sec % 3600) / 60) | 0;\n    sec = (sec % 60) | 0;\n    let time = hour > 0 ? hour + \":\" : \"\";\n    time += min.toString().padStart(2, '0') + \":\";\n    time += sec.toString().padStart(2, '0');\n    return time;\n}\n\n/**\n * 字节转换成大小\n * @param {Number} byte 大小\n * @returns {String} 格式化后的文件大小\n */\nfunction byteToSize(byte) {\n    if (!byte || byte < 1024) { return 0; }\n    if (byte < 1024 * 1024) {\n        return (byte / 1024).toFixed(1) + \"KB\";\n    } else if (byte < 1024 * 1024 * 1024) {\n        return (byte / 1024 / 1024).toFixed(1) + \"MB\";\n    } else {\n        return (byte / 1024 / 1024 / 1024).toFixed(1) + \"GB\";\n    }\n}\n\n/**\n * Firefox download API 无法下载 data URL\n * @param {String} url \n * @param {String} fileName 文件名\n */\nfunction downloadDataURL(url, fileName) {\n    let link = document.createElement(\"a\");\n    link.href = url;\n    link.download = fileName;\n    link.click();\n    link = null;\n}\n\n/**\n * 判断变量是否为空\n * @param {Object|String} obj 判断的变量\n * @returns {Boolean}\n */\nfunction isEmpty(obj) {\n    return (typeof obj == \"undefined\" ||\n        obj == null ||\n        obj == \"\" ||\n        obj == \" \")\n}\n\n/**\n * 修改请求头\n * @param {Object} data 请求头数据\n * @param {Function} callback \n */\nfunction setRequestHeaders(data = {}, callback = undefined) {\n    chrome.declarativeNetRequest.updateSessionRules({ removeRuleIds: [1] });\n    chrome.tabs.getCurrent(function (tabs) {\n        const rules = { removeRuleIds: [tabs ? tabs.id : 1] };\n        if (Object.keys(data).length) {\n            rules.addRules = [{\n                \"id\": tabs ? tabs.id : 1,\n                \"priority\": tabs ? tabs.id : 1,\n                \"action\": {\n                    \"type\": \"modifyHeaders\",\n                    \"requestHeaders\": Object.keys(data).map(key => ({ header: key, operation: \"set\", value: data[key] }))\n                },\n                \"condition\": {\n                    \"resourceTypes\": [\"xmlhttprequest\", \"media\", \"image\"],\n                }\n            }];\n            if (tabs) {\n                rules.addRules[0].condition.tabIds = [tabs.id];\n            } else {\n                // initiatorDomains 只支持 chrome 101+ firefox 113+\n                if (G.version < 101 || (G.isFirefox && G.version < 113)) {\n                    callback && callback();\n                    return;\n                }\n                const domain = G.isFirefox\n                    ? new URL(chrome.runtime.getURL(\"\")).hostname\n                    : chrome.runtime.id;\n                rules.addRules[0].condition.initiatorDomains = [domain];\n            }\n        }\n        chrome.declarativeNetRequest.updateSessionRules(rules, function () {\n            callback && callback();\n        });\n    });\n}\n\n/**\n * 指定标签页修改 urlFilter请求头\n * @param {Object} data 需要修改请求头的对象数组\n * @param {*} callBack 回调函数\n * @param {*} tabId 需要修改的tabId\n */\nfunction setHeaders(data, callBack, tabId = -1) {\n    if (!tabId == -1) {\n        tabId = G.tabId;\n    }\n    const rules = { removeRuleIds: [], addRules: [] };\n    if (!Array.isArray(data)) {\n        data = [data];\n    }\n    for (let item of data) {\n        if (!item.requestHeaders) { continue; }\n        const rule = {\n            \"id\": parseInt(item.requestId),\n            \"action\": {\n                \"type\": \"modifyHeaders\",\n                \"requestHeaders\": Object.keys(item.requestHeaders).map(key => ({ header: key, operation: \"set\", value: item.requestHeaders[key] }))\n            },\n            \"condition\": {\n                \"resourceTypes\": [\"xmlhttprequest\", \"media\", \"image\"],\n                \"tabIds\": [tabId],\n                \"urlFilter\": item.url\n            }\n        }\n        if (item.cookie) {\n            rule.action.requestHeaders.push({ header: \"Cookie\", operation: \"set\", value: item.cookie });\n        }\n        rules.removeRuleIds.push(parseInt(item.requestId));\n        rules.addRules.push(rule);\n    }\n    chrome.declarativeNetRequest.updateSessionRules(rules, () => {\n        callBack && callBack();\n    });\n}\n\n/**\n * 等待全局变量G初始化完成\n * @param {Function} callback \n * @param {Number} sec\n */\nfunction awaitG(callback, sec = 0) {\n    const timer = setInterval(() => {\n        if (G.initSyncComplete && G.initLocalComplete) {\n            clearInterval(timer);\n            callback();\n        }\n    }, sec);\n}\n\n/**\n * 分割字符串 不分割引号内的内容\n * @param {String} text 需要处理的文本\n * @param {String} separator 分隔符\n * @returns {String} 返回分割后的字符串\n */\nfunction splitString(text, separator) {\n    text = text.trim();\n    if (text.length == 0) { return []; }\n    const parts = [];\n    let inQuotes = false;\n    let inSingleQuotes = false;\n    let start = 0;\n\n    for (let i = 0; i < text.length; i++) {\n        if (text[i] === separator && !inQuotes && !inSingleQuotes) {\n            parts.push(text.slice(start, i));\n            start = i + 1;\n        } else if (text[i] === '\"' && !inSingleQuotes) {\n            inQuotes = !inQuotes;\n        } else if (text[i] === \"'\" && !inQuotes) {\n            inSingleQuotes = !inSingleQuotes;\n        }\n    }\n    parts.push(text.slice(start));\n    return parts;\n}\n\n/**\n * 模板的函数处理\n * @param {String} text 文本\n * @param {String} action 函数名\n * @param {Object} data 填充的数据\n * @returns {String} 返回处理后的字符串\n */\nfunction templatesFunction(text, action, data) {\n    text = isEmpty(text) ? \"\" : text.toString();\n    action = splitString(action, \"|\");\n    for (let item of action) {\n        let action = item.trim();   // 函数\n        let arg = [];   //参数\n        // 查找 \":\" 区分函数与参数\n        const colon = item.indexOf(\":\");\n        if (colon != -1) {\n            action = item.slice(0, colon).trim();\n            arg = splitString(item.slice(colon + 1).trim(), \",\").map(item => {\n                // return item.trim().replace(/^['\"]|['\"]$/g, \"\");\n                return item.trim().replace(/^(['\"])([\\s\\S]*)\\1$/, '$2');\n            });\n        }\n        // 字符串不允许为空 除非 exists find prompt函数\n        if (isEmpty(text) && ![\"exists\", \"find\", \"prompt\"].includes(action)) { return \"\" };\n        // 参数不能为空 除非 filter prompt函数\n        if (arg.length == 0 && ![\"filter\", \"prompt\"].includes(action)) { return text }\n\n        if (action == \"slice\") {\n            text = text.slice(...arg);\n        } else if (action == \"replace\") {\n            text = text.replace(...arg);\n        } else if (action == \"replaceAll\") {\n            text = text.replaceAll(...arg);\n        } else if (action == \"regexp\") {\n            const result = text.match(new RegExp(...arg));\n            text = \"\";\n            if (result && result.length >= 2) {\n                for (let i = 1; i < result.length; i++) {\n                    if (result[i]) {\n                        text += result[i].trim();\n                    }\n                }\n            }\n        } else if (action == \"exists\") {\n            if (text) {\n                text = arg[0].replaceAll(\"*\", text);\n                continue;\n            }\n            if (arg[1]) {\n                text = arg[1].replaceAll(\"*\", text);\n                continue;\n            }\n            text = \"\";\n        } else if (action == \"prepend\") {\n            text = arg[0] + text;\n        } else if (action == \"concat\") {\n            text = text + arg[0];\n        } else if (action == \"to\") {\n            if (arg[0] == \"base64\") {\n                text = window.Base64 ? Base64.encode(text) : btoa(unescape(encodeURIComponent(text)));\n            } else if (arg[0] == \"urlEncode\") {\n                text = encodeURIComponent(text);\n            } else if (arg[0] == \"urlDecode\") {\n                text = decodeURIComponent(text);\n            } else if (arg[0] == \"lowerCase\") {\n                text = text.toLowerCase();\n            } else if (arg[0] == \"upperCase\") {\n                text = text.toUpperCase();\n            } else if (arg[0] == \"trim\") {\n                if (text) { text = text.trim(); }\n            } else if (arg[0] == \"filter\") {\n                if (text) { text = text.trim(); }\n                text = stringModify(text);\n            }\n        } else if (action == \"find\") {\n            text = \"\";\n            if (data.pageDOM) {\n                try {\n                    text = data.pageDOM.querySelector(arg[0]).innerText?.trim();\n                } catch (e) { text = \"\"; }\n            }\n        } else if (action == \"filter\") {\n            text = stringModify(text, arg[0]);\n        } else if (action == \"prompt\") {\n            text = window.prompt(\"\", text);\n        }\n    }\n    return text;\n}\n\n/**\n * 模板替换\n * @param {String} text 标签模板\n * @param {Object} data 填充的数据\n * @returns {String} 返回填充后的字符串\n */\nfunction templates(text, data) {\n    if (isEmpty(text)) { return \"\"; }\n    // fullFileName\n    try {\n        data.fullFileName = new URL(data.url).pathname.split(\"/\").pop();\n    } catch (e) {\n        data.fullFileName = 'NULL';\n    }\n    // fileName\n    data.fileName = data.fullFileName.split(\".\");\n    data.fileName.length > 1 && data.fileName.pop();\n    data.fileName = data.fileName.join(\".\");\n    // ext\n    if (isEmpty(data.ext)) {\n        data.ext = data.fullFileName.split(\".\");\n        data.ext = data.ext.length == 1 ? \"\" : data.ext[data.ext.length - 1];\n    }\n    const date = new Date();\n    const trimData = {\n        // 资源信息\n        url: data.url ?? \"\",\n        referer: data.requestHeaders?.referer ?? \"\",\n        origin: data.requestHeaders?.origin ?? \"\",\n        initiator: data.requestHeaders?.referer ? data.requestHeaders.referer : data.initiator,\n        webUrl: data.webUrl ?? \"\",\n        title: data._title ?? data.title,\n        pageDOM: data.pageDOM,\n        cookie: data.cookie ?? \"\",\n        tabId: data.tabId ?? 0,\n\n        // 时间相关\n        year: date.getFullYear(),\n        month: appendZero(date.getMonth() + 1),\n        date: appendZero(date.getDate()),\n        day: [\"Sunday\", \"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\"][date.getDay()],\n        fullDate: `${date.getFullYear()}-${appendZero(date.getMonth() + 1)}-${appendZero(date.getDate())}`,\n        time: `${appendZero(date.getHours())}'${appendZero(date.getMinutes())}'${appendZero(date.getSeconds())}`,\n        hours: appendZero(date.getHours()),\n        minutes: appendZero(date.getMinutes()),\n        seconds: appendZero(date.getSeconds()),\n        now: Date.now(),\n        timestamp: new Date().toISOString(),\n\n        // 文件名\n        fullFileName: data.fullFileName ? data.fullFileName : \"\",\n        fileName: data.fileName ? data.fileName : \"\",\n        ext: data.ext ?? \"\",\n\n        // 全局变量\n        mobileUserAgent: G.MobileUserAgent,\n        userAgent: G.userAgent ? G.userAgent : navigator.userAgent,\n    }\n    const _data = { ...data, ...trimData };\n    text = text.replace(reTemplates, function (original, tag, action) {\n        tag = tag.trim();\n        // 特殊标签 data 返回所有数据\n        if (tag == 'data') { return JSON.stringify(trimData); }\n        if (action) {\n            return templatesFunction(_data[tag], action.trim(), _data);\n        }\n        return _data[tag] ?? original;\n    });\n\n    return text;\n}\n\n/**\n * 从url中获取文件名\n * @param {String} url \n * @returns {String} 文件名\n */\nfunction getUrlFileName(url) {\n    let pathname = new URL(url).pathname;\n    let filename = pathname.split(\"/\").pop();\n    return filename ? filename : \"NULL\";\n}\n\n/**\n * 解析json字符串 尝试修复键名没有双引号 解析错误返回默认值\n * @param {string} str json字符串\n * @param {object} error 解析错误返回的默认值\n * @param {number} attempt 尝试修复次数\n * @returns {object} 返回解析后的对象\n */\nfunction JSONparse(str, error = {}, attempt = 0) {\n    if (!str) { return error; }\n    try {\n        return JSON.parse(str);\n    } catch (e) {\n        if (attempt === 0) {\n            // 第一次解析失败，修正字符串后递归调用\n            reJSONparse.lastIndex = 0;\n            const fixedStr = str.replace(reJSONparse, '$1\"$2\"$3');\n            return JSONparse(fixedStr, error, ++attempt);\n        } else {\n            // 第二次解析仍然失败，返回 error 对象\n            return error;\n        }\n    }\n}\n\n/**\n * ArrayBuffer转Blob 大于2G的做切割\n * @param {ArrayBuffer|Uint8Array} buffer 原始数据\n * @param {Object} options Blob配置\n * @returns {Blob} 返回Blob对象\n */\nfunction ArrayBufferToBlob(buffer, options = {}) {\n    if (buffer instanceof Blob) {\n        return buffer;\n    }\n    if (buffer instanceof Uint8Array) {\n        buffer = buffer.buffer;\n    }\n    if (!buffer.byteLength) {\n        return new Blob();\n    }\n    if (!buffer instanceof ArrayBuffer) {\n        return new Blob();\n    }\n    if (buffer.byteLength >= 2 * 1024 * 1024 * 1024) {\n        const MAX_CHUNK_SIZE = 1024 * 1024 * 1024;\n        let offset = 0;\n        const blobs = [];\n        while (offset < buffer.byteLength) {\n            const chunkSize = Math.min(MAX_CHUNK_SIZE, buffer.byteLength - offset);\n            const chunk = buffer.slice(offset, offset + chunkSize);\n            blobs.push(new Blob([chunk]));\n            offset += chunkSize;\n        }\n        return new Blob(blobs, options);\n    }\n    return new Blob([buffer], options);\n}\n\n\n/**\n * 清理冗余数据\n */\nfunction clearRedundant() {\n    chrome.tabs.query({}, function (tabs) {\n        const allTabId = new Set(tabs.map(tab => tab.id));\n\n        if (!cacheData.init) {\n            // 清理 缓存数据\n            let cacheDataFlag = false;\n            for (let key in cacheData) {\n                if (!allTabId.has(Number(key))) {\n                    cacheDataFlag = true;\n                    delete cacheData[key];\n                }\n            }\n            cacheDataFlag && (chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData });\n        }\n\n        // 清理\n        G.urlMap.forEach((_, key) => {\n            !allTabId.has(key) && G.urlMap.delete(key);\n        });\n\n        // 清理脚本\n        G.scriptList.forEach(function (scriptList) {\n            scriptList.tabId.forEach(function (tabId) {\n                if (!allTabId.has(tabId)) {\n                    scriptList.tabId.delete(tabId);\n                }\n            });\n        });\n\n        if (!G.initLocalComplete) { return; }\n\n        // 清理 declarativeNetRequest 模拟手机\n        chrome.declarativeNetRequest.getSessionRules(function (rules) {\n            let mobileFlag = false;\n            for (let item of rules) {\n                if (item.condition.tabIds) {\n                    // 如果tabIds列表都不存在 则删除该条规则\n                    if (!item.condition.tabIds.some(id => allTabId.has(id))) {\n                        mobileFlag = true;\n                        item.condition.tabIds.forEach(id => G.featMobileTabId.delete(id));\n                        chrome.declarativeNetRequest.updateSessionRules({\n                            removeRuleIds: [item.id]\n                        });\n                    }\n                } else if (item.id == 1) {\n                    // 清理预览视频增加的请求头\n                    chrome.declarativeNetRequest.updateSessionRules({ removeRuleIds: [1] });\n                }\n            }\n            mobileFlag && (chrome.storage.session ?? chrome.storage.local).set({ featMobileTabId: Array.from(G.featMobileTabId) });\n        });\n        // 清理自动下载\n        let autoDownFlag = false;\n        G.featAutoDownTabId.forEach(function (tabId) {\n            if (!allTabId.has(tabId)) {\n                autoDownFlag = true;\n                G.featAutoDownTabId.delete(tabId);\n            }\n        });\n        autoDownFlag && (chrome.storage.session ?? chrome.storage.local).set({ featAutoDownTabId: Array.from(G.featAutoDownTabId) });\n\n        G.blockUrlSet = new Set([...G.blockUrlSet].filter(x => allTabId.has(x)));\n        G.damnUrlSet = new Set([...G.damnUrlSet].filter(x => allTabId.has(x)));\n\n        if (G.requestHeaders.size >= 10240) {\n            G.requestHeaders.clear();\n        }\n    });\n    // G.referer.clear();\n    // G.blackList.clear();\n    // G.temp.clear();\n}\n\n/**\n * 替换掉文件名中的特殊字符 包含路径\n * @param {String} str 需要处理的文本\n * @param {String} text 需要替换的文本\n * @returns {String} 返回替换后的字符串\n */\nfunction stringModify(str, text) {\n    if (!str) { return str; }\n    str = filterFileName(str, text);\n    return str.replaceAll(\"\\\\\", \"&bsol;\").replaceAll(\"/\", \"&sol;\");\n}\n\n/**\n * 替换掉文件名中的特殊字符 不包含路径\n * @param {String} str 需要处理的文本\n * @param {String} text 需要替换的文本\n * @returns {String} 返回替换后的字符串\n */\nfunction filterFileName(str, text) {\n    if (!str) { return str; }\n    reFilterFileName.lastIndex = 0;\n    str = str.replaceAll(/\\u200B/g, \"\").replaceAll(/\\u200C/g, \"\").replaceAll(/\\u200D/g, \"\");\n    str = str.replace(reFilterFileName, function (match) {\n        return text || {\n            '<': '&lt;',\n            '>': '&gt;',\n            ':': '&colon;',\n            '\"': '&quot;',\n            '|': '&vert;',\n            '?': '&quest;',\n            '*': '&ast;',\n            '~': '_'\n        }[match];\n    });\n\n    // 前后不能是 “.”\n    if (str.endsWith(\".\")) {\n        str = str + \"catCatch\";\n    }\n    if (str.startsWith(\".\")) {\n        str = \"catCatch\" + str;\n    }\n    return str;\n}\n\n/**\n * 展平嵌套对象的函数\n * @param {Object} obj 参数对象\n * @param {String} prefix 前缀\n * @returns 嵌套对象扁平化\n */\nfunction flattenObject(obj, prefix = '') {\n    let result = {};\n    for (const key in obj) {\n        if (Object.prototype.hasOwnProperty.call(obj, key)) {\n            const value = obj[key];\n            const newKey = prefix ? `${prefix}[${key}]` : key;\n            if (typeof value === 'object' && value !== null && !Array.isArray(value)) {\n                // 递归处理嵌套对象\n                Object.assign(result, flattenObject(value, newKey));\n            } else {\n                // 处理基本类型和数组\n                result[newKey] = value;\n            }\n        }\n    }\n    return result;\n}\n\n/**\n * 发送数据到本地\n * @param {String} action 发送类型\n * @param {Object|Srting} data 发送的数据\n * @param {Number} tabId 发送数据的标签页ID\n */\nfunction send2local(action, data, tabId = 0) {\n    return new Promise((resolve, reject) => {\n\n        // 请求方式\n        const option = { method: G.send2localMethod };\n\n        // 处理替换模板\n        let body = G.send2localBody;\n        // 处理 addKey 请求\n        if (action == 'addKey' || typeof data === 'string') {\n            body = G.send2localBody.replaceAll('${data}', `\"${data}\"`);\n            data = { tabId: tabId };\n        }\n\n        data.action = action;\n        let postData = templates(body, data);\n\n        // 转为对象\n        postData = JSONparse(postData, { action, data, tabId });\n\n        try {\n            // 处理URL中的模板字符串并检查合法性\n            let send2localURL = templates(G.send2localURL, data);\n            send2localURL = new URL(send2localURL);\n\n            // GET请求拼接参数\n            if (option.method === 'GET') {\n                const flattenedObj = flattenObject(postData);\n                const urlParams = new URLSearchParams(flattenedObj);\n                send2localURL.search = send2localURL.search\n                    ? `${send2localURL.search}&${urlParams}`\n                    : `?${urlParams}`;\n            }\n            // 非GET请求处理不同Content-Type\n            else {\n                const contentType = {\n                    0: 'application/json;charset=utf-8',\n                    1: 'multipart/form-data',\n                    2: 'application/x-www-form-urlencoded',\n                    3: 'text/plain'\n                }[G.send2localType];\n                option.headers = { 'Content-Type': contentType };\n\n                switch (contentType) {\n                    case 'application/json;charset=utf-8':\n                        option.body = JSON.stringify(postData);\n                        break;\n                    case 'multipart/form-data':\n                        const formData = new FormData();\n                        const flattened = flattenObject(postData);\n                        Object.entries(flattened).forEach(([key, value]) => {\n                            formData.append(key, value);\n                        });\n                        option.body = formData;\n                        delete option.headers['Content-Type']; // 浏览器自动生成boundary\n                        break;\n                    case 'application/x-www-form-urlencoded':\n                        const flattenedObj = flattenObject(postData);\n                        const urlParams = new URLSearchParams(flattenedObj);\n                        option.body = urlParams.toString();\n                        break;\n                    case 'text/plain':\n                        option.body = typeof postData === 'object'\n                            ? JSON.stringify(postData)\n                            : String(postData);\n                        break;\n                    default:\n                        option.body = JSON.stringify(postData);\n                        break;\n                }\n            }\n\n            send2localURL = send2localURL.toString();\n            fetch(send2localURL, option)\n                .then(response => resolve(response))\n                .catch(error => reject(error));\n        } catch (e) {\n            reject(e);\n        }\n    });\n}\n\nfunction isDamnUrl(url) {\n    for (let key in G.damnUrl) {\n        G.damnUrl[key].lastIndex = 0;\n        if (G.damnUrl[key].test(url)) {\n            return true;\n        }\n    }\n    return false;\n}\n\n/**\n * 判断url是否在屏蔽网址中\n * @param {String} url \n * @returns {Boolean}\n */\nfunction isLockUrl(url) {\n    for (let key in G.blockUrl) {\n        if (!G.blockUrl[key].state) { continue; }\n        G.blockUrl[key].url.lastIndex = 0;\n        if (G.blockUrl[key].url.test(url)) {\n            return true;\n        }\n    }\n    return false;\n}\n\n/**\n * 关闭标签页 如果tabId为0 则关闭当前标签\n * 当前只有一个标签页面 创建新标签页 再关闭\n * @param {Number|Array} tabId \n */\nfunction closeTab(tabId = 0) {\n    chrome.tabs.query({}, async function (tabs) {\n        if (tabs.length === 1) {\n            await chrome.tabs.create({ url: 'chrome://newtab' });\n            tabId ? chrome.tabs.remove(tabId) : window.close();\n        } else {\n            tabId ? chrome.tabs.remove(tabId) : window.close();\n        }\n    });\n}\n\n/**\n * 打开解析器\n * @param {Object} data 资源对象\n * @param {Object} options 选项\n */\nfunction openParser(data, options = {}) {\n    chrome.tabs.get(G.tabId, function (tab) {\n        const url = `/${data.parsing ? data.parsing : \"m3u8\"}.html?${new URLSearchParams({\n            url: data.url,\n            title: data.title,\n            filename: data.downFileName,\n            tabid: data.tabId == -1 ? G.tabId : data.tabId,\n            initiator: data.initiator,\n            requestHeaders: data.requestHeaders ? JSON.stringify(data.requestHeaders) : undefined,\n            ...Object.fromEntries(Object.entries(options).map(([key, value]) => [key, typeof value === 'boolean' ? 1 : value])),\n        })}`\n        chrome.tabs.create({\n            url: url,\n            index: tab.index + 1,\n            active: G.isMobile || !options.autoDown\n        });\n    });\n}\n/**\n * 加载CSS样式\n */\nfunction loadCSS() {\n    if (G.isMobile) {\n        const mobileCssLink = document.createElement('link');\n        mobileCssLink.rel = 'stylesheet';\n        mobileCssLink.type = 'text/css';\n        mobileCssLink.href = 'css/mobile.css';\n        document.head.appendChild(mobileCssLink);\n    }\n    const styleElement = document.createElement('style');\n    styleElement.textContent = G.css;\n    document.head.appendChild(styleElement);\n}\n\n/**\n * 修建数据 不发送不必要的数据\n * @param {Object} originalData 原始数据\n * @returns {Object} 返回处理后的数据\n */\nfunction trimData(originalData) {\n    const data = { ...originalData };\n    // 不发送HTML内容\n    data.html = undefined;\n    data.panelHeading = undefined;\n    data.urlPanel = undefined;\n    data.urlPanelShow = undefined;\n    return data;\n}"
  },
  {
    "path": "js/i18n.js",
    "content": "(function () {\n    document.querySelectorAll('[data-i18n]').forEach(function (element) {\n        element.innerHTML = i18n(element.dataset.i18n) ?? element.dataset.i18n;\n    });\n    document.querySelectorAll('[data-i18n-outer]').forEach(function (element) {\n        element.outerHTML = i18n(element.dataset.i18nOuter) ?? element.dataset.i18nOuter;\n    });\n    document.querySelectorAll('i18n').forEach(function (element) {\n        element.outerHTML = i18n(element.innerHTML) ?? element.innerHTML;\n    });\n    document.querySelectorAll('[data-i18n-placeholder]').forEach(function (element) {\n        element.setAttribute('placeholder', i18n(element.dataset.i18nPlaceholder) ?? element.dataset.i18nPlaceholder);\n    });\n    document.title = i18n(document.title) ?? document.title;\n})();"
  },
  {
    "path": "js/init.js",
    "content": "// 低版本chrome manifest v3协议 会有 getMessage 函数不存在的bug\nif (chrome.i18n.getMessage === undefined) {\n    chrome.i18n.getMessage = (key) => key;\n    fetch(chrome.runtime.getURL(\"_locales/zh_CN/messages.json\")).then(res => res.json()).then(data => {\n        chrome.i18n.getMessage = (key) => data[key].messages;\n    }).catch((e) => { console.error(e); });\n}\n/**\n * 部分修改版chrome 不存在 chrome.downloads API\n * 例如 夸克浏览器\n * 使用传统下载方式下载 但无法监听 无法另存为 无法判断下载是否失败 唉~\n */\nif (!chrome.downloads) {\n    chrome.downloads = {\n        download: function (options, callback) {\n            let a = document.createElement('a');\n            a.href = options.url;\n            a.download = options.filename;\n            a.click();\n            a = null;\n            callback && callback();\n        },\n        onChanged: { addListener: function () { } },\n        showDefaultFolder: function () { },\n        show: function () { },\n    }\n}\n// 兼容 114版本以下没有chrome.sidePanel\nif (!chrome.sidePanel || !chrome.sidePanel.setPanelBehavior) {\n    chrome.sidePanel = {\n        setOptions: function (options) { },\n        setPanelBehavior: function (options) { },\n    }\n}\n\n// 简写翻译函数\nconst i18n = new Proxy(chrome.i18n.getMessage, {\n    get: function (target, key) {\n        return chrome.i18n.getMessage(key);\n    }\n});\n// 全局变量\nvar G = {};\nG.initSyncComplete = false;\nG.initLocalComplete = false;\n// 缓存数据\nvar cacheData = { init: true };\nG.blackList = new Set();    // 正则屏蔽资源列表\nG.blockUrlSet = new Set();    // 屏蔽网址列表\nG.requestHeaders = new Map();   // 临时储存请求头\nG.urlMap = new Map();   // url查重map\nG.deepSearchTemporarilyClose = null; // 深度搜索临时变量\n\n// 避免抓取列表\nG.damnUrl = [\n    /^https:\\/\\/.*\\.douyin\\.com\\/.*$/i,\n];\nG.damnUrlSet = new Set();\n\n// 初始化当前tabId\nchrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {\n    if (tabs[0] && tabs[0].id) {\n        G.tabId = tabs[0].id;\n    } else {\n        G.tabId = -1;\n    }\n});\n\n// 手机浏览器\nG.isMobile = /Mobile|Android|iPhone|iPad/i.test(navigator.userAgent);\n\n// 所有设置变量 默认值\nG.OptionLists = {\n    Ext: [\n        { \"ext\": \"flv\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"hlv\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"f4v\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"mp4\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"mp3\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"wma\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"wav\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"m4a\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"ts\", \"size\": 0, \"operator\": \">=\", \"state\": false },\n        { \"ext\": \"webm\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"ogg\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"ogv\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"acc\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"mov\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"mkv\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"m4s\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"m3u8\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"m3u\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"mpeg\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"avi\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"wmv\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"asf\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"movie\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"divx\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"mpeg4\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"vid\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"aac\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"mpd\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"weba\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"opus\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"ext\": \"srt\", \"size\": 0, \"operator\": \">=\", \"state\": false },\n        { \"ext\": \"vtt\", \"size\": 0, \"operator\": \">=\", \"state\": false },\n    ],\n    Type: [\n        { \"type\": \"audio/*\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"type\": \"video/*\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"type\": \"application/ogg\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"type\": \"application/vnd.apple.mpegurl\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"type\": \"application/x-mpegurl\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"type\": \"application/mpegurl\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"type\": \"application/octet-stream-m3u8\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"type\": \"application/dash+xml\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n        { \"type\": \"application/m4s\", \"size\": 0, \"operator\": \">=\", \"state\": true },\n    ],\n    Regex: [\n        { \"type\": \"ig\", \"regex\": \"https://cache\\\\.video\\\\.[a-z]*\\\\.com/dash\\\\?tvid=.*\", \"ext\": \"json\", \"state\": false },\n        { \"type\": \"ig\", \"regex\": \".*\\\\.bilivideo\\\\.(com|cn).*\\\\/live-bvc\\\\/.*m4s\", \"ext\": \"\", \"blackList\": true, \"state\": false },\n        { \"type\": \"ig\", \"regex\": \"(^https://scontent[a-z0-9-]*\\\\.cdninstagram\\\\.com/.*)&bytestart=.*\", \"ext\": \"\", \"blackList\": false, \"state\": false },\n        { \"type\": \"ig\", \"regex\": \"(^https://.*\\\\.fbcdn\\\\.net/.*)&bytestart=.*\", \"ext\": \"\", \"blackList\": false, \"state\": false },\n    ],\n    TitleName: false,\n    Player: \"\",\n    ShowWebIco: !G.isMobile,\n    MobileUserAgent: \"Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1\",\n    m3u8dl: 0,\n    m3u8dlArg: `\"\\${url}\" --save-dir \"%USERPROFILE%\\\\Downloads\\\\m3u8dl\" --save-name \"\\${title}_\\${now}\" \\${referer|exists:'-H \"Referer:*\"'} \\${cookie|exists:'-H \"Cookie:*\"'} --no-log`,\n    m3u8dlConfirm: false,\n    playbackRate: 2,\n    copyM3U8: \"${url}\",\n    copyMPD: \"${url}\",\n    copyOther: \"${url}\",\n    autoClearMode: 1,\n    catDownload: false,\n    saveAs: false,\n    userAgent: \"\",\n    downFileName: \"${title}.${ext}\",\n    css: \"\",\n    checkDuplicates: true,\n    enable: true,\n    downActive: !G.isMobile,    // 手机端默认不启用 后台下载\n    downAutoClose: true,\n    downStream: false,\n    aria2Rpc: \"http://localhost:6800/jsonrpc\",\n    enableAria2Rpc: false,\n    enableAria2RpcReferer: true,\n    aria2RpcToken: \"\",\n    m3u8AutoDown: true,\n    badgeNumber: true,\n    send2local: false,\n    send2localManual: false,\n    send2localURL: \"http://127.0.0.1:8000/\",\n    send2localMethod: 'POST',\n    send2localBody: '{\"action\": \"${action}\", \"data\": ${data}, \"tabId\": \"${tabId}\"}',\n    send2localType: 0,\n    popup: false,\n    popupMode: 0, // 0:preview.html 1:popup.html 2:window preview.html 3: window popup.html\n    invoke: false,\n    invokeText: `m3u8dlre:\"\\${url}\" --save-dir \"%USERPROFILE%\\\\Downloads\" --del-after-done --save-name \"\\${title}_\\${now}\" --auto-select \\${referer|exists:'-H \"Referer: *\"'}`,\n    invokeConfirm: false,\n    // m3u8解析器默认参数\n    M3u8Thread: 6,\n    M3u8Mp4: false,\n    M3u8OnlyAudio: false,\n    M3u8SkipDecrypt: false,\n    M3u8StreamSaver: false,\n    M3u8Ffmpeg: true,\n    M3u8AutoClose: false,\n    // 第三方服务地址\n    onlineServiceAddress: 0,\n    chromeLimitSize: 1.8 * 1024 * 1024 * 1024,\n    blockUrl: [],\n    blockUrlWhite: false,\n    maxLength: G.isMobile ? 999 : 9999,\n    sidePanel: false,   // 侧边栏\n    deepSearch: false, // 常开深度搜索\n    // MQTT 配置\n    send2MQTT: false,\n    mqttEnable: false,\n    mqttBroker: \"test.mosquitto.org\",\n    mqttPort: 8081,\n    mqttPath: \"/mqtt\",\n    mqttProtocol: \"wss\",\n    mqttClientId: \"cat-catch-client\",\n    mqttUser: \"\",\n    mqttPassword: \"\",\n    mqttTopic: \"cat-catch/media\",\n    mqttQos: 0,\n    mqttTitleLength: 100,\n    mqttDataFormat: \"\",\n    getHtmlDOM: false,\n    damn: false\n};\n\n// 本地储存的配置\nG.LocalVar = {\n    featMobileTabId: [],\n    featAutoDownTabId: [],\n    mediaControl: { tabid: 0, index: -1 },\n    previewShowTitle: false, // 是否显示标题\n    previewDeleteDuplicateFilenames: false, // 是否删除重复文件名\n};\n\n// 102版本以上 非Firefox 开启更多功能\nG.isFirefox = navigator.userAgent.includes('Firefox') && (typeof browser !== 'undefined' && !!browser.runtime?.getBrowserInfo);\nG.version = navigator.userAgent.match(/(Chrome|Firefox)\\/([\\d]+)/);\nG.version = G.version && G.version[2] ? parseInt(G.version[2]) : 93;\n\n// 脚本列表\nG.scriptList = new Map();\nG.scriptList.set(\"search.js\", { key: \"search\", refresh: true, allFrames: true, world: \"MAIN\", name: i18n.deepSearch, off: i18n.closeSearch, i18n: false, tabId: new Set() });\nG.scriptList.set(\"catch.js\", { key: \"catch\", refresh: true, allFrames: true, world: \"MAIN\", name: i18n.cacheCapture, off: i18n.closeCapture, i18n: true, tabId: new Set() });\nG.scriptList.set(\"recorder.js\", { key: \"recorder\", refresh: false, allFrames: true, world: \"MAIN\", name: i18n.videoRecording, off: i18n.closeRecording, i18n: true, tabId: new Set() });\nG.scriptList.set(\"recorder2.js\", { key: \"recorder2\", refresh: false, allFrames: false, world: \"ISOLATED\", name: i18n.screenCapture, off: i18n.closeCapture, i18n: true, tabId: new Set() });\nG.scriptList.set(\"webrtc.js\", { key: \"webrtc\", refresh: true, allFrames: true, world: \"MAIN\", name: i18n.recordWebRTC, off: i18n.closeRecording, i18n: true, tabId: new Set() });\n\n// ffmpeg\nG.ffmpegConfig = {\n    tab: 0,\n    cacheData: [],\n    version: 1,\n    get url() {\n        return G.onlineServiceAddress == 0 ? \"https://ffmpeg.bmmmd.com/\" : \"https://ffmpeg.94cat.com/\";\n    }\n}\n// streamSaver 边下边存\nG.streamSaverConfig = {\n    get url() {\n        return G.onlineServiceAddress == 0 ? \"https://stream.bmmmd.com/mitm.html\" : \"https://ffmpeg.94cat.com/mitm.html\";\n    }\n}\n\n// 正则预编译\nconst reFilename = /filename=\"?([^\"]+)\"?/;\nconst reStringModify = /[<>:\"\\/\\\\|?*~]/g;\nconst reFilterFileName = /[<>:\"|?*~]/g;\nconst reTemplates = /\\${([^}|]+)(?:\\|([^}]+))?}/g;\nconst reJSONparse = /([{,]\\s*)([\\w-]+)(\\s*:)/g;\n\n// 防抖\nlet debounce = undefined;\nlet debounceCount = 0;\nlet debounceTime = 0;\n\n// Init\nInitOptions();\n\n// 初始变量\nfunction InitOptions() {\n    // 断开重新连接后 立刻把local里MediaData数据交给cacheData\n    (chrome.storage.session ?? chrome.storage.local).get({ MediaData: {} }, function (items) {\n        if (items.MediaData.init) {\n            cacheData = {};\n            return;\n        }\n        cacheData = items.MediaData;\n    });\n    // 读取sync配置数据 交给全局变量G\n    chrome.storage.sync.get(G.OptionLists, function (items) {\n        if (chrome.runtime.lastError) {\n            items = G.OptionLists;\n        }\n        // 确保有默认值\n        for (let key in G.OptionLists) {\n            if (items[key] === undefined || items[key] === null) {\n                items[key] = G.OptionLists[key];\n            }\n        }\n        // Ext的Array转为Map类型 如果是范围 增加min max属性\n        items.Ext = new Map(items.Ext.map(item => {\n            if (item.operator === undefined) { item.operator = \">=\"; }\n            if (item.operator === \"~\") {\n                const [min, max] = item.size.split(\"-\");\n                item.min = min ? parseInt(min) : 0;\n                item.max = max ? parseInt(max) : 0;\n            }\n            return [item.ext, item];\n        }));\n        // Type的Array转为Map类型 如果是范围 增加min max属性\n        items.Type = new Map(items.Type.map(item => {\n            if (item.operator === undefined) { item.operator = \">=\"; }\n            if (item.operator === \"~\") {\n                const [min, max] = item.size.split(\"-\");\n                item.min = min ? parseInt(min) : 0;\n                item.max = max ? parseInt(max) : 0;\n            }\n            return [item.type, item];\n        }));\n        // 预编译正则匹配\n        items.Regex = items.Regex.map(item => {\n            let reg = undefined;\n            try { reg = new RegExp(item.regex, item.type) } catch (e) { item.state = false; }\n            return { regex: reg, ext: item.ext, blackList: item.blackList, state: item.state }\n        });\n        // 预编译屏蔽通配符\n        items.blockUrl = items.blockUrl.map(item => {\n            return { url: wildcardToRegex(item.url), state: item.state }\n        });\n\n        // 兼容旧配置\n        if (items.copyM3U8.includes('$url$')) {\n            items.copyM3U8 = items.copyM3U8.replaceAll('$url$', '${url}').replaceAll('$referer$', '${referer}').replaceAll('$title$', '${title}');\n            chrome.storage.sync.set({ copyM3U8: items.copyM3U8 });\n        }\n        if (items.copyMPD.includes('$url$')) {\n            items.copyMPD = items.copyMPD.replaceAll('$url$', '${url}').replaceAll('$referer$', '${referer}').replaceAll('$title$', '${title}');\n            chrome.storage.sync.set({ copyMPD: items.copyMPD });\n        }\n        if (items.copyOther.includes('$url$')) {\n            items.copyOther = items.copyOther.replaceAll('$url$', '${url}').replaceAll('$referer$', '${referer}').replaceAll('$title$', '${title}');\n            chrome.storage.sync.set({ copyOther: items.copyOther });\n        }\n        if (typeof items.m3u8dl == 'boolean') {\n            items.m3u8dl = items.m3u8dl ? 1 : 0;\n            chrome.storage.sync.set({ m3u8dl: items.m3u8dl });\n        }\n\n        // 侧边栏\n        chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: items.sidePanel });\n\n        G = { ...items, ...G };\n\n        // 初始化 G.blockUrlSet\n        (typeof isLockUrl == 'function') && chrome.tabs.query({}, function (tabs) {\n            for (const tab of tabs) {\n                if (tab.url) {\n                    isLockUrl(tab.url) && G.blockUrlSet.add(tab.id);\n                    isDamnUrl(tab.url) && G.damnUrlSet.add(tab.id);\n                }\n            }\n        });\n\n        chrome.action.setIcon({ path: G.enable ? \"/img/icon.png\" : \"/img/icon-disable.png\" });\n        G.initSyncComplete = true;\n    });\n    // 读取local配置数据 交给全局变量G\n    (chrome.storage.session ?? chrome.storage.local).get(G.LocalVar, function (items) {\n        items.featMobileTabId = new Set(items.featMobileTabId);\n        items.featAutoDownTabId = new Set(items.featAutoDownTabId);\n        G = { ...items, ...G };\n        G.initLocalComplete = true;\n    });\n}\n// 监听变化，新值给全局变量\nchrome.storage.onChanged.addListener(function (changes, namespace) {\n    if (changes.MediaData) {\n        if (changes.MediaData.newValue?.init) { cacheData = {}; }\n        return;\n    }\n    for (let [key, { oldValue, newValue }] of Object.entries(changes)) {\n        newValue ??= G.OptionLists[key];\n        if (key == \"Ext\") {\n            G.Ext = new Map(newValue.map(item => {\n                if (item.operator === \"~\") {\n                    const [min, max] = item.size.split(\"-\");\n                    item.min = min ? parseInt(min) : 0;\n                    item.max = max ? parseInt(max) : 0;\n                }\n                return [item.ext, item];\n            }));\n            continue;\n        }\n        if (key == \"Type\") {\n            G.Type = new Map(newValue.map(item => {\n                if (item.operator === \"~\") {\n                    const [min, max] = item.size.split(\"-\");\n                    item.min = min ? parseInt(min) : 0;\n                    item.max = max ? parseInt(max) : 0;\n                }\n                return [item.type, item];\n            }));\n            continue;\n        }\n        if (key == \"Regex\") {\n            G.Regex = newValue.map(item => {\n                let reg = undefined;\n                try { reg = new RegExp(item.regex, item.type) } catch (e) { item.state = false; }\n                return { regex: reg, ext: item.ext, blackList: item.blackList, state: item.state }\n            });\n            continue;\n        }\n        if (key == \"blockUrl\") {\n            G.blockUrl = newValue.map(item => {\n                return { url: wildcardToRegex(item.url), state: item.state }\n            });\n            continue;\n        }\n        if (key == \"featMobileTabId\" || key == \"featAutoDownTabId\") {\n            G[key] = new Set(newValue);\n            continue;\n        }\n        if (key == \"sidePanel\" && !G.isFirefox) {\n            chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: newValue });\n            continue;\n        }\n        G[key] = newValue;\n    }\n});\n\n// 扩展升级，清空本地储存\nchrome.runtime.onInstalled.addListener(function (details) {\n    if (details.reason == \"update\") {\n        chrome.storage.local.clear(function () {\n            if (chrome.storage.session) {\n                chrome.storage.session.clear(InitOptions);\n            } else {\n                InitOptions();\n            }\n        });\n        chrome.alarms.create(\"nowClear\", { when: Date.now() + 3000 });\n    }\n    if (details.reason == \"install\") {\n        chrome.tabs.create({ url: \"install.html\" });\n    }\n});\n\n/**\n * 将用户输入的URL（可能包含通配符）转换为正则表达式\n * @param {string} urlPattern - 用户输入的URL，可能包含通配符\n * @returns {RegExp} - 转换后的正则表达式\n */\nfunction wildcardToRegex(urlPattern) {\n    // 将通配符 * 转换为正则表达式的 .*\n    // 将通配符 ? 转换为正则表达式的 .\n    // 同时转义其他正则表达式特殊字符\n    const regexPattern = urlPattern\n        .replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&') // 转义正则表达式特殊字符\n        .replace(/\\*/g, '.*') // 将 * 替换为 .*\n        .replace(/\\?/g, '.'); // 将 ? 替换为 .\n\n    // 创建正则表达式，确保匹配整个URL\n    return new RegExp(`^${regexPattern}$`, 'i'); // 忽略大小写\n}"
  },
  {
    "path": "js/install.js",
    "content": "window.addEventListener('DOMContentLoaded', function () {\n    // 多语言支持\n    let currentLang = 'zh';\n\n    function setLanguage(lang) {\n        currentLang = lang;\n\n        // 设置活动语言\n        document.querySelectorAll('.lang-zh').forEach(el => {\n            el.classList.toggle('active', lang === 'zh');\n        });\n        document.querySelectorAll('.lang-en').forEach(el => {\n            el.classList.toggle('active', lang === 'en');\n        });\n\n        // 更新语言切换按钮\n        document.getElementById('langText').textContent = lang === 'zh' ? 'English' : '中文';\n        document.getElementById('langEmoji').textContent = lang === 'zh' ? '🌐' : '🇨🇳';\n\n        // 更新动态文本\n        if (lang === 'en') {\n            document.getElementById('main-title').textContent = 'Installation Successful!';\n            document.getElementById('subtitle').textContent = 'Cat Catch Extension is now installed';\n            document.getElementById('welcome-title').textContent = 'Welcome to Cat Catch';\n            document.getElementById('privacy-title').textContent = 'Privacy Policy';\n            document.getElementById('disclaimer-title').textContent = 'Disclaimer';\n            document.getElementById('issue-title').textContent = 'Issue Submission';\n            document.getElementById('agreeText').textContent = 'Agree';\n            document.getElementById('uninstallText').textContent = 'Uninstall Extension';\n        } else {\n            document.getElementById('main-title').textContent = '恭喜 猫抓 扩展已成功安装 !';\n            document.getElementById('subtitle').textContent = 'Installation successful !';\n            document.getElementById('welcome-title').textContent = '希望本扩展能帮助到你';\n            document.getElementById('privacy-title').textContent = '隐私政策 / Privacy Policy';\n            document.getElementById('disclaimer-title').textContent = '免责声明 / Disclaimer';\n            document.getElementById('issue-title').textContent = '问题提交 / Issue Submission';\n            document.getElementById('agreeText').textContent = '同意';\n            document.getElementById('uninstallText').textContent = '卸载扩展';\n        }\n    }\n\n    // 语言切换功能\n    document.getElementById('langToggle').addEventListener('click', function () {\n        const newLang = currentLang === 'zh' ? 'en' : 'zh';\n        setLanguage(newLang);\n    });\n\n    // 按钮事件处理\n    document.getElementById('agreeBtn').addEventListener('click', function () {\n        closeTab();\n    });\n\n    document.getElementById('uninstallBtn').addEventListener('click', function () {\n        chrome.management.uninstallSelf({ showConfirmDialog: true });\n    });\n\n    const lang = navigator.language || navigator.userLanguage;\n    const isChinese = lang.startsWith('zh');\n    setLanguage(isChinese ? 'zh' : 'en');\n\n    // 添加动画效果\n    document.querySelector('.card').classList.add('fade-in');\n    document.getElementById('agreeBtn').focus();\n});"
  },
  {
    "path": "js/json.js",
    "content": "// url 参数解析\nconst params = new URL(location.href).searchParams;\nvar _url = params.get(\"url\");\n// const _referer = params.get(\"referer\");\nconst _requestHeaders = params.get(\"requestHeaders\");\n\n// 修改当前标签下的所有xhr的requestHeaders\nlet requestHeaders = JSONparse(_requestHeaders);\nsetRequestHeaders(requestHeaders, () => { awaitG(init); });\n\nfunction init() {\n    loadCSS();\n    var jsonContent = \"\";\n    var options = {\n        collapsed: true,\n        rootCollapsable: false,\n        withQuotes: false,\n        withLinks: true\n    };\n\n    if (isEmpty(_url)) {\n        $(\"#jsonCustom\").show(); $(\"#main\").hide();\n        $(\"#format\").click(function () {\n            _url = $(\"#jsonUrl\").val().trim();\n            if (isEmpty(_url)) {\n                let jsonText = $(\"#jsonText\").val();\n                jsonContent = JSON.parse(jsonText);\n                renderJson();\n                $(\"#jsonCustom\").hide(); $(\"#main\").show();\n                return;\n            }\n            getJson(_url);\n        });\n    } else {\n        getJson(_url);\n    }\n\n    function getJson(url) {\n        $(\"#jsonCustom\").hide(); $(\"#main\").show();\n        $.ajax({\n            url: url,\n            dataType: \"text\",\n        }).fail(function (result) {\n            console.log(result);\n            $('#json-renderer').html(i18n.fileRetrievalFailed);\n            $(\"#collapsed\").hide();\n        }).done(function (result) {\n            // console.log(result);\n            result = result.replace(/^try{/, \"\").replace(/}catch\\(e\\){.*}$/ig, \"\"); //去除try{}catch(e){}\n            try {\n                jsonContent = JSON.parse(result);\n            } catch (e) {\n                console.log(e);\n                let regexp = [\n                    /^.*=({.*}).*$/,\n                    /^.*\\(({.*})\\).*$/\n                ]\n                for (let regex of regexp) {\n                    let res = new RegExp(regex, \"ig\").exec(result);\n                    if (res) {\n                        // console.log(res);\n                        result = res[1];\n                        break;\n                    }\n                }\n                // console.log(result);\n                jsonContent = JSON.parse(result);\n            }\n            renderJson();\n        });\n    }\n\n    function renderJson() {\n        $('#json-renderer').jsonViewer(jsonContent, options);\n    }\n    $(\"#collapsed\").click(function () {\n        options.collapsed = !options.collapsed;\n        if (options.collapsed) {\n            collapsed.innerHTML = i18n.expandAllNodes;\n        } else {\n            collapsed.innerHTML = i18n.collapseAllNodes;\n        }\n        renderJson();\n    });\n}"
  },
  {
    "path": "js/m3u8.downloader.js",
    "content": "class Downloader {\n    constructor(fragments = [], thread = 6) {\n        this.fragments = fragments;      // 切片列表\n        this.allFragments = fragments;   // 储存所有原始切片列表\n        this.thread = thread;            // 线程数\n        this.events = {};                // events\n        this.decrypt = null;             // 解密函数\n        this.transcode = null;           // 转码函数\n        this.init();\n    }\n    /**\n     * 初始化所有变量\n     */\n    init() {\n        this.index = 0;                  // 当前任务索引\n        this.buffer = [];                // 储存的buffer\n        this.state = 'waiting';          // 下载器状态 waiting running done abort\n        this.success = 0;                // 成功下载数量\n        this.errorList = new Set();      // 下载错误的列表\n        this.buffersize = 0;             // 已下载buffer大小\n        this.duration = 0;               // 已下载时长\n        this.pushIndex = 0;              // 推送顺序下载索引\n        this.controller = [];            // 储存中断控制器\n        this.running = 0;                // 正在下载数量\n    }\n    /**\n     * 设置监听\n     * @param {string} eventName 监听名\n     * @param {Function} callBack \n     */\n    on(eventName, callBack) {\n        if (this.events[eventName]) {\n            this.events[eventName].push(callBack);\n        } else {\n            this.events[eventName] = [callBack];\n        }\n    }\n    /**\n     * 触发监听器\n     * @param {string} eventName 监听名\n     * @param  {...any} args \n     */\n    emit(eventName, ...args) {\n        if (this.events[eventName]) {\n            this.events[eventName].forEach(callBack => {\n                callBack(...args);\n            });\n        }\n    }\n    /**\n     * 设定解密函数\n     * @param {Function} callback \n     */\n    setDecrypt(callback) {\n        this.decrypt = callback;\n    }\n    /**\n     * 设定转码函数\n     * @param {Function} callback \n     */\n    setTranscode(callback) {\n        this.transcode = callback;\n    }\n    /**\n     * 停止下载 没有目标 停止所有线程\n     * @param {number} index 停止下载目标\n     */\n    stop(index = undefined) {\n        if (index !== undefined) {\n            this.controller[index] && this.controller[index].abort();\n            return;\n        }\n        this.controller.forEach(controller => { controller.abort() });\n        this.state = 'abort';\n    }\n    /**\n     * 检查对象是否错误列表内\n     * @param {object} fragment 切片对象\n     * @returns {boolean}\n     */\n    isErrorItem(fragment) {\n        return this.errorList.has(fragment);\n    }\n    /**\n     * 返回所有错误列表\n     */\n    get errorItem() {\n        return this.errorList;\n    }\n    /**\n     * 按照顺序推送buffer数据\n     */\n    sequentialPush() {\n        if (!this.events[\"sequentialPush\"]) { return; }\n        for (; this.pushIndex < this.fragments.length; this.pushIndex++) {\n            if (this.buffer[this.pushIndex]) {\n                this.emit('sequentialPush', this.buffer[this.pushIndex]);\n                delete this.buffer[this.pushIndex];\n                continue;\n            }\n            break;\n        }\n    }\n    /**\n     * 限定下载范围\n     * @param {number} start 下载范围 开始索引\n     * @param {number} end 下载范围 结束索引\n     * @returns {boolean}\n     */\n    range(start = 0, end = this.fragments.length) {\n        if (start > end) {\n            this.emit('error', 'start > end');\n            return false;\n        }\n        if (end > this.fragments.length) {\n            this.emit('error', 'end > total');\n            return false;\n        }\n        if (start >= this.fragments.length) {\n            this.emit('error', 'start >= total');\n            return false;\n        }\n        if (start != 0 || end != this.fragments.length) {\n            this.fragments = this.fragments.slice(start, end);\n            // 更改过下载范围 重新设定index\n            this.fragments.forEach((fragment, index) => {\n                fragment.index = index;\n            });\n        }\n        // 总数为空 抛出错误\n        if (this.fragments.length == 0) {\n            this.emit('error', 'List is empty');\n            return false;\n        }\n        return true;\n    }\n    /**\n     * 获取切片总数量\n     * @returns {number}\n     */\n    get total() {\n        return this.fragments.length;\n    }\n    /**\n     * 获取切片总时间\n     * @returns {number}\n     */\n    get totalDuration() {\n        return this.fragments.reduce((total, fragment) => total + fragment.duration, 0);\n    }\n    /**\n     * 切片对象数组的 setter getter\n     */\n    set fragments(fragments) {\n        // 增加index参数 为多线程异步下载 根据index属性顺序保存\n        this._fragments = fragments.map((fragment, index) => ({ ...fragment, index }));\n    }\n    get fragments() {\n        return this._fragments;\n    }\n    /**\n     * 获取 #EXT-X-MAP 标签的文件url\n     * @returns {string}\n     */\n    get mapTag() {\n        if (this.fragments[0].initSegment && this.fragments[0].initSegment.url) {\n            return this.fragments[0].initSegment.url;\n        }\n        return \"\";\n    }\n    /**\n     * 添加一条新资源\n     * @param {Object} fragment\n     */\n    push(fragment) {\n        fragment.index = this.fragments.length;\n        this.fragments.push(fragment);\n    }\n    /**\n     * 下载器 使用fetch下载文件\n     * @param {object} fragment 重新下载的对象\n     */\n    downloader(fragment = null) {\n        if (this.state === 'abort') { return; }\n        // 是否直接下载对象\n        const directDownload = !!fragment;\n\n        // 非直接下载对象 从this.fragments获取下一条资源 若不存在跳出\n        if (!directDownload && !this.fragments[this.index]) { return; }\n\n        // fragment是数字 直接从this.fragments获取\n        if (typeof fragment === 'number') {\n            fragment = this.fragments[fragment];\n        }\n\n        // 不存在下载对象 从提取fragments\n        fragment ??= this.fragments[this.index++];\n        this.state = 'running';\n        this.running++;\n\n        // 资源已下载 跳过\n        // if (this.buffer[fragment.index]) { return; }\n\n        // 停止下载控制器\n        const controller = new AbortController();\n        this.controller[fragment.index] = controller;\n        const options = { signal: controller.signal };\n\n        // 下载前触发事件\n        this.emit('start', fragment, options);\n\n        // 存在byteRange 添加请求头\n        if (fragment.byteRange && fragment.byteRange.length == 2) {\n            options.headers = {\n                'Range': `bytes=${fragment.byteRange[0]}-${fragment.byteRange[1] - 1}`\n            };\n        }\n\n        // 开始下载\n        fetch(fragment.url, options)\n            .then(response => {\n                if (!response.ok) {\n                    throw new Error(response.status, { cause: 'HTTPError' });\n                }\n                const reader = response.body.getReader();\n                const contentLength = parseInt(response.headers.get('content-length')) || 0;\n                fragment.contentType = response.headers.get('content-type') ?? 'null';\n                let receivedLength = 0;\n                const chunks = [];\n                const pump = async () => {\n                    while (true) {\n                        const { value, done } = await reader.read();\n                        if (done) { break; }\n\n                        // 流式下载\n                        fragment.fileStream ? fragment.fileStream.write(new Uint8Array(value)) : chunks.push(value);\n\n                        receivedLength += value.length;\n                        this.emit('itemProgress', fragment, false, receivedLength, contentLength, value);\n                    }\n                    if (fragment.fileStream) {\n                        return new ArrayBuffer();\n                    }\n                    const allChunks = new Uint8Array(receivedLength);\n                    let position = 0;\n                    for (const chunk of chunks) {\n                        allChunks.set(chunk, position);\n                        position += chunk.length;\n                    }\n                    this.emit('itemProgress', fragment, true);\n                    return allChunks.buffer;\n                }\n                return pump();\n            })\n            .then(buffer => {\n                this.emit('rawBuffer', buffer, fragment);\n                // 存在解密函数 调用解密函数 否则直接返回buffer\n                return this.decrypt ? this.decrypt(buffer, fragment) : buffer;\n            })\n            .then(buffer => {\n                this.emit('decryptedData', buffer, fragment);\n                // 存在转码函数 调用转码函数 否则直接返回buffer\n                return this.transcode ? this.transcode(buffer, fragment) : buffer;\n            })\n            .then(buffer => {\n                // 储存解密/转码后的buffer\n                this.buffer[fragment.index] = buffer;\n\n                // 成功数+1 累计buffer大小和视频时长\n                this.success++;\n                this.buffersize += buffer.byteLength;\n                this.duration += fragment.duration ?? 0;\n\n                // 下载对象来自错误列表 从错误列表内删除\n                this.errorList.has(fragment) && this.errorList.delete(fragment);\n\n                // 推送顺序下载\n                this.sequentialPush();\n\n                this.emit('completed', buffer, fragment);\n\n                // 下载完成\n                if (this.success == this.fragments.length) {\n                    this.state = 'done';\n                    this.emit('allCompleted', this.buffer, this.fragments);\n                }\n            }).catch((error) => {\n                console.log(error);\n                if (error.name == 'AbortError') {\n                    this.emit('stop', fragment, error);\n                    return;\n                }\n                this.emit('downloadError', fragment, error);\n\n                // 储存下载错误切片\n                !this.errorList.has(fragment) && this.errorList.add(fragment);\n            }).finally(() => {\n                this.running--;\n                // 下载下一个切片\n                if (!directDownload && this.index < this.fragments.length) {\n                    this.downloader();\n                }\n            });\n    }\n    /**\n     * 开始下载 准备数据 调用下载器\n     * @param {number} start 下载范围 开始索引\n     * @param {number} end 下载范围 结束索引\n     */\n    start(start = 0, end = this.fragments.length) {\n        // 检查下载器状态\n        if (this.state == 'running') {\n            this.emit('error', 'state running');\n            return;\n        }\n        // 从下载范围内 切出需要下载的部分\n        if (!this.range(start, end)) {\n            return;\n        }\n        // 初始化变量\n        this.init();\n        // 开始下载 多少线程开启多少个下载器\n        for (let i = 0; i < this.thread && i < this.fragments.length; i++) {\n            this.downloader();\n        }\n    }\n    /**\n     * 销毁 初始化所有变量\n     */\n    destroy() {\n        this.stop();\n        this._fragments = [];\n        this.allFragments = [];\n        this.thread = 6;\n        this.events = {};\n        this.decrypt = null;\n        this.transcode = null;\n        this.init();\n    }\n}"
  },
  {
    "path": "js/m3u8.js",
    "content": "// url 参数解析\nconst params = new URL(location.href).searchParams;\nlet _m3u8Url = params.get(\"url\");   // m3u8的url地址\nconst _requestHeaders = params.get(\"requestHeaders\");   // 自定义请求头\nconst _initiator = params.get(\"initiator\"); // referer 备用\nconst _title = params.get(\"title\"); // 来源网页标题\nconst _fileName = params.get(\"filename\");   // 自定义文件名\nlet tsAddArg = params.get(\"tsAddArg\");  // 自定义 切片参数\nlet autoReferer = params.get(\"autoReferer\");    // 是否已经自动调整 referer\nconst tabId = parseInt(params.get(\"tabid\"));    // 资源所在的标签页ID 用来获取密钥\nconst key = params.get(\"key\");  // 自定义密钥\nlet autoDown = params.get(\"autoDown\");  //是否自动下载\nconst autoClose = params.get(\"autoClose\");  // 下载完是否关闭页面\nlet retryCount = parseInt(params.get(\"retryCount\"));  // 重试次数\n\nlet currentTabId = 0;   // 本页面tab Id\nlet currentIndex = 0;   // 本页面Index\n\n/*\n*   popup 合并多个m3u8 需要提交以下参数 ffmpeg才能判断文件是否添加完毕\n*   _ffmpeg 参数为ffmpeg动作 例如: \"merge\"为合并\n*   _quantity: m3u8数量\n*   _taskId: 唯一任务ID\n**/\nconst _ffmpeg = params.get(\"ffmpeg\");   // 是否发送到 ffmpeg\nconst _quantity = params.get(\"quantity\");   // 同时下载的总数\nconst _taskId = params.get(\"taskId\");   // 任务id\n\nlet isSendFfmpeg = false;   // 是否发送到ffmpeg\n\n// 修改当前标签下的所有xhr的Referer 修改完成 运行init函数\nconst requestHeaders = JSONparse(_requestHeaders);\n// 当前资源数据\nlet _data = {\n    url: _m3u8Url,\n    title: _title ?? \"NULL\",\n};\nsetRequestHeaders(requestHeaders, () => {\n    chrome.tabs.getCurrent(function (tab) {\n        currentIndex = tab.index;\n        currentTabId = tab.id;\n        if (tabId && tabId != -1) {\n            chrome.runtime.sendMessage(chrome.runtime.id, { Message: \"getData\", tabId: tabId }, (data) => {\n                if (chrome.runtime.lastError) {\n                    awaitG(init);\n                    return;\n                }\n                if (data) {\n                    data = data.find(item => item.url == _m3u8Url);\n                    _data = data ?? _data;\n                }\n                awaitG(init);\n            });\n        } else {\n            awaitG(init);\n        }\n    });\n});\n\n// 默认设置\nconst allOption = {\n    addParam: false,\n    fold: !G.isMobile,\n    m3u8dlRE: false,\n};\nlet _m3u8Content;   // 储存m3u8文件内容\n/* m3u8 解析工具 */\nconst hls = new Hls({\n    enableWorker: false,\n    debug: false\n});  // hls.js 对象\nconst _fragments = []; // 储存切片对象\nconst keyContent = new Map(); // 储存key的内容\nconst initData = new Map(); // 储存map的url\nconst decryptor = new AESDecryptor(); // 解密工具 来自hls.js 分离出来的\nlet skipDecrypt = false; // 是否跳过解密\nlet possibleKeys = new Set();   // 储存疑似 密钥\nlet downId = 0; // chrome下载api 回调id\nlet currentLevel = -1;  // 当前Level\nlet estimateFileSize = 0; // 估算的文件最终大小\n\nlet downDuration = 0; // 下载媒体得时长\n\n\nlet fileStream = undefined; // 流式下载文件输出流\nconst downSet = {};   // 下载时 储存设置\n\n/* 录制相关 */\nlet recorder = false; // 开关\nlet recorderLast = \"\";  // 最后下载的url\n\n/* mp4 转码工具 */\nlet transmuxer = undefined;\n\n/* DOM */\nconst $fileSize = $(\"#fileSize\");   // 下载文件大小 进度\nconst $progress = $(\"#progress\");   // 下载进度\nconst $fileDuration = $(\"#fileDuration\");   // 下载总时长\nconst $m3u8dlArg = $(\"#m3u8dlArg\"); // m3u8DL 参数\nconst $media_file = $(\"#media_file\");   // 切片列表\n\n/**\n * 初始化函数，界面默认配置 loadSource载入 m3u8 url\n */\nfunction init() {\n    // 获取页面DOM\n    if (tabId && tabId != -1) {\n        chrome.tabs.sendMessage(parseInt(tabId), { Message: \"getPage\" }, { frameId: 0 }, function (result) {\n            if (chrome.runtime.lastError) { return; }\n            _data.pageDOM = new DOMParser().parseFromString(result, 'text/html');\n        });\n    }\n    loadCSS();\n\n    // 隐藏firefox 不支持的功能\n    G.isFirefox && $(\".firefoxHide\").each(function () { $(this).hide(); });\n\n    // 读取本地配置并装载\n    chrome.storage.local.get(allOption, function (items) {\n        for (let key in items) {\n            if (key == \"fold\") {\n                items[key] ? $(\"details\").attr(\"open\", \"\") : $(\"details\").removeAttr(\"open\");\n                continue;\n            }\n            const $dom = $(`#${key}`);\n            $dom.length && $dom.prop(\"checked\", items[key]);\n        }\n    });\n\n    // 转载默认配置\n    $(\"#thread\").val(G.M3u8Thread);\n    $(\"#mp4\").prop(\"checked\", G.M3u8Mp4);\n    $(\"#onlyAudio\").prop(\"checked\", G.M3u8OnlyAudio);\n    $(\"#skipDecrypt\").prop(\"checked\", G.M3u8SkipDecrypt);\n    $(\"#StreamSaver\").prop(\"checked\", G.M3u8StreamSaver);\n    $(\"#ffmpeg\").prop(\"checked\", G.M3u8Ffmpeg);\n    $(\"#autoClose\").prop(\"checked\", autoClose ? true : G.M3u8AutoClose);\n\n    // 发送到ffmpeg取消边下边存设置\n    _ffmpeg && $(\"#StreamSaver\").prop(\"checked\", false);\n\n    // 存在密钥参数 自动填写密钥\n    key && $(\"#customKey\").val(key);\n\n    // 解码 切片URL参数\n    if (tsAddArg != null) {\n        tsAddArg = decodeURIComponent(tsAddArg);\n        $(\"#tsAddArg\").html(i18n.restoreGetParameters);\n    }\n\n    // 填充重试次数\n    retryCount && $(\"#retryCount\").val(retryCount);\n\n    if (isEmpty(_m3u8Url)) {\n        $(\"#loading\").hide(); $(\"#m3u8Custom\").show();\n\n        $(\"#uploadM3U8\").change(function (event) {\n            const file = event.target.files[0];\n            const reader = new FileReader();\n            reader.onload = () => {\n                $(\"#m3u8Text\").val(reader.result)\n            };\n            reader.readAsText(file);\n        });\n\n        $(\"#parse\").click(async function () {\n            let m3u8Text = $(\"#m3u8Text\").val().trim();\n            let baseUrl = $(\"#baseUrl\").val().trim();\n            let referer = $(\"#referer\").val().trim();\n            if (referer) {\n                if (referer.startsWith(\"http\")) {\n                    setRequestHeaders({ referer: referer });\n                } else {\n                    setRequestHeaders(JSONparse(referer));\n                }\n            }\n\n            if (m3u8Text == \"\") { return; }\n\n            // // 批量生成切片链接 解析range标签\n            if (m3u8Text.includes('${range:')) {\n                const rangePattern = /\\$\\{range:(\\d+)-(\\d+|\\?),?(\\d+)?\\}/;\n                const match = m3u8Text.match(rangePattern);\n                if (!match) { return; }\n                const start = parseInt(match[1]);\n                let end = match[2];\n                const padding = match[3] ? parseInt(match[3]) : 0;\n                const urls = [];\n                $(\"#m3u8Text\").val(i18n.loadingData);\n\n                if (end === \"?\") {\n                    let i = start;\n                    while (true) {\n                        let number = i.toString();\n                        if (padding > 0) {\n                            number = number.padStart(padding, '0');\n                        }\n                        const url = m3u8Text.replace(rangePattern, number);\n                        try {\n                            const response = await fetch(url, { method: 'HEAD' });\n                            if (!response.ok) {\n                                break;\n                            }\n                            urls.push(url);\n                        } catch (error) { break; }\n\n                        i++;\n                        // 防止死循环 最大9999个\n                        if (urls.length >= 9999) { break; }\n                    }\n                } else {\n                    end = parseInt(end);\n                    for (let i = start; i <= end; i++) {\n                        let number = i.toString();\n                        if (padding > 0) {\n                            number = number.padStart(padding, '0');\n                        }\n                        urls.push(m3u8Text.replace(rangePattern, number));\n                    }\n                }\n                if (urls && urls.length) {\n                    m3u8Text = urls.join(\"\\n\\n\");\n                    $(\"#m3u8Text\").val(m3u8Text);\n                } else {\n                    $(\"#m3u8Text\").val(\"\");\n                    alert(i18n.m3u8Error);\n                    return;\n                }\n            }\n\n            // 只有一个链接 后缀为m3u8 直接解析\n            if (m3u8Text.split(\"\\n\").length == 1 && (GetExt(m3u8Text) == \"m3u8\" || GetExt(m3u8Text) == \"txt\")) {\n                let url = \"m3u8.html?url=\" + encodeURIComponent(m3u8Text);\n                if (referer) {\n                    if (referer.startsWith(\"http\")) {\n                        url += \"&requestHeaders=\" + encodeURIComponent(JSON.stringify({ referer: referer }));\n                    } else {\n                        url += \"&requestHeaders=\" + encodeURIComponent(referer);\n                    }\n                }\n                chrome.tabs.update({ url: url });\n                return;\n            }\n\n            // 如果不是 m3u8 文件内容 转换为 m3u8 文件内容\n            if (!m3u8Text.includes(\"#EXTM3U\")) {\n                // ts列表链接 转 m3u8\n                const tsList = m3u8Text.split(\"\\n\");\n                m3u8Text = \"#EXTM3U\\n\";\n                m3u8Text += \"#EXT-X-TARGETDURATION:233\\n\";\n                for (let ts of tsList) {\n                    if (ts) {\n                        m3u8Text += \"#EXTINF:1\\n\";\n                        m3u8Text += ts + \"\\n\";\n                    }\n                }\n                m3u8Text += \"#EXT-X-ENDLIST\";\n            }\n            if (baseUrl != \"\") {\n                m3u8Text = addBashUrl(baseUrl, m3u8Text);\n            }\n            autoReferer = true; // 不自动调整referer\n\n            _m3u8Url = URL.createObjectURL(new Blob([new TextEncoder(\"utf-8\").encode(m3u8Text)]));\n            hls.loadSource(_m3u8Url);\n            $(\"#m3u8Custom\").hide();\n        });\n        // 从mpd解析器读取数据\n        const getId = parseInt(params.get(\"getId\"));\n        if (getId) {\n            chrome.tabs.sendMessage(getId, \"getM3u8\", function (result) {\n                $(\"#m3u8Text\").val(result.m3u8Content);\n                $(\"#parse\").click();\n                $(\"#info\").html(result.mediaInfo);\n            });\n        }\n    } else {\n        hls.loadSource(_m3u8Url);\n    }\n\n    G.saveAs && $(\"#saveAs\").prop(\"checked\", true);\n}\n\n// 监听 MANIFEST_LOADED 装载解析的m3u8 URL\nhls.on(Hls.Events.MANIFEST_LOADED, function (event, data) {\n    $(\"#m3u8_url\").attr(\"href\", data.url).html(data.url);\n});\n\n// 监听 MANIFEST_PARSED m3u8解析完成\nhls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {\n    // console.log(data);\n    $(\"#m3u8\").show(); $(\"#loading\").hide();\n    const more = (data.levels.length + data.audioTracks.length + data.subtitleTracks.length >= 2);\n    const dataMerge = {};\n\n    // 多个视频\n    if (more && data.levels.length) {\n        $(\"#more_m3u8\").show();\n        let maxBandwidth = 0;\n        for (let index in data.levels) {\n            const item = data.levels[index];\n            let [name, url] = getNewUrl(item);\n            maxBandwidth = Math.max(maxBandwidth, item.attrs.BANDWIDTH);\n            if (maxBandwidth == item.attrs.BANDWIDTH) { dataMerge.video = item; }   // 默选择码率最大的\n            const html = $(`<div class=\"block\">\n                    <div><label class=\"more_class\"><input type=\"radio\" name=\"more_video\" ${maxBandwidth == item.attrs.BANDWIDTH ? \"checked\" : \"\"}/>${item.attrs.RESOLUTION ? i18n.resolution + \":\" + item.attrs.RESOLUTION : \"\"}${item.attrs.BANDWIDTH ? \" | \" + i18n.bitrate + \":\" + (parseInt(item.attrs.BANDWIDTH / 1000) + \" Kbps\") : \"\"}</label></div>\n                    <a href=\"${url}\">${name}</a>\n                    <button id=\"parser\" type=\"button\">${i18n.parser}</button>\n                    <button class=\"sendFfmpeg\" type=\"button\">${i18n.sendFfmpeg}</button>\n                </div>`);\n            html.find(\".sendFfmpeg\").click(function () {\n                let newUrl = url + `&autoDown=1`;\n                newUrl += `&ffmpeg=addFile`;\n                chrome.tabs.create({ url: newUrl, index: currentIndex + 1, active: false });\n            });\n            html.find(\"#parser\").click(function () {\n                chrome.tabs.update({ url: url });\n            });\n            html.find(\".more_class\").click(function (e) {\n                dataMerge.video = item;\n            });\n            $(\"#next_m3u8\").append(html);\n        }\n    }\n    // 多个音频\n    if (more && data.audioTracks.length) {\n        $(\"#more_audio\").show();\n        for (let index in data.audioTracks) {\n            const item = data.audioTracks[index];\n            // 音频信息没有m3u8文件 使用groupId去寻找\n            if (item.url == \"\") {\n                let groupId = item.groupId;\n                for (let item2 of data.levels) {\n                    if (item2.audioGroupIds.includes(groupId)) {\n                        item.url = item2.uri;\n                        break;\n                    }\n                }\n            }\n            let [name, url] = getNewUrl(item);\n            if (index == 0) { dataMerge.audio = item; }     // 默选择第一个\n            const html = $(`<div class=\"block\">\n                    <div><label><input type=\"radio\" name=\"more_audio\" ${index == 0 ? \"checked\" : \"\"}/>${item.name ? item.name : \"\"} | ${item.lang ? item.lang : \"\"} | ${item.groupId ? item.groupId : \"\"}</label></div>\n                    <a href=\"${url}\">${name}</a>\n                    <button id=\"parser\" type=\"button\">${i18n.parser}</button>\n                    <button class=\"sendFfmpeg\" type=\"button\">${i18n.sendFfmpeg}</button>\n                </div>`);\n            html.find(\".sendFfmpeg\").click(function () {\n                let newUrl = url + `&autoDown=1`;\n                newUrl += `&ffmpeg=addFile`;\n                chrome.tabs.create({ url: newUrl, index: currentIndex + 1, active: false });\n            });\n            html.find(\"#parser\").click(function () {\n                chrome.tabs.update({ url: url });\n            });\n            html.find(\"label\").click(function () {\n                dataMerge.video = item;\n            });\n            $(\"#next_audio\").append(html);\n        }\n    }\n    // 多个字幕\n    if (more && data.subtitleTracks.length) {\n        $(\"#more_subtitle\").show();\n        for (let item of data.subtitleTracks) {\n            const [name, url] = getNewUrl(item);\n            const html = `<div class=\"block\">\n                    <div>${item.name ? item.name : \"\"} | ${item.lang ? item.lang : \"\"}</div>\n                    <a href=\"${url}\">${name}</a>\n                </div>`;\n            $(\"#next_subtitle\").append(html);\n        }\n    }\n\n    // 合并按钮\n    if (dataMerge.audio && dataMerge.video) {\n        $(\"#more_options\").show();\n        $(\"#more_options_merge\").click(function () {\n            const taskId = Date.parse(new Date());\n            if (dataMerge.audio && dataMerge.video) {\n                const data = {\n                    title: _title,\n                    downFileName: _fileName,\n                    tabId: tabId,\n                    initiator: _initiator,\n                    requestHeaders: requestHeaders,\n                }\n                const option = { ffmpeg: \"merge\", quantity: 2, taskId: taskId, autoDown: true, autoClose: true };\n                openParser({ ...data, url: dataMerge.audio.url }, option);\n                openParser({ ...data, url: dataMerge.video.url }, option);\n            }\n        });\n    } else {\n        $(\"#more_m3u8 input\").hide();\n        $(\"#more_audio input\").hide();\n    }\n\n\n    // 有下一级m3u8 停止解析\n    if (more) {\n        autoDown && highlight();\n        $(\"#m3u8\").hide();\n        // $(\"button\").hide();\n        return;\n    }\n    function getNewUrl(item) {\n        const url = encodeURIComponent(item.uri ?? item.url);\n        const referer = requestHeaders.referer ? \"&requestHeaders=\" + encodeURIComponent(JSON.stringify(requestHeaders)) : \"&initiator=\" + (_initiator ? encodeURIComponent(_initiator) : \"\");\n        const title = _title ? encodeURIComponent(_title) : \"\";\n        const name = GetFile(item.uri ?? item.url);\n        let newUrl = `/m3u8.html?url=${url}${referer}`;\n        if (title) { newUrl += `&title=${title}`; }\n        if (tabId) { newUrl += `&tabid=${tabId}`; }\n        if (key) { newUrl += `&key=${key}`; }\n        return [name, newUrl];\n    }\n});\n\n// 监听 LEVEL_LOADED 所有切片载入完成\nhls.on(Hls.Events.LEVEL_LOADED, function (event, data) {\n    // console.log(data);\n    parseTs(data.details);  // 提取Ts链接\n    // 获取视频信息\n    if ($(\".videoInfo #info\").html() == \"\") {\n        let video = document.createElement(\"video\");\n        video.muted = true;\n        video.autoplay = false;\n        hls.attachMedia(video);\n        hls.on(Hls.Events.MEDIA_ATTACHED, function () {\n            video && video.play();\n        });\n        video.oncanplay = function () {\n            hls.detachMedia(video);\n            video.remove();\n            video = null;\n        }\n        video.onerror = function () {\n            hls.stopLoad();\n            hls.detachMedia(video);\n            video.remove();\n            video = null;\n        }\n    }\n    currentLevel = data.level;\n});\n\n// 监听 ERROR m3u8解析错误\nhls.on(Hls.Events.ERROR, function (event, data) {\n    autoDown && highlight();\n    console.log(data);\n    if (data.details == \"bufferStalledError\") {\n        hls.stopLoad();\n    }\n    if (data.type == \"mediaError\" && data.details == \"fragParsingError\") {\n        if (data.error.message == \"No ADTS header found in AAC PES\" && !hls.adtsTips) {\n            $(\"#tips\").append(\"<b>\" + i18n.ADTSerror + \"</b>\");\n            hls.stopLoad();\n            hls.destroy();\n            hls.adtsTips = true; // 标记已经提示过\n        }\n        $(\"#play\").hide();\n        return;\n    }\n    if (data.type == \"otherError\" && data.error.message.includes(\"remux\") && hls.skipTheError) {\n        return;\n    }\n    $(\"#loading\").show();\n    $(\"#loading .optionBox\").html(`${i18n.m3u8Error}<button id=\"setRequestHeadersError\">${i18n.setRequestHeaders}</button>`);\n\n    /**\n     * 下载出错 如果在录制中 停止下载 保存文件\n     * 检查重试次数 重新下载\n     */\n    if (retryCount) {\n        recorder && stopRecorder();\n        const recorderRetryCount = parseInt($(\"#retryCount\").val());\n        const url = new URL(location.href);\n        const params = new URLSearchParams(url.search);\n        params.set(\"retryCount\", recorderRetryCount ? recorderRetryCount - 1 : 0);\n        params.set(\"autoDown\", 1);\n        $progress.html(i18n.retryCount + \": \" + recorderRetryCount);\n        setTimeout(() => {\n            window.location.href = window.location.origin + window.location.pathname + \"?\" + params.toString();\n        }, 3000);\n        return;\n    }\n    if (recorder) {\n        stopRecorder();\n        autoReferer = true;\n        return;\n    }\n\n    // 尝试添加 / 删除请求头\n    if (data.type == \"networkError\" && data.details != \"keyLoadError\") {\n        if (requestHeaders.referer) {\n            params.delete(\"requestHeaders\");\n        } else if (_initiator) {\n            params.delete(\"requestHeaders\");\n            let origin = null;\n            try { origin = new URL(_initiator).origin; } catch (e) { }\n            params.append(\"requestHeaders\", JSON.stringify({ \"referer\": _initiator, \"origin\": origin ?? _initiator }));\n        }\n        const href = window.location.origin + window.location.pathname + \"?\" + params.toString();\n        if (!autoReferer && window.location.href != href) {\n            window.location.href = href + \"&autoReferer=1\";\n        }\n    }\n});\n\n// 监听 BUFFER_CREATED 获得第一个切片数据\nhls.on(Hls.Events.BUFFER_CREATED, function (event, data) {\n    // console.log(data);\n    const info = $(\".videoInfo #info\");\n    if (data.tracks && info.html() == \"\") {\n        if (data.tracks.audiovideo) {\n            if (data.tracks.audiovideo?.metadata) {\n                info.append(` ${i18n.resolution}:${data.tracks.audiovideo.metadata.width} x ${data.tracks.audiovideo.metadata.height}`);\n            }\n            if (data.tracks.audiovideo.codec && data.tracks.audiovideo.codec.startsWith(\"hvc1\")) {\n                info.append(` <b>${i18n.hevcTip}</b>`);\n            }\n            return;\n        }\n        !data.tracks.audio && info.append(` (${i18n.noAudio})`);\n        !data.tracks.video && info.append(` (${i18n.noVideo})`);\n        if (data.tracks.video?.metadata) {\n            info.append(` ${i18n.resolution}:${data.tracks.video.metadata.width} x ${data.tracks.video.metadata.height}`);\n        }\n        if (hls.levels[currentLevel]?.bitrate) {\n            info.append(` ${i18n.bitrate}:${parseInt(hls.levels[currentLevel].bitrate / 1000)} Kbps`);\n        }\n        if (data.tracks?.video?.codec && data.tracks.video.codec.startsWith(\"hvc1\")) {\n            info.append(` <b>${i18n.hevcTip}</b>`);\n        }\n    }\n});\n\n/* 来自 监听 LEVEL_LOADED 提取所有ts链接 进一步处理 */\nfunction parseTs(data) {\n    // console.log(data);\n    let isEncrypted = false;\n    _fragments.splice(0);   // 清空 防止直播HLS无限添加\n    /* 获取 m3u8文件原始内容 MANIFEST_PARSED也能获取但偶尔会为空(BUG?) 放在LEVEL_LOADED获取更安全*/\n    _m3u8Content = data.m3u8;\n\n    // #EXT-X-DISCONTINUITY\n    let discontinuity = { start: 0, cc: 0 };\n    data.endCC && $(\"#cc\").show();\n\n    for (let i in data.fragments) {\n        i = parseInt(i);\n        /*\n        * 少部分网站下载ts必须带有参数才能正常下载\n        * 添加m3u8地址的参数\n        */\n        if (tsAddArg != null) {\n            const arg = new RegExp(\"([^?]*)\").exec(data.fragments[i].url);\n            if (arg && arg[0]) {\n                data.fragments[i].url = arg[0] + (tsAddArg ? \"?\" + tsAddArg : \"\");\n            }\n        }\n        /* \n        * 查看是否加密 下载key\n        * firefox CSP政策不允许在script-src 使用blob 不能直接调用hls.js下载好的密钥\n        */\n        if (data.fragments[i].encrypted && data.fragments[i].decryptdata) {\n            isEncrypted = true;\n            // 填入key内容\n            Object.defineProperty(data.fragments[i].decryptdata, \"keyContent\", {\n                get: function () { return keyContent.get(this.uri); },\n                configurable: true\n            });\n            // 如果不存在key 开始下载\n            if (!keyContent.get(data.fragments[i].decryptdata.uri)) {\n                // 占位 等待fetch获取key\n                keyContent.set(data.fragments[i].decryptdata.uri, true);\n                // 下载key\n                fetch(data.fragments[i].decryptdata.uri)\n                    .then(response => response.arrayBuffer())\n                    .then(function (buffer) {\n                        if (buffer.byteLength == 16) {\n                            keyContent.set(data.fragments[i].decryptdata.uri, buffer); // 储存密钥\n                            showKeyInfo(buffer, data.fragments[i].decryptdata, i);\n                            autoMerge();\n                            return;\n                        }\n                        showKeyInfo(false, data.fragments[i].decryptdata, i);\n                    })\n                    .catch(function (error) {\n                        console.log(error);\n                        showKeyInfo(false, data.fragments[i].decryptdata, i);\n                    });\n            }\n        }\n        // 处理 #EXT-X-MAP 标签\n        let initSegment = null;\n        if (data.fragments[i].initSegment && !initData.get(data.fragments[i].initSegment.url)) {\n            initSegment = data.fragments[i].initSegment;\n            initData.set(data.fragments[i].initSegment.url, true);\n\n            const options = {};\n            if (data.fragments[i].initSegment.byteRange && data.fragments[i].initSegment.byteRange.length == 2) {\n                const [start, end] = data.fragments[i].initSegment.byteRange;\n                options.headers = {\n                    'Range': `bytes=${start}-${end - 1}`\n                };\n            }\n            fetch(data.fragments[i].initSegment.url, options)\n                .then(response => response.arrayBuffer())\n                .then(function (buffer) {\n                    initData.set(data.fragments[i].initSegment.url, buffer);\n                    autoMerge();\n                }).catch(function (error) { console.log(error); });\n            $(\"#tips\").append('EXT-X-MAP: <input type=\"text\" class=\"keyUrl\" value=\"' + data.fragments[i].initSegment.url + '\" spellcheck=\"false\" readonly=\"readonly\">');\n        }\n\n        if (data.live && data.fragments[i].initSegment) {\n            initSegment = data.fragments[i].initSegment;\n        }\n\n        // #EXT-X-DISCONTINUITY\n        if (i === data.fragments.length - 1 || data.fragments[i].cc !== data.fragments[i + 1].cc) {\n            if (discontinuity.start == 0) {\n                $('#cc').append(`<option value=\"0\">${i18n.selectAll}</option>`);\n            }\n            $('#cc').append(`<option value=\"${+discontinuity.start + 1}-${i + 1}\">playlist: ${data.fragments[i].cc}</option>`);\n            discontinuity.start = i + 1;\n        }\n        _fragments.push({\n            url: data.fragments[i].url,\n            decryptdata: data.fragments[i].decryptdata,\n            encrypted: data.fragments[i].encrypted,\n            duration: data.fragments[i].duration,\n            initSegment: initSegment,\n            sn: data.fragments[i].sn,\n            cc: data.fragments[i].cc,\n            live: data.live,\n            byteRange: data.fragments[i].byteRange\n        });\n    }\n    /* \n    * 录制直播\n    * 直播是持续更新的m3u8 \n    * recorderLast保存下载的最后一个url 以便下次更新时判断从哪个切片开始继续下载\n    */\n    if (recorder) {\n        let indexLast = _fragments.findIndex((fragment) => {\n            return fragment.url == recorderLast;\n        });\n        recorderLast = _fragments[_fragments.length - 1].url;\n        downloadNew(indexLast + 1);\n    }\n\n    writeText(_fragments);   // 写入ts链接到textarea\n\n    // 提示加密\n    isEncrypted && $(\"#count\").append(` (${i18n.encryptedHLS})`);\n\n    // SAMPLE 加密算法\n    if (_m3u8Content.includes(\"#EXT-X-KEY:METHOD=SAMPLE-AES-CTR\")) {\n        $(\"#count\").append(' <b>' + i18n.encryptedSAMPLE + '</b>');\n    }\n\n    // 范围下载所需数据\n    $(\"#rangeStart\").attr(\"max\", _fragments.length);\n    $(\"#rangeEnd\").attr(\"max\", _fragments.length).val(_fragments.length);\n    $m3u8dlArg.val(getM3u8DlArg());\n\n    if (data.live) {\n        autoDown && highlight();\n        $(\"#recorder\").show();\n        $(\".videoInfo #info\").html(i18n.liveHLS);\n    } else {\n        estimateSize(_fragments); // 估算文件大小\n        $(\"#count\").append(i18n(\"m3u8Info\", [_fragments.length, secToTime(data.totalduration)]));\n        $(\"#sendFfmpeg\").show();\n        $(\"#retryCount\").parent().hide();\n    }\n    if (!_fragments.some(fragment => fragment.initSegment) && autoDown) {\n        $(\"#mergeTs\").click();\n    }\n\n    if (tabId && tabId != -1) {\n        chrome.webNavigation.getAllFrames({ tabId: tabId }, function (frames) {\n            if (!frames) { return; }\n            frames.forEach(function (frame) {\n                chrome.tabs.sendMessage(tabId, { Message: \"getKey\" }, { frameId: frame.frameId }, function (result) {\n                    if (chrome.runtime.lastError || !result || result.length == 0) { return; }\n                    const maybeKey = $(\"#maybeKey select\");\n                    for (let item of result) {\n                        if (possibleKeys.has(item)) { continue; }\n                        possibleKeys.add(item);\n                        maybeKey.append(`<option value=\"${item}\">${item}</option>`);\n                    }\n                    $(\"#maybeKey\").show();\n                    maybeKey.change(function () {\n                        this.value != \"tips\" && $(\"#customKey\").val(this.value);\n                        $m3u8dlArg.val(getM3u8DlArg());\n                    });\n                });\n            });\n        });\n    }\n    function showKeyInfo(buffer, decryptdata, i) {\n        $(\"#tips\").append(i18n.keyAddress + ': <input type=\"text\" value=\"' + decryptdata.uri + '\" spellcheck=\"false\" readonly=\"readonly\" class=\"keyUrl\">');\n        if (buffer) {\n            $(\"#tips\").append(`\n                <div class=\"key flex\">\n                    <div class=\"method\">${i18n.encryptionAlgorithm}: <input type=\"text\" value=\"${decryptdata.method ? decryptdata.method : \"NONE\"}\" spellcheck=\"false\" readonly=\"readonly\"></div>\n                    <div>${i18n.key}(Hex): <input type=\"text\" value=\"${ArrayBufferToHexString(buffer)}\" spellcheck=\"false\" readonly=\"readonly\"></div>\n                    <div>${i18n.key}(Base64): <input type=\"text\" value=\"${ArrayBufferToBase64(buffer)}\" spellcheck=\"false\" readonly=\"readonly\"></div>\n                </div>`);\n        } else {\n            $(\"#tips\").append(`\n                <div class=\"key flex\">\n                    <div class=\"method\">${i18n.encryptionAlgorithm}: <input type=\"text\" value=\"${decryptdata.method ? decryptdata.method : \"NONE\"}\" spellcheck=\"false\" readonly=\"readonly\"></div>\n                    <div>${i18n.key}(Hex): <input type=\"text\" value=\"${i18n.keyDownloadFailed}\" spellcheck=\"false\" readonly=\"readonly\"></div>\n                </div>`);\n        }\n        // 如果是默认iv 则不显示\n        let iv = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, i + 1]).toString();\n        let iv2 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, i]).toString();\n        let _iv = decryptdata.iv.toString();\n        if (_iv != iv && _iv != iv2) {\n            iv = \"0x\" + ArrayBufferToHexString(decryptdata.iv.buffer);\n            $(\"#tips\").append('<div class=\"key flex\"><div>Offset(IV): <input type=\"text\" value=\"' + iv + '\" spellcheck=\"false\" readonly=\"readonly\" class=\"offset\"></div></div>');\n        }\n    }\n}\n/**\n * 估算整个视频大小\n * 获取3个切片大小 取平均值 * 切片数量\n * @param {Array} url ts对象数组\n */\nasync function estimateSize(fragments) {\n    if (!fragments || fragments.length === 0) return;\n\n    const samplesToCheck = Math.min(3, fragments.length);\n    let totalSize = 0;\n    let successfulFetches = 0;\n\n    const promises = [];\n\n    for (let i = 0; i < samplesToCheck; i++) {\n        promises.push(\n            fetch(fragments[i].url, {\n                method: \"HEAD\",\n                headers: requestHeaders,\n            }).then(function (response) {\n                if (response.ok) {\n                    const contentLength = response.headers.get(\"Content-Length\");\n                    if (contentLength) {\n                        totalSize += parseInt(contentLength);\n                        successfulFetches++;\n                    }\n                }\n            }).catch(function (error) {\n                console.log(`Error estimating file size for sample ${i}:`, error);\n            })\n        );\n    }\n\n    await Promise.all(promises);\n\n    if (successfulFetches > 0) {\n        estimateFileSize = totalSize / successfulFetches * fragments.length;\n        $(\"#estimateFileSize\").append(` ${i18n.estimateSize}: ${byteToSize(estimateFileSize)}`);\n    }\n}\n/**************************** 监听 / 按钮绑定 ****************************/\n// 标题\nlet progressTimer = setInterval(() => {\n    if ($progress.html()) {\n        document.title = $progress.html();\n    }\n}, 1000);\n// 监听下载事件 修改提示\nchrome.downloads.onChanged.addListener(function (downloadDelta) {\n    if (!downloadDelta.state) { return; }\n    if (downloadDelta.state.current == \"complete\" && downId == downloadDelta.id) {\n        $progress.html(i18n.SavePrompt);\n        $(\"#autoClose\").prop(\"checked\") && closeTab();\n    }\n});\n// 打开目录\n$(\".openDir\").click(function () {\n    downId ? chrome.downloads.show(downId) : chrome.downloads.showDefaultFolder();\n});\n// 下载ts列表\n$(\"#downText\").click(function () {\n    const filename = GetFileName(_m3u8Url) + '.txt';\n    let text = \"data:text/plain,\";\n    _fragments.forEach(function (item) {\n        text += item.url + \"\\n\";\n    });\n    if (G.isFirefox) {\n        downloadDataURL(text, filename);\n        return;\n    }\n    chrome.downloads.download({\n        url: text,\n        filename: filename\n    });\n});\n// 原始m3u8\n$(\"#originalM3U8\").click(function () {\n    writeText(_m3u8Content);\n});\n// 提取ts\n$(\"#getTs\").click(function () {\n    writeText(_fragments);\n});\n//把远程文件替换成本地文件\n$(\"#localFile\").click(function () {\n    writeText(\"\");\n    let textarea = \"\";\n    let m3u8_split = _m3u8Content.split(\"\\n\");\n    for (let key in m3u8_split) {\n        if (isEmpty(m3u8_split[key])) { continue; }\n        if (m3u8_split[key].includes(\"URI=\")) {\n            let KeyURL = /URI=\"(.*)\"/.exec(m3u8_split[key]);\n            if (KeyURL && KeyURL[1]) {\n                KeyURL = GetFile(KeyURL[1]);\n                m3u8_split[key] = m3u8_split[key].replace(/URI=\"(.*)\"/, 'URI=\"' + KeyURL + '\"');\n            }\n        }\n        if (!m3u8_split[key].includes(\"#\")) {\n            m3u8_split[key] = GetFile(m3u8_split[key]);\n        }\n        textarea += m3u8_split[key] + \"\\n\";\n    }\n    writeText(textarea);\n});\n// 播放m3u8\n$(\"#play\").click(function () {\n    if ($(this).data(\"switch\") == \"on\") {\n        $(\"#video\").show();\n        hls.attachMedia($(\"#video\")[0]);\n        $media_file.hide();\n        $(\"#downList\").hide();\n        $(this).html(i18n.close).data(\"switch\", \"off\");\n        hls.on(Hls.Events.MEDIA_ATTACHED, function () {\n            video.play();\n        });\n        return;\n    }\n    $(\"#video\").hide();\n    hls.detachMedia($(\"#video\")[0]);\n    $media_file.show();\n    $(this).html(i18n.play).data(\"switch\", \"on\");\n});\n// 调用m3u8DL下载\n$(\"#m3u8DL\").click(function () {\n    if (_m3u8Url.startsWith(\"blob:\")) {\n        alert(i18n.blobM3u8DLError);\n        return;\n    }\n    const m3u8dlArg = getM3u8DlArg();\n    $m3u8dlArg.val(m3u8dlArg);\n    navigator.clipboard.writeText(m3u8dlArg);\n    const m3u8dl = 'm3u8dl:' + (G.m3u8dl == 1 ? Base64.encode(m3u8dlArg) : m3u8dlArg);\n    if (m3u8dl.length >= 2046) {\n        alert(i18n.M3U8DLparameterLong);\n    }\n    chrome.tabs.update({ url: m3u8dl });\n});\n// 调用自定义协议\n$(\"#invoke\").click(function () {\n    const url = getTemplates(G.invokeText);\n    chrome.tabs.update({ url: url });\n});\n// 复制m3u8DL命令\n$(\"#copyM3U8dl\").click(function () {\n    const m3u8dlArg = getM3u8DlArg();\n    $m3u8dlArg.val(m3u8dlArg);\n    navigator.clipboard.writeText(m3u8dlArg);\n});\n// 显示m3u8DL命令\n$(\"#setM3u8dl\").click(function () {\n    $m3u8dlArg.val(getM3u8DlArg());\n    $m3u8dlArg.slideToggle();\n});\n// 设置载入参数\n$(\"#addParam\").click(function () {\n    $m3u8dlArg.val(getM3u8DlArg());\n});\n$(\"input\").click(function () {\n    $m3u8dlArg.val(getM3u8DlArg());\n});\n$(\"input\").keyup(function () {\n    $m3u8dlArg.val(getM3u8DlArg());\n});\n// 只要音频\n$(\"#onlyAudio\").on(\"change\", function () {\n    if (transmuxer) {\n        $(this).prop(\"checked\", !$(this).prop(\"checked\"));\n        alert(i18n.runningCannotChangeSettings);\n        return;\n    }\n    if ($(this).prop(\"checked\") && !$(\"#mp4\").prop(\"checked\") && !$(\"#ffmpeg\").prop(\"checked\")) {\n        $(\"#mp4\").click();\n    }\n});\n$(\"#mp4\").on(\"change\", function () {\n    if (transmuxer) {\n        $(this).prop(\"checked\", !$(this).prop(\"checked\"));\n        alert(i18n.runningCannotChangeSettings);\n        return;\n    }\n    $(\"#ffmpeg\").prop(\"checked\") && $(\"#ffmpeg\").click();\n    if (!$(this).prop(\"checked\") && !$(\"#ffmpeg\").prop(\"checked\") && $(\"#onlyAudio\").prop(\"checked\")) {\n        $(\"#onlyAudio\").click();\n    }\n});\n$(\"#StreamSaver\").on(\"change\", function () {\n    if (transmuxer) {\n        $(this).prop(\"checked\", !$(this).prop(\"checked\"));\n        alert(i18n.runningCannotChangeSettings);\n        return;\n    }\n    if ($(this).prop(\"checked\")) {\n        $progress.html(`<b>${i18n.streamSaverTip}</b>`);\n        $(\"#ffmpeg\").prop(\"checked\") && $(\"#ffmpeg\").click();\n        $(\"#saveAs\").prop(\"checked\", false);\n    }\n});\n$(\"#ffmpeg\").on(\"change\", function () {\n    if (transmuxer) {\n        $(this).prop(\"checked\", !$(this).prop(\"checked\"));\n        alert(i18n.runningCannotChangeSettings);\n        return;\n    }\n    if ($(this).prop(\"checked\")) {\n        $(\"#mp4\").prop(\"checked\", false);\n        $(\"#StreamSaver\").prop(\"checked\", false);\n        $(\"#saveAs\").prop(\"checked\", false);\n    }\n});\n// 范围 线程数 滚轮调节\nlet debounce2 = undefined;\n$(\"#rangeStart, #rangeEnd, #thread\").on(\"wheel\", function (event) {\n    $(this).blur();\n    let number = $(this).val();\n    number = parseInt(number ? number : 1);\n    number = event.originalEvent.wheelDelta < 0 ? number - 1 : number + 1;\n    if (number < 1 || number > $(this).attr(\"max\")) {\n        return false;\n    }\n    $(this).val(number);\n    $m3u8dlArg.val(getM3u8DlArg());\n    if (this.id == \"thread\") {\n        clearTimeout(debounce2);\n        debounce2 = setTimeout(() => {\n            chrome.storage.local.set({ thread: number });\n        }, 500);\n    }\n    return false;\n});\n$(\"#rangeStart, #rangeEnd, #thread\").keyup(function () {\n    if ($(this).val() == '') {\n        switch (this.id) {\n            case 'rangeStart':\n                $(this).val(1);\n                break;\n            case 'rangeEnd':\n                $(this).val(_fragments.length);\n                break;\n            case 'thread':\n                $(this).val(32);\n                break;\n        }\n    }\n});\n// 储存设置\n$(\"#addParam\").on(\"change\", function () {\n    allOption.addParam = $(\"#addParam\").prop(\"checked\");\n    chrome.storage.local.set(allOption);\n});\n// 上传key\n$(\"#uploadKeyFile\").change(function () {\n    let fileReader = new FileReader();\n    fileReader.onload = function () {\n        if (this.result.byteLength != 16) {\n            $progress.html(`<b>${i18n.incorrectKey}</b>`);\n            return;\n        }\n        $(\"#customKey\").val(ArrayBufferToBase64(this.result));\n        $m3u8dlArg.val(getM3u8DlArg());\n    };\n    let file = $(\"#uploadKeyFile\").prop('files')[0];\n    fileReader.readAsArrayBuffer(file);\n});\n$(\"#uploadKey\").click(function () {\n    $(\"#uploadKeyFile\").click();\n});\nfunction stopRecorder() {\n    $(\"#recorder\").html(i18n.recordLive).data(\"switch\", \"on\");\n    recorder = false;\n    fileStream.close();\n    buttonState(\"#mergeTs\", true);\n    $progress.html(i18n.stopRecording);\n    initDownload();\n}\n// 录制直播\n$(\"#recorder\").click(function () {\n    if ($(this).data(\"switch\") == \"on\") {\n        initDownload(); // 初始化下载变量\n        recorder = true;\n\n        // 只允许流式下载\n        $(\"#StreamSaver\").prop(\"checked\", true);\n        $(\"#ffmpeg\").prop(\"checked\", false);\n        fileStream = createStreamSaver(_fragments[0].url);\n\n        $(this).html(fileStream ? i18n.stopDownload : i18n.download).data(\"switch\", \"off\");\n        $progress.html(i18n.waitingForLiveData);\n        retryCount = parseInt($(\"#retryCount\").val());\n        return;\n    }\n    stopRecorder();\n});\n// 在线下载合并ts\n$(\"#mergeTs\").click(async function () {\n    initDownload(); // 初始化下载变量\n    // 设定起始序号\n    let start = $(\"#rangeStart\").val();\n    if (start.includes(\":\")) {\n        start = timeToIndex(start);\n    } else {\n        start = parseInt(start);\n        start = start ? start - 1 : 0;\n    }\n    // 设定结束序号\n    let end = $(\"#rangeEnd\").val();\n    if (end.includes(\":\")) {\n        end = timeToIndex(end);\n    } else {\n        end = parseInt(end);\n        end = end ? end - 1 : _fragments.length - 1;\n    }\n    // 检查序号\n    if (start == -1 || end == -1) {\n        $progress.html(`<b>${i18n.sNumError}</b>`);\n        return;\n    }\n    if (start > end) {\n        $progress.html(`<b>${i18n.startGTend}</b>`);\n        return;\n    }\n    if (start > _fragments.length - 1 || end > _fragments.length - 1) {\n        $progress.html(`<b>${i18n(\"sNumMax\", _fragments.length)}</b>`);\n        return;\n    }\n    /* 设定自定义密钥和IV */\n    let customKey = $(\"#customKey\").val().trim();\n    if (customKey) {\n        if (isHexKey(customKey)) {\n            customKey = HexStringToArrayBuffer(customKey);\n        } else if (customKey.length == 24 && customKey.slice(-2) == \"==\") {\n            customKey = Base64ToArrayBuffer(customKey);\n            // console.log(customKey);\n        } else if (/^http[s]*:\\/\\/.+/i.test(customKey)) {\n            let flag = false;\n            await $.ajax({\n                url: customKey,\n                xhrFields: { responseType: \"arraybuffer\" }\n            }).fail(function () {\n                flag = true;\n            }).done(function (responseData) {\n                customKey = responseData;\n                $(\"#customKey\").val(ArrayBufferToBase64(customKey));\n                $m3u8dlArg.val(getM3u8DlArg());\n            });\n            if (flag) {\n                $progress.html(`<b>${i18n.keyDownloadFailed}</b>`);\n                return;\n            }\n        } else {\n            $progress.html(`<b>${i18n.incorrectKey}</b>`);\n            return;\n        }\n        for (let i in _fragments) {\n            _fragments[i].encrypted = true;\n            _fragments[i].decryptdata = {};\n            if (!keyContent.get(\"customKey\")) {\n                keyContent.set(\"customKey\", true);\n            }\n            Object.defineProperty(_fragments[i].decryptdata, \"keyContent\", {\n                get: function () { return keyContent.get(\"customKey\"); },\n                configurable: true\n            });\n        }\n        keyContent.forEach(function (value, key) {\n            keyContent.set(key, customKey);\n        });\n    }\n    // 自定义IV\n    let customIV = $(\"#customIV\").val().trim();\n    if (customIV) {\n        customIV = StringToUint8Array(customIV);\n        for (let i in _fragments) {\n            _fragments[i].decryptdata.iv = customIV;\n        }\n    }\n    skipDecrypt = $(\"#skipDecrypt\").prop(\"checked\");    // 是否跳过解密\n    $progress.html(`0/${end - start + 1}`); // 进度显示\n\n    // 估算检查文件大小\n    if (!$(\"#StreamSaver\").prop(\"checked\") && estimateFileSize > G.chromeLimitSize && confirm(i18n(\"fileTooLargeStream\", [\"2G\"]))) {\n        $(\"#StreamSaver\").prop(\"checked\", true);\n    }\n\n    // 流式下载\n    if ($(\"#StreamSaver\").prop(\"checked\")) {\n        fileStream = createStreamSaver(_fragments[0].url);\n        downloadNew(start, end + 1);\n        $(\"#ffmpeg\").prop(\"checked\", false);\n        $(\"#saveAs\").prop(\"checked\", false);\n        $(\"#stopDownload\").show();\n        return;\n    }\n    $(\"#stopDownload\").show();\n    downloadNew(start, end + 1);\n});\n\n// 添加ts 参数\n$(\"#tsAddArg\").click(function () {\n    if (tsAddArg != null) {\n        window.location.href = window.location.href.replace(/&tsAddArg=[^&]*/g, \"\");\n        return;\n    }\n    //获取m3u8参数\n    let m3u8Arg = new RegExp(\"\\\\.m3u8\\\\?([^\\n]*)\").exec(_m3u8Url);\n    if (m3u8Arg) {\n        m3u8Arg = m3u8Arg[1];\n    }\n    const arg = window.prompt(i18n.addParameters, m3u8Arg ?? \"\");\n    if (arg != null) {\n        window.location.href += \"&tsAddArg=\" + encodeURIComponent(arg);\n    }\n});\n// 下载进度\n$(\"#downProgress\").click(function () {\n    $media_file.hide();\n    $(\"#downList\").show();\n});\n// 设置请求头\n$(document).on(\"click\", \"#setRequestHeaders, #setRequestHeadersError\", function () {\n    const arg = window.prompt(i18n.addParameters, JSON.stringify(requestHeaders));\n    if (arg != null) {\n        params.delete(\"requestHeaders\");\n        params.append(\"requestHeaders\", arg);\n        window.location.href = window.location.origin + window.location.pathname + \"?\" + params.toString();\n    }\n});\n\n// #EXT-X-DISCONTINUITY 范围选择\n$('#cc').change(function () {\n    if (this.value == \"0\") {\n        $(\"#rangeStart\").val(1);\n        $(\"#rangeEnd\").val(_fragments.length);\n        return;\n    }\n    const range = this.value.split(\"-\");\n    $(\"#rangeStart\").val(+range[0]);\n    $(\"#rangeEnd\").val(+range[1]);\n});\n\n// 折叠\n$(\"details summary\").click(function () {\n    allOption.fold = !$(\"details\")[0].open;\n    chrome.storage.local.set(allOption);\n});\n\n// 发送到在线ffmpeg\n$(\"#sendFfmpeg\").click(function () {\n    isSendFfmpeg = true;\n    $(\"#StreamSaver\").prop(\"checked\", false);\n    $(\"#mergeTs\").click();\n});\n\n// 找到真密钥\n$(\"#searchingForRealKey\").click(function () {\n    const keys = $('#maybeKey option').map(function () {\n        return $(this).val();\n    }).get();\n    keys.shift();   // 删除提示\n\n    let iv = _fragments[0].decryptdata?.iv;\n    if (!iv) {\n        iv = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, _fragments[0].sn]);\n    }\n\n    const customIV = $(\"#customIV\").val().trim();\n    if (customIV) {\n        iv = StringToUint8Array(customIV);\n    }\n    $(\"#searchingForRealKey\").html(i18n.verifying);\n\n    const check = (buffer) => {\n        const uint8Array = new Uint8Array(buffer);\n        // fmp4\n        if ((uint8Array[4] === 0x73 || uint8Array[4] === 0x66) && uint8Array[5] === 0x74 && uint8Array[6] === 0x79 && uint8Array[7] === 0x70) {\n            return true;\n        }\n        // moof\n        if (uint8Array[4] === 0x6d && uint8Array[5] === 0x6f && uint8Array[6] === 0x6f && uint8Array[7] === 0x66) {\n            return true;\n        }\n        // webm\n        if (uint8Array[0] === 0x1a && uint8Array[1] === 0x45 && uint8Array[2] === 0xdf && uint8Array[3] === 0xa3) {\n            return true;\n        }\n        // mp3 ID3\n        if (uint8Array[0] === 0x49 && uint8Array[1] === 0x44 && uint8Array[2] === 0x33) {\n            return true;\n        }\n        // mp3 (MPEG audio frame header)\n        if (uint8Array[0] === 0xff && (uint8Array[1] & 0xe0) === 0xe0) {\n            return true;\n        }\n        // aac (ADTS header)\n        if (uint8Array[0] === 0xff && (uint8Array[1] & 0xf0) === 0xf0) {\n            return true;\n        }\n        // ts\n        const maxCheckLength = Math.min(512, uint8Array.length);\n        for (let i = 0; i < maxCheckLength; i++) {\n            if (uint8Array[i] === 0x47 && (i + 188) < uint8Array.length && uint8Array[i + 188] === 0x47) {\n                return true;\n            }\n        }\n    }\n    const decryptor = new AESDecryptor();\n    fetch(_fragments[0].url)\n        .then(response => response.arrayBuffer())\n        .then(function (buffer) {\n            if (check(buffer)) {\n                $(\"#searchingForRealKey\").html(i18n.searchingForRealKey);\n                alert(i18n.noKeyIsRequired);\n                return;\n            }\n            for (let key of keys) {\n                try {\n                    decryptor.expandKey(Base64ToArrayBuffer(key));\n                    const testBuffer = decryptor.decrypt(buffer, 0, iv.buffer, true);\n                    // 检查是否解密成功\n                    if (check(testBuffer)) {\n                        if (!prompt(i18n.searchingForRealKey, key)) { continue; }\n                        $(\"#searchingForRealKey\").html(i18n.searchingForRealKey);\n                        $(\"#customKey\").val(key);\n                        $('#maybeKey select').val(key);\n                        $m3u8dlArg.val(getM3u8DlArg());\n                        return;\n                    }\n                } catch (error) {\n                    console.log(error);\n                }\n            };\n            $(\"#searchingForRealKey\").html(i18n.realKeyNotFound);\n        }).catch(function (error) {\n            $(\"#searchingForRealKey\").html(i18n.dataFetchFailed);\n            console.log(error);\n        });\n});\n\n/**\n * 调用新下载器的方法\n * @param {number} start 下载范围 开始索引\n * @param {number} end 下载范围 结束索引\n */\nfunction downloadNew(start = 0, end = _fragments.length) {\n\n    $(\"#video\").hide();\n    hls.detachMedia($(\"#video\")[0]);\n\n    // 避免重复下载\n    buttonState(\"#mergeTs\", false);\n\n    // 切片下载器\n    const down = new Downloader(_fragments, parseInt($(\"#thread\").val()));\n\n    // 储存切片所需 DOM 提高性能\n    const itemDOM = new Map();\n\n    // 解密函数\n    down.setDecrypt(function (buffer, fragment) {\n        return new Promise(function (resolve, reject) {\n            // 跳过解密 录制模式 切片不存在加密 跳过解密 直接返回\n            if (skipDecrypt || recorder || !fragment.encrypted || !fragment.decryptdata) {\n                /**\n                 * (!fragment.live || fragment.index == 0)\n                 * 如果是直接下载直播流 只有第一个切片才会添加MAP 否则每个切片都添加MAP视频无法播放。\n                 */\n                if (fragment.initSegment && (!fragment.live || fragment.index == 0)) {\n                    buffer = addInitSegmentData(buffer, fragment.initSegment);\n                }\n                resolve(buffer);\n                return;\n            }\n            // 载入密钥 开始解密\n            try {\n                decryptor.expandKey(fragment.decryptdata.keyContent);\n                const iv = fragment.decryptdata.iv ?? new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, fragment.sn]);\n                buffer = decryptor.decrypt(buffer, 0, iv.buffer, true);\n            } catch (e) {\n                $progress.html(i18n.decryptionError + e);\n                down.stop();\n                buttonState(\"#mergeTs\", true);\n                console.log(e);\n                reject(e);\n                return;\n            }\n            // 如果存在MAP切片 把MAP整合进buffer\n            // MAP切片不需要解密\n            if (fragment.initSegment) {\n                buffer = addInitSegmentData(buffer, fragment.initSegment);\n            }\n            resolve(buffer);\n        });\n    });\n    // 转码函数 如果存在down.mapTag 跳过转码\n    if (downSet.mp4 && !down.mapTag) {\n        let tempBuffer = null;\n        let head = true;\n        transmuxer = new muxjs.mp4.Transmuxer({ keepOriginalTimestamps: false, remux: !downSet.onlyAudio });    // mux.js 对象\n        transmuxer.on('data', function (segment) {\n            if (downSet.onlyAudio && segment.type != \"audio\") { return; }\n            if (head) {\n                let data = new Uint8Array(segment.initSegment.byteLength + segment.data.byteLength);\n                data.set(segment.initSegment, 0);\n                data.set(segment.data, segment.initSegment.byteLength);\n                tempBuffer = fixFileDuration(data, down.totalDuration);\n                return;\n            }\n            tempBuffer = segment.data;\n        });\n        down.setTranscode(async function (buffer, fragment) {\n            head = fragment.index == 0;\n            transmuxer.push(new Uint8Array(buffer));\n            transmuxer.flush();\n            return tempBuffer ? tempBuffer.buffer : buffer;\n        });\n    }\n    // 下载错误\n    down.on('downloadError', function (fragment, error) {\n        $(\"#ForceDownload\").show(); // 强制下载\n        $(\"#errorDownload\").show(); // 重下所有失败项\n\n        // const $dom = $(`#downItem${fragment.index}`);\n        // $dom.find(\".percentage\").addClass('error').html(i18n.downloadFailed);\n        itemDOM.get(fragment.index).percentage.addClass('error').html(i18n.downloadFailed);\n        $button = itemDOM.get(fragment.index).button;\n        $button.html(i18n.retryDownload).data(\"action\", \"start\");\n        if (down.isErrorItem(fragment)) {\n            const count = parseInt($button.data(\"count\")) + 1;\n            $button.data(\"count\", count).html(`${i18n.retryDownload}(${count})`);\n        } else {\n            $button.data(\"count\", 0);\n        }\n    });\n    // 切片下载完成\n    down.on('completed', function (buffer, fragment) {\n        if (recorder) {\n            $progress.html(i18n.waitingForLiveData);\n            downDuration += fragment.duration;\n            $fileDuration.html(i18n.recordingDuration + \":\" + secToTime(downDuration));\n            return;\n        }\n        itemDOM.get(fragment.index).root.remove();\n        $progress.html(`${down.success}/${down.total}`);\n        $fileSize.html(i18n.downloaded + \":\" + byteToSize(down.buffersize));\n        $fileDuration.html(i18n.downloadedVideoLength + \":\" + secToTime(down.duration));\n    });\n    // 全部下载完成\n    down.on('allCompleted', async function (buffer) {\n        if (recorder) { return; }\n        $(\"#stopDownload\").hide();\n        if (fileStream) {\n            fileStream.close();\n            fileStream = undefined;\n            $progress.html(i18n.downloadComplete);\n        } else {\n            mergeTsNew(down);\n        }\n        transmuxer?.off && transmuxer.off('data');\n        transmuxer = undefined;\n\n        $(\"#ForceDownload\").hide(); // 强制下载\n        $(\"#errorDownload\").hide(); // 重下所有失败项\n\n        buttonState(\"#mergeTs\", true);\n    });\n    // 单个项目下载进度\n    let lastEmitted = Date.now();\n    down.on('itemProgress', function (fragment, state, receivedLength, contentLength) {\n        if (Date.now() - lastEmitted >= 233) {\n            itemDOM.get(fragment.index).percentage.html((receivedLength / contentLength * 100).toFixed(2) + \"%\");\n            lastEmitted = Date.now();\n        }\n    });\n    if (fileStream) {\n        down.on('sequentialPush', function (buffer) {\n            fileStream && fileStream.write(new Uint8Array(buffer));\n        });\n    }\n    down.on('error', function (error) {\n        console.log(error);\n    });\n    down.on('stop', function (fragment, error) {\n        console.log(error);\n    });\n\n    // 开始下载\n    down.start(start, end);\n\n    // 单项进度\n    const tempDOM = $(\"<div>\");\n    down.fragments.forEach((fragment) => {\n        const html = $(`<div id=\"downItem${fragment.index}\">\n            <a href=\"${fragment.url}\" target=\"_blank\">${fragment.url}</a>\n            <div class=\"itemProgress\">\n            <span>${i18n.downloadProgress}: </span>\n            <span class=\"percentage\">${i18n.waitDownload}</span>\n            <button data-action=\"stop\">${i18n.stopDownload}</button>\n            </div>\n        </div>`);\n\n        const $button = html.find(\"button\");\n\n        // 保存进程 DOM 更新下载进度提升性能\n        itemDOM.set(fragment.index, {\n            root: html,\n            percentage: html.find(\".percentage\"),\n            button: $button,\n        });\n\n        $button.click(function () {\n            html.find(\".percentage\").removeClass('error');\n            if ($(this).data(\"action\") == \"stop\") {\n                down.stop(fragment.index);\n                down.downloader();  // 停止当前下载器 重新开一个下载器保持线程数量\n                $(this).html(i18n.retryDownload).data(\"action\", \"start\");\n            } else {\n                down.downloader(fragment);\n                $(this).html(i18n.stopDownload).data(\"action\", \"stop\");\n            }\n        });\n        tempDOM.append(html);\n    });\n    $media_file.hide();\n    $(\"#downList\").html(\"\").show().append(tempDOM);\n\n    // 强制下载\n    $(\"#ForceDownload\").off(\"click\").click(function () {\n        mergeTsNew(down);\n    });\n\n    // 重新下载\n    $(\"#errorDownload\").off(\"click\").click(function () {\n        down.errorItem.forEach(function (fragment, index) {\n            const button = $(`#downItem${fragment.index} button`);\n            setTimeout(() => {\n                button.click();\n            }, index * 233);\n        });\n    });\n\n    // 停止下载\n    $(\"#stopDownload\").off(\"click\").click(function () {\n        down.stop();\n        setTimeout(() => {\n            fileStream && fileStream.close();\n            $progress.html(i18n.stopDownload);\n            $(\"#stopDownload\").hide();\n            buttonState(\"#mergeTs\", true);\n            $fileSize.html(\"\");\n            $fileDuration.html(\"\");\n        }, 1000);\n    });\n}\nfunction addInitSegmentData(buffer, initSegment) {\n    let initSegmentData = initData.get(initSegment.url);\n    if (!initSegmentData && initSegment.data) {\n        initSegmentData = initSegment.data.buffer;\n    }\n    const initLength = initSegmentData.byteLength;\n    const newData = new Uint8Array(initLength + buffer.byteLength);\n    newData.set(new Uint8Array(initSegmentData), 0);\n    newData.set(new Uint8Array(buffer), initLength);\n    return newData.buffer;\n}\n\n// 合并下载\nfunction mergeTsNew(down) {\n    $progress.html(i18n.merging);\n\n    // 创建Blob\n    const fileBlob = new Blob(down.buffer, { type: down.transcode ? \"video/mp4\" : \"video/MP2T\" });\n\n    // 默认后缀\n    let ext = (down.mapTag && !down.mapTag.startsWith(\"data:\") ? down.mapTag : down.fragments[0].url).split(\"/\").pop();\n    ext = ext.split(\"?\").shift();\n    ext = ext.split(\".\").pop();\n    ext = ext ? ext : \"ts\";\n    ext = down.transcode ? \"mp4\" : ext;\n\n    let fileName = \"\";\n    const customFilename = $('#customFilename').val().trim();\n    if (customFilename) {\n        fileName = customFilename;\n    } else if (_fileName) {\n        fileName = _fileName;\n    } else {\n        fileName = GetFileName(_m3u8Url);\n    }\n    // 删除目录\n    // fileName = fileName.split(\"/\");\n    // fileName = fileName.length > 1 ? fileName.pop() : fileName.join(\"\");\n    // 删除后缀\n    let originalExt = null;\n    if (/\\.[a-zA-Z0-9]{1,4}$/.test(fileName)) {\n        fileName = fileName.split(\".\");\n        originalExt = fileName.pop();\n        fileName = fileName.join(\".\");\n    }\n    // 发送到ffmpeg\n    if ($(\"#ffmpeg\").prop(\"checked\") || _ffmpeg || isSendFfmpeg) {\n        /**\n         * 大于1.8G 不使用ffmpeg直接下载\n         * chrome每个进程限制2G内存 处理2G视频可能导致超过限制。1.8G是安全值。\n         * firefox 不受影响\n         */\n        if (!G.isFirefox && fileBlob.size > G.chromeLimitSize) {\n            $progress.html(i18n(\"fileTooLarge\", [\"2G\"]));\n            apiDownload(fileBlob, fileName, ext);\n            down.destroy();\n            return;\n        }\n        if (customFilename && originalExt) {\n            fileName += \".\" + originalExt;\n        } else if (ext != \"mp4\" && ext != \"mp3\") {\n            fileName = fileName + \".mp4\";\n        } else {\n            fileName = fileName + \".\" + ext;\n        }\n        let action = $(\"#onlyAudio\").prop(\"checked\") ? \"onlyAudio\" : \"transcode\";\n        if (_ffmpeg) {\n            action = _ffmpeg;\n        }\n        if (isSendFfmpeg) {\n            action = \"addFile\";\n            isSendFfmpeg = false;\n        }\n        const data = {\n            Message: \"catCatchFFmpeg\",\n            action: action,\n            files: [{ data: G.isFirefox ? fileBlob : URL.createObjectURL(fileBlob), name: `memory${new Date().getTime()}.${ext}` }],\n            title: fileName,\n            output: fileName,\n            name: \"memory\" + new Date().getTime() + \".\" + ext,\n            active: G.isMobile || !autoDown,\n            tabId: currentTabId,\n        };\n        if (_quantity) {\n            data.quantity = parseInt(_quantity);\n        }\n        if (_taskId) {\n            data.taskId = _taskId;\n        }\n        chrome.runtime.sendMessage(data, function (response) {\n            if (!chrome.runtime?.lastError && response && response == \"ok\") {\n                $progress.html(i18n.sendFfmpeg);\n                buttonState(\"#mergeTs\", true);\n                return;\n            }\n            apiDownload(fileBlob, fileName, ext);\n            down.destroy();\n            return;\n        });\n    } else {\n        apiDownload(fileBlob, fileName, ext);\n        down.destroy();\n    }\n}\nfunction apiDownload(fileBlob, fileName, ext) {\n    chrome.downloads.download({\n        url: URL.createObjectURL(fileBlob),\n        filename: fileName + \".\" + ext,\n        saveAs: $(\"#saveAs\").prop(\"checked\")\n    }, function (downloadId) {\n        if (downloadId) {\n            downId = downloadId;\n            $(\".openDir\").show();\n            buttonState(\"#mergeTs\", true);\n        } else if (chrome.runtime?.lastError?.message && chrome.runtime.lastError.message == 'Invalid filename') {\n            apiDownload(fileBlob, stringModify(fileName), ext);\n            return;\n        }\n    });\n}\n\n// 初始化下载变量\nfunction initDownload() {\n    $fileSize.html(\"\");\n    downDuration = 0;   // 初始化时长\n    $fileDuration.html(\"\");\n    recorderLast = \"\";  // 录制最后下载的url\n    fileStream = undefined; // 流式下载 文件流\n    // 转码工具初始化\n    transmuxer = undefined;\n    // 避免下载中途 更改设置 暂时储存下载配置\n    downSet.mp4 = $(\"#mp4\").prop(\"checked\");\n    downSet.onlyAudio = $(\"#onlyAudio\").prop(\"checked\");\n}\n\n// 流式下载\nfunction createStreamSaver(url) {\n    streamSaver.mitm = G.streamSaverConfig.url;\n    const ext = $(\"#mp4\").prop(\"checked\") ? \"mp4\" : GetExt(url);\n    return streamSaver.createWriteStream(`${GetFileName(url)}.${ext}`).getWriter();\n}\nwindow.addEventListener('beforeunload', function () {\n    fileStream && fileStream.abort();\n});\nwindow.onbeforeunload = function (event) {\n    if (fileStream) {\n        event.returnValue = i18n.streamOnbeforeunload;\n    }\n}\nfunction getTemplates(text) {\n    // 也许请求头被更改\n    if (Object.keys(requestHeaders).length) {\n        _data.requestHeaders = { ...requestHeaders };\n        _data.initiator = requestHeaders?.referer ?? _initiator;\n    }\n    return templates(text, _data);\n}\nfunction getM3u8DlREArg() {\n    let m3u8dlArg = G.m3u8dlArg;\n    const addParam = $(\"#addParam\").prop(\"checked\");    // 是否添加参数\n    const customFilename = $(\"#customFilename\").val().trim();   // 自定义文件名\n    if (customFilename && addParam) {\n        m3u8dlArg = m3u8dlArg.replace(/--save-name \"[^\"]+\"/g, `--save-name \"${customFilename}\"`);\n    }\n    m3u8dlArg = getTemplates(m3u8dlArg);\n    if (!addParam) { return m3u8dlArg; }\n\n    // 线程处理\n    const tsThread = $(\"#thread\").val();  // 线程数量\n    const threadCountRegex = /(--thread-count\\s+)\\d+/;\n    if (m3u8dlArg.match(threadCountRegex)) {\n        m3u8dlArg = m3u8dlArg.replace(threadCountRegex, `\\$1${tsThread}`);\n    } else {\n        m3u8dlArg += ` --thread-count ${tsThread}`;\n    }\n\n    // 范围处理\n    let rangeStart = $(\"#rangeStart\").val();\n    rangeStart = rangeStart.includes(\":\") ? rangeStart : rangeStart - 1;\n    let rangeEnd = $(\"#rangeEnd\").val();\n    rangeEnd = rangeEnd.includes(\":\") ? rangeEnd : rangeEnd - 1;\n    if (rangeStart != 0 || rangeEnd != _fragments.length - 1) {\n        m3u8dlArg += ` --custom-range \"${rangeStart}-${rangeEnd}\"`\n    }\n\n    // 自定义密钥\n    let customKey = $(\"#customKey\").val().trim();  // 自定义密钥\n    if (customKey) {\n        m3u8dlArg += ` --custom-hls-key \"${customKey}\"`;\n    }\n\n    // 自定义IV\n    const customIV = $(\"#customIV\").val();  // 自定义IV\n    m3u8dlArg += customIV ? ` --custom-hls-iv \"${customIV}\"` : \"\";\n\n    // 只要音频\n    // const onlyAudio = $(\"#onlyAudio\").prop(\"checked\");\n    // m3u8dlArg += onlyAudio ? ` --drop-video all` : \"\";\n\n    return m3u8dlArg;\n}\nfunction getM3u8DlArg() {\n    if (G.m3u8dl == 2) { return getM3u8DlREArg(); }\n\n    let m3u8dlArg = G.m3u8dlArg;\n    const addParam = $(\"#addParam\").prop(\"checked\");\n    // 自定义文件名\n    const customFilename = $(\"#customFilename\").val().trim();\n    if (customFilename && addParam) {\n        m3u8dlArg = m3u8dlArg.replace(/--saveName \"[^\"]+\"/g, `--saveName \"${customFilename}\"`);\n    }\n    m3u8dlArg = getTemplates(m3u8dlArg);\n\n    if (!addParam) { return m3u8dlArg; }\n\n    if (m3u8dlArg.includes(\"--maxThreads\")) {\n        m3u8dlArg = m3u8dlArg.replace(/--maxThreads \"?[0-9]+\"?/g, \"\");\n    }\n    const tsThread = $(\"#thread\").val();  // 线程数量\n    m3u8dlArg += ` --maxThreads \"${tsThread}\"`\n\n    let rangeStart = $(\"#rangeStart\").val();\n    rangeStart = rangeStart.includes(\":\") ? rangeStart : rangeStart - 1;\n    let rangeEnd = $(\"#rangeEnd\").val();\n    rangeEnd = rangeEnd.includes(\":\") ? rangeEnd : rangeEnd - 1;\n    m3u8dlArg += ` --downloadRange \"${rangeStart}-${rangeEnd}\"`\n\n    let customKey = $(\"#customKey\").val().trim();  // 自定义密钥\n    if (customKey) {\n        if (isHexKey(customKey)) {\n            customKey = HexStringToArrayBuffer(customKey);\n            customKey = ArrayBufferToBase64(customKey);\n            m3u8dlArg += ` --useKeyBase64 \"${customKey}\"`;\n        } else if (customKey.length == 24 && customKey.slice(-2) == \"==\") {\n            m3u8dlArg += ` --useKeyBase64 \"${customKey}\"`;\n        }\n    }\n    const customIV = $(\"#customIV\").val();  // 自定义IV\n    m3u8dlArg += customIV ? ` --useKeyIV \"${customIV}\"` : \"\";\n    // 只要音频\n    const onlyAudio = $(\"#onlyAudio\").prop(\"checked\");\n    m3u8dlArg += onlyAudio ? ` --enableAudioOnly` : \"\";\n\n    return m3u8dlArg;\n}\n\n/**\n * 时间格式转为切片序号\n * @param {string} time\n * @returns {number}\n */\nfunction timeToIndex(time) {\n    let totalSeconds = time.split(\":\").reduce((acc, time) => 60 * acc + +time);\n    return _fragments.findIndex(fragment => (totalSeconds -= fragment.duration) < 0);\n}\n// 写入ts链接\nfunction writeText(text) {\n    $media_file.show();\n    $(\"#downList\").hide();\n    if (typeof text == \"object\") {\n        let url = [];\n        for (let key in text) {\n            url.push(text[key].url + \"\\n\");\n        }\n        $media_file.val(url.join(\"\\n\"));\n        $media_file.data(\"type\", \"link\");\n        return;\n    }\n    $media_file.val(text);\n    $media_file.data(\"type\", \"m3u8\");\n}\n// 获取文件名\nfunction GetFile(str) {\n    str = str.split(\"?\")[0];\n    if (str.substr(0, 5) != \"data:\" && str.substr(0, 4) != \"skd:\") {\n        return str.split(\"/\").pop();\n    }\n    return str;\n}\n// 获得不带扩展的文件名\nfunction GetFileName(url) {\n    //hiaming 增加自定义名字功能\n    if ($('#customFilename').val()) {\n        return $('#customFilename').val().trim();\n    }\n    if (G.TitleName && _title) {\n        if (_title.length >= 150) {\n            return _title.substring(_title.length - 150);\n        }\n        return _title;\n    }\n    url = GetFile(url);\n    url = url.split(\".\");\n    url.length > 1 && url.pop();\n    url = url.join(\".\");\n    if (url.length >= 150) {\n        url = url.substring(url.length - 150);\n    }\n    if (url.length == 0) {\n        url = \"NULL\";\n    }\n    return stringModify(url);\n}\n// 获取扩展名\nfunction GetExt(url) {\n    let fileName = GetFile(url);\n    let str = fileName.split(\".\");\n    if (str.length == 1) {\n        return undefined;\n    }\n    let ext = str[str.length - 1];\n    ext = ext.match(/[0-9a-zA-Z]*/);\n    return ext[0].toLowerCase();\n}\n// 按钮状态\nfunction buttonState(obj = \"#mergeTs\", state = true) {\n    if (state) {\n        $(obj).prop(\"disabled\", false).removeClass(\"no-drop\");\n        return;\n    }\n    $(obj).prop(\"disabled\", true).addClass(\"no-drop\");\n}\n// ArrayBuffer 转 16进制字符串\nfunction ArrayBufferToHexString(buffer) {\n    let binary = \"\";\n    let bytes = new Uint8Array(buffer);\n    for (let i = 0; i < bytes.byteLength; i++) {\n        binary += ('00' + bytes[i].toString(16)).slice(-2);\n    }\n    return binary;\n}\n// ArrayBuffer 转 Base64\nfunction ArrayBufferToBase64(buffer) {\n    let binary = \"\";\n    let bytes = new Uint8Array(buffer);\n    for (let i = 0; i < bytes.byteLength; i++) {\n        binary += String.fromCharCode(bytes[i]);\n    }\n    return btoa(binary);\n}\n// Base64 转 ArrayBuffer\nfunction Base64ToArrayBuffer(base64) {\n    let binary_string = atob(base64);\n    let len = binary_string.length;\n    let bytes = new Uint8Array(len);\n    for (let i = 0; i < len; i++) {\n        bytes[i] = binary_string.charCodeAt(i);\n    }\n    return bytes.buffer;\n}\n// 字符串 转 ArrayBuffer\nfunction StringToArrayBuffer(str) {\n    let bytes = new Uint8Array(str.length);\n    for (let i = 0; i < str.length; i++) {\n        bytes[i] = str.charCodeAt(i);\n    }\n    return bytes.buffer;\n}\n// 16进制字符串 转 ArrayBuffer\nfunction HexStringToArrayBuffer(hex) {\n    let typedArray = new Uint8Array(hex.match(/[\\da-f]{2}/gi).map(function (h) {\n        return parseInt(h, 16)\n    }));\n    return typedArray.buffer\n}\n// 字符串 转 Uint8Array\nfunction StringToUint8Array(str) {\n    str = str.replace(\"0x\", \"\");\n    return new Uint8Array(HexStringToArrayBuffer(str));\n}\n/* 修正mp4文件显示时长 */\nfunction fixFileDuration(data, duration) {\n    // duration = parseInt(duration);\n    let mvhdBoxDuration = duration * 90000;\n    function getBoxDuration(data, duration, index) {\n        let boxDuration = \"\";\n        index += 16;    // 偏移量 16 为timescale\n        boxDuration += data[index].toString(16);\n        boxDuration += data[++index].toString(16);\n        boxDuration += data[++index].toString(16);\n        boxDuration += data[++index].toString(16);\n        boxDuration = parseInt(boxDuration, 16);\n        boxDuration *= duration;\n        return boxDuration;\n    }\n    for (let i = 0; i < data.length; i++) {\n        // mvhd\n        if (data[i] == 0x6D && data[i + 1] == 0x76 && data[i + 2] == 0x68 && data[i + 3] == 0x64) {\n            mvhdBoxDuration = getBoxDuration(data, duration, i);   // 获得 timescale\n            data[i + 11] = 0;   //删除创建日期\n            i += 20;    // mvhd 偏移20 为duration\n            data[i] = (mvhdBoxDuration & 0xFF000000) >> 24;\n            data[++i] = (mvhdBoxDuration & 0xFF0000) >> 16;\n            data[++i] = (mvhdBoxDuration & 0xFF00) >> 8;\n            data[++i] = mvhdBoxDuration & 0xFF;\n            continue;\n        }\n        // tkhd\n        if (data[i] == 0x74 && data[i + 1] == 0x6B && data[i + 2] == 0x68 && data[i + 3] == 0x64) {\n            i += 24;    // tkhd 偏移24 为duration\n            data[i] = (mvhdBoxDuration & 0xFF000000) >> 24;\n            data[++i] = (mvhdBoxDuration & 0xFF0000) >> 16;\n            data[++i] = (mvhdBoxDuration & 0xFF00) >> 8;\n            data[++i] = mvhdBoxDuration & 0xFF;\n            continue;\n        }\n        // mdhd\n        if (data[i] == 0x6D && data[i + 1] == 0x64 && data[i + 2] == 0x68 && data[i + 3] == 0x64) {\n            let mdhdBoxDuration = getBoxDuration(data, duration, i);   // 获得 timescale\n            i += 20;    // mdhd 偏移20 为duration\n            data[i] = (mdhdBoxDuration & 0xFF000000) >> 24;\n            data[++i] = (mdhdBoxDuration & 0xFF0000) >> 16;\n            data[++i] = (mdhdBoxDuration & 0xFF00) >> 8;\n            data[++i] = mdhdBoxDuration & 0xFF;\n            continue;\n        }\n        //  mdat 之后是媒体数据 结束头部修改\n        if (data[i] == 0x6D && data[i + 1] == 0x64 && data[i + 2] == 0x61 && data[i + 3] == 0x74) {\n            return data;\n        }\n    }\n    return data;\n}\nfunction isHexKey(str) {\n    return /^[0-9a-fA-F]{32}$/.test(str);\n}\n// m3u8文件内容加入bashUrl\nfunction addBashUrl(baseUrl, m3u8Text) {\n    let m3u8_split = m3u8Text.split(\"\\n\");\n    m3u8Text = \"\";\n    for (let key in m3u8_split) {\n        if (isEmpty(m3u8_split[key])) { continue; }\n        if (m3u8_split[key].includes(\"URI=\")) {\n            let KeyURL = /URI=\"(.*)\"/.exec(m3u8_split[key]);\n            if (KeyURL && KeyURL[1] && !/^[\\w]+:.+/i.test(KeyURL[1])) {\n                m3u8_split[key] = m3u8_split[key].replace(/URI=\"(.*)\"/, 'URI=\"' + baseUrl + KeyURL[1] + '\"');\n            }\n        }\n        if (!m3u8_split[key].includes(\"#\") && !/^[\\w]+:.+/i.test(m3u8_split[key])) {\n            m3u8_split[key] = baseUrl + m3u8_split[key];\n        }\n        m3u8Text += m3u8_split[key] + \"\\n\";\n    }\n    return m3u8Text;\n}\n\nfunction highlight() {\n    autoDown = false;\n    chrome.tabs.getCurrent(function name(params) {\n        chrome.tabs.highlight({ tabs: params.index });\n    });\n}\n\nlet autoMergeTimer = null;\nfunction autoMerge() {\n    if (!autoDown) { return; }\n    clearTimeout(autoMergeTimer);\n    autoMergeTimer = setTimeout(() => {\n        $(\"#mergeTs\").click();\n    }, 1000);\n}\n\n// 接收 catCatchFFmpegResult\nchrome.runtime.onMessage.addListener(function (Message, sender, sendResponse) {\n    if (!Message.Message || Message.Message != \"catCatchFFmpegResult\" || Message.state != \"ok\" || currentTabId == 0 || Message.tabId != currentTabId) { return; }\n    setTimeout(() => {\n        $(\"#autoClose\").prop(\"checked\") && closeTab();\n    }, Math.ceil(Math.random() * 500));\n});\n"
  },
  {
    "path": "js/media-control.js",
    "content": "(function () {\n    let _tabId = -1;   // 选择的页面ID\n    let _index = -1;    //选择的视频索引\n    let VideoTagTimer;  // 获取所有视频标签的定时器\n    let VideoStateTimer;  // 获取所有视频信息的定时器\n    let compareTab = [];\n    let compareVideo = [];\n\n    function setVideoTagTimer() {\n        clearInterval(VideoTagTimer);\n        VideoTagTimer = setInterval(getVideoTag, 1000);\n    }\n    function getVideoTag() {\n        chrome.tabs.query({ windowType: \"normal\" }, function (tabs) {\n            let videoTabList = [];\n            for (let tab of tabs) {\n                videoTabList.push(tab.id);\n            }\n            if (compareTab.toString() == videoTabList.toString()) {\n                return;\n            }\n            compareTab = videoTabList;\n            // 列出所有标签\n            for (let tab of tabs) {\n                if ($(\"#option\" + tab.id).length == 1) { continue; }\n                $(\"#videoTabIndex\").append(`<option value='${tab.id}' id=\"option${tab.id}\">${stringModify(tab.title)}</option>`);\n            }\n            // 删除没有媒体的标签. 异步的原因，使用一个for去处理无法保证标签顺序一致\n            for (let tab of videoTabList) {\n                chrome.tabs.sendMessage(tab, { Message: \"getVideoState\", index: 0 }, { frameId: 0 }, function (state) {\n                    if (chrome.runtime.lastError || state.count == 0) {\n                        $(\"#option\" + tab).remove();\n                        return;\n                    }\n                    $(\"#videoTabTips\").remove();\n                    if (tab == G.tabId && _tabId == -1) {\n                        _tabId = tab;\n                        $(\"#videoTabIndex\").val(tab);\n                    }\n                });\n            }\n        });\n    }\n    function setVideoStateTimer() {\n        clearInterval(VideoStateTimer);\n        VideoStateTimer = setInterval(getVideoState, 500);\n    }\n    function getVideoState(setSpeed = false) {\n        if (_tabId == -1) {\n            let currentTabId = $(\"#videoTabIndex\").val();\n            if (currentTabId == -1) { return; }\n            _tabId = parseInt(currentTabId);\n        }\n        chrome.tabs.sendMessage(_tabId, { Message: \"getVideoState\", index: _index }, { frameId: 0 }, function (state) {\n            if (chrome.runtime.lastError || state.count == 0) { return; }\n            if (state.type == \"audio\") {\n                $(\"#pip\").hide();\n                $(\"#screenshot\").hide();\n            }\n            $(\"#volume\").val(state.volume);\n            if (state.duration && state.duration != Infinity) {\n                $(\"#timeShow\").html(secToTime(state.currentTime) + \" / \" + secToTime(state.duration));\n                $(\"#time\").val(state.time);\n            }\n            state.paused ? $(\"#control\").html(i18n.play).data(\"switch\", \"play\") : $(\"#control\").html(i18n.pause).data(\"switch\", \"pause\");\n            state.speed == 1 ? $(\"#speed\").html(i18n.speedPlayback).data(\"switch\", \"speed\") : $(\"#speed\").html(i18n.normalPlay).data(\"switch\", \"normal\");\n            $(\"#loop\").prop(\"checked\", state.loop);\n            $(\"#muted\").prop(\"checked\", state.muted);\n            if (setSpeed && state.speed != 1) {\n                $(\"#playbackRate\").val(state.speed);\n            }\n            if (compareVideo.toString() != state.src.toString()) {\n                compareVideo = state.src;\n                $(\"#videoIndex\").empty();\n                for (let i = 0; i < state.count; i++) {\n                    let src = state.src[i].split(\"/\").pop();\n                    if (src.length >= 60) {\n                        src = src.substr(0, 35) + '...' + src.substr(-35);\n                    }\n                    $(\"#videoIndex\").append(`<option value='${i}'>${src}</option>`);\n                }\n            }\n            _index = _index == -1 ? 0 : _index;\n            $(\"#videoIndex\").val(_index);\n        });\n    }\n    // 点击其他设置标签页 开始读取tab信息以及视频信息\n    getVideoTag();\n    $(\"#otherTab\").click(function () {\n        chrome.tabs.get(G.mediaControl.tabid, function (tab) {\n            if (chrome.runtime.lastError) {\n                _tabId = -1;\n                _index = -1;\n                setVideoTagTimer(); getVideoState(); setVideoStateTimer();\n                return;\n            }\n            chrome.tabs.sendMessage(G.mediaControl.tabid, { Message: \"getVideoState\", index: 0 }, function (state) {\n                _tabId = G.mediaControl.tabid;\n                if (state.count > G.mediaControl.index) {\n                    _index = G.mediaControl.index;\n                }\n                $(\"#videoTabIndex\").val(_tabId);\n                setVideoTagTimer(); getVideoState(true); setVideoStateTimer();\n                (chrome.storage.session ?? chrome.storage.local).set({ mediaControl: { tabid: _tabId, index: _index } });\n            });\n        });\n        // setVideoTagTimer(); getVideoState(); setVideoStateTimer();\n    });\n    // 切换标签选择 切换视频选择\n    $(\"#videoIndex, #videoTabIndex\").change(function () {\n        if (!G.isFirefox) { $(\"#pip\").show(); }\n        $(\"#screenshot\").show();\n        if (this.id == \"videoTabIndex\") {\n            _tabId = parseInt($(\"#videoTabIndex\").val());\n        } else {\n            _index = parseInt($(\"#videoIndex\").val());\n        }\n        (chrome.storage.session ?? chrome.storage.local).set({ mediaControl: { tabid: _tabId, index: _index } });\n        getVideoState(true);\n    });\n    let wheelPlaybackRateTimeout;\n    $(\"#playbackRate\").on(\"wheel\", function (event) {\n        $(this).blur();\n        let speed = parseFloat($(this).val());\n        speed = event.originalEvent.wheelDelta < 0 ? speed - 0.1 : speed + 0.1;\n        speed = parseFloat(speed.toFixed(1));\n        if (speed < 0.1 || speed > 16) { return false; }\n        $(this).val(speed);\n        clearTimeout(wheelPlaybackRateTimeout);\n        wheelPlaybackRateTimeout = setTimeout(() => {\n            chrome.storage.sync.set({ playbackRate: speed });\n            chrome.tabs.sendMessage(_tabId, { Message: \"speed\", speed: speed, index: _index });\n        }, 200);\n        return false;\n    });\n    // 倍速播放\n    $(\"#speed\").click(function () {\n        if (_index < 0 || _tabId < 0) { return; }\n        if ($(this).data(\"switch\") == \"speed\") {\n            const speed = parseFloat($(\"#playbackRate\").val());\n            chrome.tabs.sendMessage(_tabId, { Message: \"speed\", speed: speed, index: _index });\n            chrome.storage.sync.set({ playbackRate: speed });\n            return;\n        }\n        chrome.tabs.sendMessage(_tabId, { Message: \"speed\", speed: 1, index: _index });\n    });\n    // 画中画\n    $(\"#pip\").click(function () {\n        if (_index < 0 || _tabId < 0) { return; }\n        chrome.tabs.sendMessage(_tabId, { Message: \"pip\", index: _index }, function (state) {\n            if (chrome.runtime.lastError) { return; }\n            state.state ? $(\"#pip\").html(i18n.exit) : $(\"#pip\").html(i18n.pictureInPicture);\n        });\n    });\n    // 全屏\n    $(\"#fullScreen\").click(function () {\n        if (_index < 0 || _tabId < 0) { return; }\n        chrome.tabs.get(_tabId, function (tab) {\n            chrome.tabs.highlight({ 'tabs': tab.index }, function () {\n                chrome.tabs.sendMessage(_tabId, { Message: \"fullScreen\", index: _index }, function (state) {\n                    close();\n                });\n            });\n        });\n    });\n    // 暂停 播放\n    $(\"#control\").click(function () {\n        if (_index < 0 || _tabId < 0) { return; }\n        const action = $(this).data(\"switch\");\n        chrome.tabs.sendMessage(_tabId, { Message: action, index: _index });\n    });\n    // 循环 静音\n    $(\"#loop, #muted\").click(function () {\n        if (_index < 0 || _tabId < 0) { return; }\n        const action = $(this).prop(\"checked\");\n        chrome.tabs.sendMessage(_tabId, { Message: this.id, action: action, index: _index });\n    });\n    // 调节音量和视频进度时 停止循环任务\n    $(\"#volume, #time\").mousedown(function () {\n        if (_index < 0 || _tabId < 0) { return; }\n        clearInterval(VideoStateTimer);\n    });\n    // 调节音量\n    $(\"#volume\").mouseup(function () {\n        if (_index < 0 || _tabId < 0) { return; }\n        chrome.tabs.sendMessage(_tabId, { Message: \"setVolume\", volume: $(this).val(), index: _index }, function () {\n            if (chrome.runtime.lastError) { return; }\n            setVideoStateTimer();\n        });\n    });\n    // 调节视频进度\n    $(\"#time\").mouseup(function () {\n        if (_index < 0 || _tabId < 0) { return; }\n        chrome.tabs.sendMessage(_tabId, { Message: \"setTime\", time: $(this).val(), index: _index }, function () {\n            if (chrome.runtime.lastError) { return; }\n            setVideoStateTimer();\n        });\n    });\n    // 视频截图\n    $(\"#screenshot\").click(function () {\n        if (_index < 0 || _tabId < 0) { return; }\n        chrome.tabs.sendMessage(_tabId, { Message: \"screenshot\", index: _index });\n    });\n})();"
  },
  {
    "path": "js/mpd.js",
    "content": "// url 参数解析\nconst params = new URL(location.href).searchParams;\nconst _url = params.get(\"url\");\n// const _referer = params.get(\"referer\");\nconst _requestHeaders = params.get(\"requestHeaders\");\nconst _title = params.get(\"title\");\n\n// 修改当前标签下的所有xhr的requestHeaders\nlet requestHeaders = JSONparse(_requestHeaders);\nsetRequestHeaders(requestHeaders, () => { awaitG(init); });\n\nvar mpdJson = {}; // 解析器json结果\nvar mpdXml = {}; // 解析器xml结果\n// var mpdContent; // mpd文件内容\nvar m3u8Content = \"\";   //m3u8内容\nvar mediaInfo = \"\" // 媒体文件信息\n\nchrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {\n    if (message == \"getM3u8\") {\n        sendResponse({ m3u8Content, mediaInfo });\n    }\n});\n\nfunction init() {\n    loadCSS();\n    if (_url) {\n        fetch(_url)\n            .then(response => response.text())\n            .then(function (text) {\n                // mpdContent = text;\n                // parseMPD(mpdContent);\n                parseMPD(text);\n                $(\"#mpd_url\").html(_url).attr(\"href\", _url);\n            });\n    } else {\n        $(\"#loading\").hide();\n        $(\"#mpdCustom\").show();\n        $(\"#parse\").click(function () {\n            let url = $(\"#mpdUrl\").val().trim();;\n            url = \"mpd.html?url=\" + encodeURIComponent(url);\n            let referer = $(\"#referer\").val().trim();;\n            if (referer) { url += \"&requestHeaders=\" + JSON.stringify({ referer: referer }); }\n            chrome.tabs.update({ url: url });\n        });\n    }\n\n    $(\"#mpdVideoLists, #mpdAudioLists\").change(function () {\n        let type = this.id == \"mpdVideoLists\" ? \"video\" : \"audio\";\n        showSegment(type, $(this).val());\n    });\n    $(\"#getVideo, #getAudio\").click(function () {\n        let type = \"video\";\n        let index = $(\"#mpdVideoLists\").val();\n        if (this.id == \"getAudio\") {\n            type = \"audio\";\n            index = $(\"#mpdAudioLists\").val();\n        }\n        showSegment(type, index);\n    });\n    $(\"#videoToM3u8, #audioToM3u8\").click(function () {\n        let index = $(\"#mpdVideoLists\").val();\n        let items = mpdJson.playlists[index];\n        let type = \"video\";\n        if (this.id == \"audioToM3u8\") {\n            index = $(\"#mpdAudioLists\").val();\n            let temp = index.split(\"$-bmmmd-$\");\n            index = temp[0];\n            let index2 = temp[1];\n            items = mpdJson.mediaGroups.AUDIO.audio[index].playlists[index2];\n            type = \"audio\";\n        }\n        mediaInfo = getInfo(type);\n        m3u8Content = \"#EXTM3U\\n\";\n        m3u8Content += \"#EXT-X-VERSION:3\\n\";\n        m3u8Content += \"#EXT-X-TARGETDURATION:\" + items.targetDuration + \"\\n\";\n        m3u8Content += \"#EXT-X-MEDIA-SEQUENCE:0\\n\";\n        m3u8Content += \"#EXT-X-PLAYLIST-TYPE:VOD\\n\";\n        m3u8Content += '#EXT-X-MAP:URI=\"' + items.segments[0].map.resolvedUri + '\"\\n';\n        for (let key in items.segments) {\n            m3u8Content += \"#EXTINF:\" + items.segments[key].duration + \",\\n\"\n            m3u8Content += items.segments[key].resolvedUri + \"\\n\";\n        }\n        m3u8Content += \"#EXT-X-ENDLIST\";\n        // $(\"#media_file\").html(m3u8Content); return;\n        chrome.tabs.getCurrent(function (tabs) {\n            chrome.tabs.create({ url: \"m3u8.html?getId=\" + tabs.id });\n        });\n    });\n}\n\n// 加密类型\nfunction getEncryptionType(schemeIdUri) {\n    if (schemeIdUri.includes(\"edef8ba9-79d6-4ace-a3c8-27dcd51d21ed\")) {\n        return \"Widevine\";\n    } else if (schemeIdUri.includes(\"9a04f079-9840-4286-ab92-e65be0885f95\")) {\n        return \"Microsoft PlayReady\";\n    } else if (schemeIdUri.includes(\"94ce86fb-07ff-4f43-adb8-93d2fa968ca2\")) {\n        return \"Apple FairPlay\";\n    } else {\n        return \"Unknown\";\n    }\n}\n// 判断DRM\nfunction isDRM(mpdContent) {\n    const parser = new DOMParser();\n    const xmlDoc = parser.parseFromString(mpdContent, \"application/xml\");\n    let drmInfo = new Map();\n    const contentProtections = xmlDoc.getElementsByTagName(\"ContentProtection\");\n    for (let i = 0; i < contentProtections.length; i++) {\n        const protection = contentProtections[i];\n        const schemeIdUri = protection.getAttribute(\"schemeIdUri\");\n        let pssh = protection.getElementsByTagName(\"cenc:pssh\")[0];\n        if (!pssh) {\n            pssh = protection.getElementsByTagName(\"mspr:pro\")[0];\n        }\n\n        if (schemeIdUri && pssh) {\n            if (!drmInfo.has(schemeIdUri)) {\n                drmInfo.set(schemeIdUri, pssh.textContent);\n            }\n        }\n    }\n    return Array.from(drmInfo.entries()).map(([schemeIdUri, pssh]) => ({\n        schemeIdUri,\n        pssh,\n        encryptionType: getEncryptionType(schemeIdUri)\n    }));\n}\nfunction parseMPD(mpdContent) {\n    $(\"#loading\").hide(); $(\"#main\").show();\n    mpdJson = mpdParser.parse(mpdContent, { manifestUri: _url });\n\n    const drmInfo = isDRM(mpdContent);\n    if (drmInfo.length > 0) {\n        $(\"#loading\").show();\n        $(\"#loading .optionBox\").html(`<b>${i18n.DRMerror}</b><br><br>`);\n        drmInfo.map(item => {\n            $(\"#loading .optionBox\").append(`<b>${item.encryptionType}</b><input value=\"${item.pssh}\" style=\"width: 100%;\"/>`);\n        });\n    }\n\n    for (let key in mpdJson.playlists) {\n        $(\"#mpdVideoLists\").append(`<option value='${key}'>${mpdJson.playlists[key].attributes.NAME\n            } | ${(mpdJson.playlists[key].attributes.BANDWIDTH / 1024).toFixed(1)\n            } kbps |  ${mpdJson.playlists[key].attributes[\"FRAME-RATE\"].toFixed(1)\n            } fps |  ${mpdJson.playlists[key].attributes.RESOLUTION.width\n            } x ${mpdJson.playlists[key].attributes.RESOLUTION.height\n            }</option>`);\n    }\n    for (let key in mpdJson.mediaGroups.AUDIO.audio) {\n        for (let index in mpdJson.mediaGroups.AUDIO.audio[key].playlists) {\n            let item = mpdJson.mediaGroups.AUDIO.audio[key].playlists[index];\n            // console.log(item);\n            $(\"#mpdAudioLists\").append(`<option value='${key}$-bmmmd-$${index}'>${key} | ${item.attributes.NAME} | ${item.attributes.BANDWIDTH / 1000}Kbps</option>`);\n        }\n    }\n    $(\"#info\").html(getInfo(\"video\"));\n    showSegment(\"video\", 0);\n}\n\nfunction showSegment(type, index) {\n    let textarea = \"\";\n    let items;\n    if (type == \"video\") {\n        items = mpdJson.playlists[index];\n    } else {\n        let temp = index.split(\"$-bmmmd-$\");\n        index = temp[0];\n        let index2 = temp[1];\n        items = mpdJson.mediaGroups.AUDIO.audio[index].playlists[index2];\n    }\n    for (let key in items.segments) {\n        textarea += items.segments[key].resolvedUri + \"\\n\\n\";\n    }\n    $(\"#media_file\").html(textarea);\n    $(\"#count\").html(i18n(\"m3u8Info\", [items.segments.length, secToTime(mpdJson.duration)]));\n    items.segments.length > 0 && $(\"#tips\").html('initialization: <input type=\"text\" value=\"' + items.segments[0].map.resolvedUri + '\" spellcheck=\"false\" readonly=\"readonly\" class=\"width100\">');\n    $(\"#info\").html(getInfo(type));\n}\n\nfunction getInfo(type = \"audio\") {\n    if (type == \"audio\") {\n        return i18n.audio + \": \" + $(\"#mpdAudioLists\").find(\"option:selected\").text();\n    } else {\n        return i18n.video + \": \" + $(\"#mpdVideoLists\").find(\"option:selected\").text();\n    }\n}"
  },
  {
    "path": "js/options.js",
    "content": "////////////////////// 填充数据 //////////////////////\nchrome.storage.sync.get(G.OptionLists, function (items) {\n    if (chrome.runtime.lastError) {\n        items = G.OptionLists;\n    }\n    // 确保有默认值\n    for (let key in G.OptionLists) {\n        if (items[key] === undefined || items[key] === null) {\n            items[key] = G.OptionLists[key];\n        }\n    }\n    if (items.Ext === undefined || items.Type === undefined || items.Regex === undefined) {\n        location.reload();\n    }\n    if (G.isMobile) {\n        $(`<link rel=\"stylesheet\" type=\"text/css\" href=\"css/mobile.css\">`).appendTo(\"head\");\n    }\n    $(`<style>${items.css}</style>`).appendTo(\"head\");\n    const $extList = $(\"#extList\");\n    for (let key in items.Ext) {\n        if (items.Ext[key].operator === undefined) {\n            items.Ext[key].operator = \">=\";\n        }\n        $extList.append(Gethtml(\"Ext\", { ext: items.Ext[key].ext, size: items.Ext[key].size, operator: items.Ext[key].operator, state: items.Ext[key].state }));\n    }\n    const $typeList = $(\"#typeList\");\n    for (let key in items.Type) {\n        if (items.Type[key].operator === undefined) {\n            items.Type[key].operator = \">=\";\n        }\n        $typeList.append(Gethtml(\"Type\", { type: items.Type[key].type, size: items.Type[key].size, operator: items.Type[key].operator, state: items.Type[key].state }));\n    }\n    const $regexList = $(\"#regexList\");\n    for (let key in items.Regex) {\n        $regexList.append(Gethtml(\"Regex\", { type: items.Regex[key].type, regex: items.Regex[key].regex, ext: items.Regex[key].ext, blackList: items.Regex[key].blackList, state: items.Regex[key].state }));\n    }\n    const $blockUrlList = $(\"#blockUrlList\");\n    for (let key in items.blockUrl) {\n        $blockUrlList.append(Gethtml(\"blockUrl\", { url: items.blockUrl[key].url, state: items.blockUrl[key].state }));\n    }\n    setTimeout(() => {\n        for (let key in items) {\n            if (key == \"Ext\" || key == \"Type\" || key == \"Regex\") { continue; }\n            if (typeof items[key] == \"boolean\") {\n                $(`#${key}`).prop(\"checked\", items[key]);\n            } else {\n                $(`#${key}`).val(items[key]);\n            }\n        }\n    }, 100);\n});\n\n//新增格式\n$(\"#AddExt\").bind(\"click\", function () {\n    $(\"#extList\").append(Gethtml(\"Ext\", { state: true }));\n    $(\"#extList [name=text]\").last().focus();\n});\n$(\"#AddType\").bind(\"click\", function () {\n    $(\"#typeList\").append(Gethtml(\"Type\", { state: true }));\n    $(\"#typeList [name=text]\").last().focus();\n});\n$(\"#AddRegex\").bind(\"click\", function () {\n    $(\"#regexList\").append(Gethtml(\"Regex\", { type: \"ig\", state: true }));\n    $(\"#regexList [name=text]\").last().focus();\n});\n$(\"#blockAddUrl\").bind(\"click\", function () {\n    $(\"#blockUrlList\").append(Gethtml(\"blockUrl\", { state: true }));\n    $(\"#blockUrlList [name=url]\").last().focus();\n});\n$(\"#version\").html(i18n.catCatch + \" v\" + chrome.runtime.getManifest().version);\n\n// 自定义播放调用模板\nplayerList = new Map();\nplayerList.set(\"tips\", { name: i18n.invokeProtocolTemplate, template: \"\" });\nplayerList.set(\"default\", { name: i18n.default + \" / \" + i18n.disable, template: \"\" });\nplayerList.set(\"potplayer\", { name: \"PotPlayer\", template: \"potplayer://${url} ${referer|exists:'/referer=\\\"*\\\"'}\" });\nplayerList.set(\"potplayerFix\", { name: \"PotPlayerFix\", template: \"potplayer:${url} ${referer|exists:'/referer=\\\"*\\\"'}\" });\nplayerList.set(\"mxPlayerAd\", { name: \"Android MX Player Free\", template: \"intent:${url}#Intent;package=com.mxtech.videoplayer.ad;end\" });\nplayerList.set(\"mxPlayerPro\", { name: \"Android MX Player Pro\", template: \"intent:${url}#Intent;package=com.mxtech.videoplayer.pro;end\" });\nplayerList.set(\"vlc\", { name: \"Android vlc\", template: \"intent:${url}#Intent;package=org.videolan.vlc;end\" });\nplayerList.set(\"vlcCustom\", { name: i18n.customVLCProtocol + \" vlc://\", template: \"vlc://${url}\" });\nplayerList.set(\"shareApi\", { name: i18n.systemShare, template: \"${shareApi}\" });\nplayerList.forEach(function (item, key) {\n    $(\"#PlayerTemplate\").append(`<option value=\"${key}\">${item.name}</option>`);\n});\n\n// 增加后缀 类型 正则表达式\nfunction Gethtml(Type, Param = new Object()) {\n    let html = \"\";\n    switch (Type) {\n        case \"Ext\":\n            html = `<td><input type=\"text\" value=\"${Param.ext ? Param.ext : \"\"}\" name=\"text\" placeholder=\"${i18n.suffix}\" class=\"ext\"></td>`\n            html += `<td><input type=\"text\" value=\"${Param.operator == \">=\" || Param.operator == \"~\" ? \"\" : Param.operator}${Param.size ? Param.size : 0}\" class=\"size\" name=\"size\">KB</td>`\n            break;\n        case \"Type\":\n            html = `<td><input type=\"text\" value=\"${Param.type ? Param.type : \"\"}\" name=\"text\" placeholder=\"${i18n.type}\" class=\"type\"></td>`\n            html += `<td><input type=\"text\" value=\"${Param.operator == \">=\" || Param.operator == \"~\" ? \"\" : Param.operator}${Param.size ? Param.size : 0}\" class=\"size\" name=\"size\">KB</td>`\n            break;\n        case \"Regex\":\n            html = `<td><input type=\"text\" value=\"${Param.type ? Param.type : \"\"}\" name=\"type\" class=\"regexType\"></td>`\n            html += `<td><input type=\"text\" value=\"${Param.regex ? Param.regex : \"\"}\" placeholder=\"${i18n.regexExpression}\" name=\"regex\" class=\"regex\"></td>`\n            html += `<td><input type=\"text\" value=\"${Param.ext ? Param.ext : \"\"}\" name=\"regexExt\" class=\"regexExt\"></td>`\n            html += `<td>\n            <div class=\"switch\">\n                <label class=\"switchLabel switchRadius\">\n                    <input type=\"checkbox\" name=\"blackList\" class=\"switchInput\" ${Param.blackList ? 'checked=\"checked\"' : \"\"}/>\n                    <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n                </label>\n            </div>\n        </td>`\n            break;\n        case \"blockUrl\":\n            html = `<td><input type=\"text\" value=\"${Param.url ? Param.url : \"\"}\" name=\"url\" placeholder=\"${i18n.blockUrlTips}\" class=\"width100\"></td>`\n            break;\n    }\n    html = $(`<tr data-type=\"${Type}\">\n            ${html}\n            <td>\n                <div class=\"switch\">\n                    <label class=\"switchLabel switchRadius\">\n                        <input type=\"checkbox\" name=\"state\" class=\"switchInput\" ${Param.state ? 'checked=\"checked\"' : \"\"}/>\n                        <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n                    </label>\n                </div>\n            </td>\n            <td>\n                <img src=\"img/delete.svg\" class=\"RemoveButton\">\n            </td>\n        </tr>`);\n    html.find(\".RemoveButton\").click(function () {\n        html.remove();\n        Save(Type);\n    });\n    html.find(\"input\").on(\"input\", function () {\n        Save(Type, 200);\n    });\n    html.find(\"[name=state]\").on(\"click\", function () {\n        Save(Type);\n    });\n    if (Type == \"Type\") {\n        html.find(\"input\").blur(function () {\n            $(\"#typeList tr\").each(function () {\n                let GetText = $(this).find(\"[name=text]\").val();\n                if (isEmpty(GetText)) { return true; }\n                GetText = GetText.trim();\n                const test = GetText.split(\"/\");\n                if (test.length != 2 || isEmpty(test[0]) || isEmpty(test[1])) {\n                    alert(i18n.addTypeError);\n                    return true;\n                }\n            });\n        });\n    }\n    return html;\n}\n// 预览模板\n$(\"#PlayerTemplate\").change(function () {\n    const Value = $(this).val();\n    if (this.id == \"PlayerTemplate\" && playerList.has(Value) && Value != \"tips\") {\n        const template = playerList.get(Value).template;\n        $(\"#Player\").val(template);\n        chrome.storage.sync.set({ Player: template });\n    }\n});\n//失去焦点 保存自动清理数 模拟手机User Agent 自定义播放调用模板\nlet debounce2 = undefined;\n$(\"[save='input']\").on(\"input\", function () {\n    let val = $(this).val().trim();\n    if (this.type == \"number\") {\n        val = parseInt(val);\n    }\n    clearTimeout(debounce2);\n    debounce2 = setTimeout(() => {\n        chrome.storage.sync.set({ [this.id]: val });\n    }, 300);\n});\n// 调试模式 使用网页标题做文件名 使用PotPlayer预览 显示网站图标 刷新自动清理\n$(\"[save='click']\").bind(\"click\", function () {\n    chrome.storage.sync.set({ [this.id]: $(this).prop('checked') });\n});\n// [save='select'] 元素 储存\n$(\"[save='select']\").on(\"change\", function () {\n    let val = $(this).val();\n    if (!isNaN(val)) { val = parseInt(val); }\n    chrome.storage.sync.set({ [this.id]: val });\n});\n\n// 一键禁用/启用\n$(\"#allDisable, #allEnable\").bind(\"click\", function () {\n    const state = this.id == \"allEnable\";\n    const obj = $(this).data(\"switch\");\n    let query;\n    if (obj == \"Ext\") {\n        query = $(\"#extList [name=state]\");\n    } else if (obj == \"Type\") {\n        query = $(\"#typeList [name=state]\");\n    } else if (obj == \"Regex\") {\n        query = $(\"#regexList [name=state]\");\n    } else if (obj == \"blockUrl\") {\n        query = $(\"#blockUrlList [name=state]\");\n    }\n    query.each(function () {\n        $(this).prop(\"checked\", state);\n    });\n    Save(obj);\n});\n// m3u8dlArg 输出测试\nfunction testTag() {\n    const data = {\n        url: $(\"#url\").val(),\n        requestHeaders: { referer: $(\"#referer\").val() },\n        initiator: $(\"#initiator\").val(),\n        webUrl: $(\"#webUrl\").val(),\n        title: $(\"#title\").val(),\n    }\n    const result = templates($(\"#testTextarea\").val() ?? \"\", data);\n    const m3u8dl = 'm3u8dl:' + (G.m3u8dl == 1 ? Base64.encode(result) : result);\n    $(\"#tagTestResult\").html(`${result}<br><br><a href=\"${m3u8dl}\" class=\"test_url\">${m3u8dl}</a>`);\n}\n$(\"#showTestTag\").bind(\"click\", function () {\n    testTag();\n    $(\"#testTag\").slideToggle();\n});\n$(\"#testTag input, #testTextarea\").on(\"input\", function () {\n    testTag();\n});\n//重置后缀 重置类型 重置正则\n$(\"[data-reset]\").bind(\"click\", function () {\n    if (confirm(i18n.confirmReset)) {\n        const Option = $(this).data(\"reset\");\n        chrome.storage.sync.set({ [Option]: G.OptionLists[Option] }, () => {\n            location.reload();\n        });\n    }\n});\n\n//重置设置\n$(\".resetOption\").click(function () {\n    if (confirm(i18n.confirmReset)) {\n        const optionBox = $(this).closest('.optionBox');\n        const result = optionBox.find('[save]').toArray().reduce((acc, { id }) => {\n            acc[id] = G.OptionLists[id];\n            return acc;\n        }, {});\n        chrome.storage.sync.set(result, () => {\n            location.reload();\n        });\n    }\n});\n\n//清空数据 重置所有设置\n$(\"#ClearData, #ResetAllOption\").bind(\"click\", function () {\n    if (this.id == \"ResetAllOption\") {\n        if (confirm(i18n.confirmReset)) {\n            chrome.storage.sync.clear();\n            InitOptions();\n        } else {\n            return;\n        }\n    }\n    chrome.storage.local.clear();\n    chrome.storage.session.clear();\n    chrome.runtime.sendMessage({ Message: \"ClearIcon\" });\n    location.reload();\n});\n\n//重启扩展\n$(\"#extensionReload\").bind(\"click\", function () {\n    chrome.runtime.reload();\n});\n//正则表达式 测试\n$(\"#testRegex, #testUrl\").keyup(function () {\n    const testUrl = $(\"#testUrl\").val();\n    const testRegex = $(\"#testRegex\").val();\n    const testFlag = $(\"#testFlag\").val();\n    if (testUrl == \"\" || testRegex == \"\") {\n        $(\"#testResult\").html(i18n.noMatch);\n        return;\n    }\n    let regex;\n    try {\n        regex = new RegExp(testRegex, testFlag);\n    } catch (e) {\n        $(\"#testResult\").html(e.message);\n        return;\n    }\n    const result = regex.exec(testUrl);\n    if (result == null) {\n        $(\"#testResult\").html(i18n.noMatch);\n        return;\n    }\n    $(\"#testResult\").html(i18n.match)\n    for (let i = 1; i < result.length; i++) {\n        if (result[i] != \"\") {\n            $(\"#testResult\").append(\n                `<input type=\"text\" style=\"width: 590px; color: #ff0000\" value=\"${decodeURIComponent(result[i])}\">`\n            );\n        }\n    }\n});\n//导出配置\n$(\"#exportOptions\").bind(\"click\", function () {\n    chrome.storage.sync.get(null, function (items) {\n        let ExportData = JSON.stringify(items);\n        ExportData = \"data:text/plain,\" + Base64.encode(ExportData);\n        let date = new Date();\n        const filename = `cat-catch-${chrome.runtime.getManifest().version}-${date.getFullYear()}${appendZero(date.getMonth() + 1)}${appendZero(date.getDate())}T${appendZero(date.getHours())}${appendZero(date.getMinutes())}.txt`;\n        if (G.isFirefox) {\n            downloadDataURL(ExportData, filename);\n            return;\n        }\n        chrome.downloads.download({\n            url: ExportData,\n            filename: filename\n        });\n    });\n});\n//导入配置\n$(\"#importOptionsFile\").change(function () {\n    const fileReader = new FileReader();\n    fileReader.onload = function () {\n        let importData = this.result;\n        try {\n            importData = JSON.parse(importData);\n        } catch (e) {\n            importData = Base64.decode(importData);\n            importData = JSON.parse(importData);\n        }\n        const keys = Object.keys(G.OptionLists);\n        for (let item in G.OptionLists) {\n            if (keys.includes(item) && importData[item] !== undefined) {\n                chrome.storage.sync.set({ [item]: importData[item] });\n            }\n        }\n        alert(i18n.alertimport);\n        location.reload();\n    }\n    const file = $(\"#importOptionsFile\").prop('files')[0];\n    fileReader.readAsText(file);\n});\n$(\"#importOptions\").bind(\"click\", function () {\n    $(\"#importOptionsFile\").click();\n});\n\nfunction SaveGetVal(Obj) {\n    let text = Obj.find(\"[name=text]\").val()?.trim();\n    let size = Obj.find(\"[name=size]\").val()?.trim();\n    let state = Obj.find(\"[name=state]\").prop(\"checked\");\n\n    // size 只保留操作符号 和 数字 和 范围符号 - 和 中间的空格\n    size = size.replace(/[^\\d><=!-\\s]/g, \"\");\n\n    let operator = \">=\";    // 默认使用大于等于符号\n\n    // 判断是否范围格式 如果存在 - 则分离 前后\n    const rangeMatch = size.match(/^(\\d+)-(\\d+)$/);\n    if (rangeMatch) { operator = \"~\"; }\n\n    // 比较符号\n    const operatorMatch = size.match(/^(>=|<=|>|<|=|!=)/);\n    if (operatorMatch) {\n        operator = operatorMatch[0];\n        size = parseInt(size.replace(operator, \"\"));\n    }\n    if (operator != \"~\") {\n        size = parseInt(size);\n        if (isNaN(size)) { size = 0; }\n    }\n    if (isEmpty(size)) { size = 0; }\n    text = text.toLowerCase();\n\n    return { text, size, operator, state };\n}\n\n// 保存 后缀 类型 正则 配置\nfunction Save(option, sec = 0) {\n    clearTimeout(debounce);\n    debounce = setTimeout(() => {\n        if (option == \"Ext\") {\n            let Ext = new Array();\n            $(\"#extList tr\").each(function (index) {\n                if (index === 0) return true;\n\n                const { text, size, operator, state } = SaveGetVal($(this));\n\n                if (isEmpty(text)) { return true; }\n                Ext.push({ ext: text, size, operator, state });\n            });\n            chrome.storage.sync.set({ Ext: Ext });\n            return;\n        }\n        if (option == \"Type\") {\n            let Type = new Array();\n            $(\"#typeList tr\").each(function (index) {\n                if (index === 0) return true;\n\n                const { text, size, operator, state } = SaveGetVal($(this));\n\n                if (isEmpty(text)) { return true; }\n                const test = text.split(\"/\");\n                if (test.length == 2 && !isEmpty(test[0]) && !isEmpty(test[1])) {\n                    Type.push({ type: text, size, operator, state });\n                }\n            });\n            chrome.storage.sync.set({ Type: Type });\n            return;\n        }\n        if (option == \"Regex\") {\n            let Regex = new Array();\n            $(\"#regexList tr\").each(function (index) {\n                if (index === 0) return true;\n\n                const _this = $(this);\n                let GetType = _this.find(\"[name=type]\").val();\n                let GetRegex = _this.find(\"[name=regex]\").val();\n                let GetExt = _this.find(\"[name=regexExt]\").val()\n                let GetState = _this.find(\"[name=state]\").prop(\"checked\");\n                let GetBlackList = _this.find(\"[name=blackList]\").prop(\"checked\");\n                try {\n                    new RegExp(\"\", GetType);\n                } catch (e) {\n                    GetType = \"ig\";\n                }\n                if (isEmpty(GetRegex)) { return true; }\n                GetExt = GetExt ? GetExt.toLowerCase() : \"\";\n                Regex.push({ type: GetType, regex: GetRegex, ext: GetExt, blackList: GetBlackList, state: GetState });\n            });\n            chrome.storage.sync.set({ Regex: Regex });\n            return;\n        }\n        if (option == \"blockUrl\") {\n            let blockUrl = new Array();\n            $(\"#blockUrlList tr\").each(function (index) {\n                if (index === 0) return true;\n\n                const _this = $(this);\n                let url = _this.find(\"[name=url]\").val();\n                let GetState = _this.find(\"[name=state]\").prop(\"checked\");\n                if (isEmpty(url)) { return true; }\n                blockUrl.push({ url: url, state: GetState });\n            });\n            chrome.storage.sync.set({ blockUrl: blockUrl });\n            return;\n        }\n    }, sec);\n}\n\n// 导航栏\ndocument.querySelectorAll('nav a').forEach(anchor => {\n    anchor.addEventListener('click', function (e) {\n        e.preventDefault();\n        const targetId = this.getAttribute('href');\n        const targetElement = document.querySelector(targetId);\n        if (targetElement) {\n            targetElement.scrollIntoView({\n                behavior: 'smooth'\n            });\n        }\n    });\n});\nconst adjustSidebarPosition = () => {\n    const wrapper = document.querySelector('.wrapper');\n    const sidebar = document.querySelector('.sidebar');\n    if (wrapper && sidebar) {\n        sidebar.style.left = `${wrapper.getBoundingClientRect().left - sidebar.offsetWidth - 20}px`;\n    }\n}\nwindow.addEventListener('load', adjustSidebarPosition)\nwindow.addEventListener('resize', adjustSidebarPosition);\n"
  },
  {
    "path": "js/popup.js",
    "content": "// 解析参数\nconst params = new URL(location.href).searchParams;\nconst _tabId = parseInt(params.get(\"tabId\"));\nconst _type = params.get(\"type\");\n\n// 当前页面\nconst $mediaList = $('#mediaList');\nconst $current = $(\"<div></div>\");\nconst $currentCount = $(\"#currentTab #quantity\");\nlet currentCount = 0;\n// 其他页面\nconst $allMediaList = $('#allMediaList');\nconst $all = $(\"<div></div>\");\nconst $allCount = $(\"#allTab #quantity\");\nlet allCount = 0;\n// 疑似密钥\nconst $maybeKey = $(\"<div></div>\");\n// 提示 操作按钮 DOM\nconst $tips = $(\"#Tips\");\nconst $down = $(\"#down\");\nconst $mergeDown = $(\"#mergeDown\");\n// 储存所有资源数据\nconst allData = new Map([\n    [true, new Map()],  // 当前页面\n    [false, new Map()]  // 其他页面\n]);\n// 筛选\nconst $filter_ext = $(\"#filter #ext\");\n// 储存所有扩展名，保存是否筛选状态 来判断新加入的资源 立刻判断是否需要隐藏\nconst filterExt = new Map();\n// 删除重复文件名\nlet duplicateFilenamesSet = null;\n// 当前所在页面\nlet activeTab = true;\n// 储存下载id\nconst downData = [];\n// 图标地址\nconst favicon = new Map();\n// 当前页面DOM\nlet pageDOM = undefined;\n// HeartBeat\nchrome.runtime.sendMessage(chrome.runtime.id, { Message: \"HeartBeat\" });\n// 清理冗余数据\nchrome.runtime.sendMessage(chrome.runtime.id, { Message: \"clearRedundant\" });\n// 监听下载 出现服务器拒绝错误 调用下载器\nchrome.downloads.onChanged.addListener(function (item) {\n    if (G.catDownload) { delete downData[item.id]; return; }\n    const errorList = [\"SERVER_BAD_CONTENT\", \"SERVER_UNAUTHORIZED\", \"SERVER_FORBIDDEN\", \"SERVER_UNREACHABLE\", \"SERVER_CROSS_ORIGIN_REDIRECT\", \"SERVER_FAILED\", \"NETWORK_FAILED\"];\n    if (item.error && errorList.includes(item.error.current) && downData[item.id]) {\n        catDownload(downData[item.id]);\n        delete downData[item.id];\n    }\n});\n// 复选框状态 点击返回或者全选后 影响新加入的资源 复选框勾选状态\nlet checkboxState = true;\n\n// 生成资源DOM\nfunction AddMedia(data, currentTab = true) {\n    data._title = data.title.replace(/[/\\\\]/g, \"_\");\n    data.title = stringModify(data.title);\n    //文件名\n    data.name = isEmpty(data.name) ? data.title + '.' + data.ext : decodeURIComponent(stringModify(data.name));\n    //截取文件名长度\n    let trimName = data.name;\n    if (data.name && data.name.length >= 50 && !_tabId) {\n        trimName = trimName.substr(0, 20) + '...' + trimName.substr(-30);\n    }\n    //添加下载文件名\n    Object.defineProperty(data, \"pageDOM\", {\n        get() { return pageDOM; }\n    });\n    data.downFileName = G.TitleName ? templates(G.downFileName, data) : data.name;\n    data.downFileName = filterFileName(data.downFileName);\n    if (isEmpty(data.downFileName)) {\n        data.downFileName = data.name;\n    }\n    // 文件大小单位转换\n    data._size = data.size;\n    if (data.size) {\n        data.size = byteToSize(data.size);\n    }\n    // 是否需要解析\n    data.parsing = false;\n    if (isM3U8(data)) {\n        data.parsing = \"m3u8\";\n    } else if (isMPD(data)) {\n        data.parsing = \"mpd\";\n    } else if (isJSON(data)) {\n        data.parsing = \"json\";\n    }\n    // 网站图标\n    if (data.favIconUrl && !favicon.has(data.webUrl)) {\n        favicon.set(data.webUrl, data.favIconUrl);\n    }\n    data.isPlay = isPlay(data);\n\n    if (allData.get(currentTab).has(data.requestId)) {\n        data.requestId = data.requestId + \"_\" + Date.now().toString();\n    }\n\n    //添加html\n    data.html = $(`\n        <div class=\"panel\">\n            <div class=\"panel-heading\">\n                <input type=\"checkbox\" class=\"DownCheck\"/>\n                ${G.ShowWebIco ? `<img class=\"favicon ${!data.favIconUrl ? \"faviconFlag\" : \"\"}\" requestId=\"${data.requestId}\" src=\"${data.favIconUrl}\"/>` : \"\"}\n                <img src=\"img/regex.png\" class=\"favicon regex ${data.isRegex ? \"\" : \"hide\"}\" title=\"${i18n.regexTitle}\"/>\n                <span class=\"name ${data.parsing || data.isRegex || data.tabId == -1 ? \"bold\" : \"\"}\">${trimName}</span>\n                <span class=\"size ${data.size ? \"\" : \"hide\"}\">${data.size}</span>\n                <img src=\"img/copy.png\" class=\"icon copy\" id=\"copy\" title=\"${i18n.copy}\"/>\n                <img src=\"img/parsing.png\" class=\"icon parsing ${data.parsing ? \"\" : \"hide\"}\" id=\"parsing\" data-type=\"${data.parsing}\" title=\"${i18n.parser}\"/>\n                <img src=\"img/play.png\" class=\"icon play ${data.isPlay ? \"\" : \"hide\"}\" id=\"play\" title=\"${i18n.preview}\"/>\n                <img src=\"img/download.svg\" class=\"icon download\" id=\"download\" title=\"${i18n.download}\"/>\n                <img src=\"img/aria2.png\" class=\"icon aria2 ${G.enableAria2Rpc ? \"\" : \"hide\"}\"\" id=\"aria2\" title=\"Aria2\"/>\n                <img src=\"img/invoke.svg\" class=\"icon invoke ${G.invoke ? \"\" : \"hide\"}\"\" id=\"invoke\" title=\"${i18n.invoke}\"/>\n                <img src=\"img/send.svg\" class=\"icon send ${G.send2localManual || G.send2local ? \"\" : \"hide\"}\"\" id=\"send2local\" title=\"${i18n.send2local}\"/>\n                <img src=\"img/mqtt.svg\" class=\"icon mqtt ${G.mqttEnable ? \"\" : \"hide\"}\" id=\"mqtt\" title=\"${i18n.send2MQTT}\"/>\n            </div>\n            <div class=\"url hide\">\n                <div id=\"mediaInfo\" data-state=\"false\">\n                    ${data.title ? `<b>${i18n.title}:</b> ${data.title}` : \"\"}\n                    ${data.type ? `<br><b>MIME:</b>  ${data.type}` : \"\"}\n                </div>\n                <div class=\"moreButton\">\n                    <div id=\"qrcode\"><img src=\"img/qrcode.png\" class=\"icon qrcode\" title=\"QR Code\"/></div>\n                    <div id=\"catDown\"><img src=\"img/cat-down.png\" class=\"icon cat-down\" title=\"${i18n.downloadWithRequestHeader}\"/></div>\n                    <div id=\"catDownFFmpeg\"><img src=\"img/send2ffmpeg.svg\" class=\"icon send2ffmpeg\" title=\"${i18n.sendFfmpeg}\"/></div>\n                    <div><img src=\"img/invoke.svg\" class=\"icon invoke\" title=\"${i18n.invoke}\"/></div>\n                </div>\n                <a href=\"${data.url}\" target=\"_blank\" download=\"${data.downFileName}\">${data.url}</a>\n                <br>\n                <img id=\"screenshots\" class=\"hide\"/>\n                <video id=\"preview\" class=\"hide\" controls></video>\n            </div>\n        </div>`);\n    ////////////////////////绑定事件////////////////////////\n    //展开网址\n    data.urlPanel = data.html.find(\".url\");\n    data.urlPanelShow = false;\n    data.panelHeading = data.html.find(\".panel-heading\");\n    data.panelHeading.click(function (event) {\n        data.urlPanelShow = !data.urlPanelShow;\n        const mediaInfo = data.html.find(\"#mediaInfo\");\n        const preview = data.html.find(\"#preview\");\n        if (!data.urlPanelShow) {\n            if (event.target.id == \"play\") {\n                preview.show().trigger(\"play\");\n                return false;\n            }\n            data.urlPanel.hide();\n            !preview[0].paused && preview.trigger(\"pause\");\n            return false;\n        }\n        data.urlPanel.show();\n        if (!mediaInfo.data(\"state\")) {\n            mediaInfo.data(\"state\", true);\n            if (isM3U8(data)) {\n                const hls = new Hls({ enableWorker: false });\n                setRequestHeaders(data.requestHeaders, function () {\n                    hls.loadSource(data.url);\n                    hls.attachMedia(preview[0]);\n                });\n                hls.on(Hls.Events.BUFFER_CREATED, function (event, data) {\n                    if (data.tracks && !data.tracks.audiovideo) {\n                        !data.tracks.audio && mediaInfo.append(`<br><b>${i18n.noAudio}</b>`);\n                        !data.tracks.video && mediaInfo.append(`<br><b>${i18n.noVideo}</b>`);\n                    }\n                });\n                hls.on(Hls.Events.ERROR, function (event, data) {\n                    hls.stopLoad();\n                });\n                hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {\n                    if (data.levels.length > 1 && !mediaInfo.text().includes(i18n.m3u8Playlist)) {\n                        mediaInfo.append(`<br><b>${i18n.m3u8Playlist}</b>`);\n                    }\n                });\n            } else if (data.isPlay) {\n                setRequestHeaders(data.requestHeaders, function () {\n                    preview.attr(\"src\", data.url);\n                });\n            } else if (isPicture(data)) {\n                setRequestHeaders(data.requestHeaders, function () {\n                    data.html.find(\"#screenshots\").show().attr(\"src\", data.url);\n                });\n                return false;\n            } else {\n                return false;\n            }\n            preview.on(\"loadedmetadata\", function () {\n                preview.show();\n                if (this.duration && this.duration != Infinity) {\n                    data.duration = this.duration;\n                    mediaInfo.append(`<br><b>${i18n.duration}:</b> ` + secToTime(this.duration));\n                }\n                if (this.videoHeight && this.videoWidth) {\n                    mediaInfo.append(`<br><b>${i18n.resolution}:</b> ` + this.videoWidth + \"x\" + this.videoHeight);\n                    data.videoWidth = this.videoWidth;\n                    data.videoHeight = this.videoHeight;\n                }\n            });\n        }\n        if (event.target.id == \"play\") {\n            preview.show().trigger(\"play\");\n        }\n        return false;\n    });\n    // 二维码\n    data.html.find(\"#qrcode\").click(function () {\n        const size = data.url.length >= 300 ? 400 : 256;\n        $(this).html(\"\").qrcode({ width: size, height: size, text: data.url }).off(\"click\");\n    });\n    // 猫抓下载器 下载\n    data.html.find(\"#catDown\").click(function () {\n        catDownload(data);\n    });\n    data.html.find(\"#catDownFFmpeg\").click(function () {\n        catDownload(data, { ffmpeg: \"addFile\" });\n    });\n    //点击复制网址\n    data.html.find('#copy').click(function () {\n        const text = copyLink(data);\n        navigator.clipboard.writeText(text);\n        Tips(i18n.copiedToClipboard);\n        return false;\n    });\n    // 发送到Aria2\n    data.html.find('#aria2').click(function () {\n        aria2AddUri(data, function (data) {\n            Tips(i18n.hasSent + JSON.stringify(data), 2000);\n        }, function (errMsg) {\n            Tips(i18n.sendFailed, 2000);\n            console.error(errMsg);\n        });\n        return false;\n    });\n    // 下载\n    data.html.find('#download').click(function (event) {\n        if (G.m3u8dl && (isM3U8(data) || isMPD(data))) {\n            if (!data.url.startsWith(\"blob:\")) {\n                const m3u8dlArg = data.m3u8dlArg ?? templates(G.m3u8dlArg, data);\n                const url = 'm3u8dl:' + (G.m3u8dl == 1 ? Base64.encode(m3u8dlArg) : m3u8dlArg);\n                if (url.length >= 2046) {\n                    navigator.clipboard.writeText(m3u8dlArg);\n                    Tips(i18n.M3U8DLparameterLong, 2000);\n                    return false;\n                }\n                // 下载前确认参数\n                if (G.m3u8dlConfirm && event.originalEvent && event.originalEvent.isTrusted) {\n                    data.html.find('.confirm').remove();\n                    const confirm = $(`<div class=\"confirm\">\n                        <textarea type=\"text\" class=\"width100\" rows=\"10\">${m3u8dlArg}</textarea>\n                        <button class=\"button2\" id=\"confirm\">${i18n.confirm}</button>\n                        <button class=\"button2\" id=\"close\">${i18n.close}</button>\n                    </div>`);\n                    confirm.find(\"#confirm\").click(function () {\n                        data.m3u8dlArg = confirm.find(\"textarea\").val();\n                        data.html.find('#download').click();\n                        confirm.hide();\n                    });\n                    confirm.find(\"#close\").click(function () {\n                        confirm.remove();\n                    });\n                    data.html.append(confirm);\n                    return false;\n                }\n                if (G.isFirefox) {\n                    window.location.href = url;\n                    return false;\n                }\n                chrome.tabs.update({ url: url });\n                return false;\n            }\n            Tips(i18n.blobM3u8DLError, 1500);\n        }\n        if (G.m3u8AutoDown && data.parsing == \"m3u8\") {\n            openParser(data, { autoDown: true });\n            return false;\n        }\n        chrome.downloads.download({\n            url: data.url,\n            filename: data.downFileName,\n            saveAs: G.saveAs\n        }, function (id) { downData[id] = data; });\n        return false;\n    });\n    // 调用\n    data.html.find('.invoke').click(function (event) {\n        const url = data.invoke ?? templates(G.invokeText, data);\n\n        // 下载前确认参数\n        if (G.invokeConfirm && event.originalEvent && event.originalEvent.isTrusted) {\n            data.html.find('.confirm').remove();\n            const confirm = $(`<div class=\"confirm\">\n                        <textarea type=\"text\" class=\"width100\" rows=\"10\">${url}</textarea>\n                        <button class=\"button2\" id=\"confirm\">${i18n.confirm}</button>\n                        <button class=\"button2\" id=\"close\">${i18n.close}</button>\n                    </div>`);\n            confirm.find(\"#confirm\").click(function () {\n                data.invoke = confirm.find(\"textarea\").val();\n                data.html.find('.invoke').click();\n                confirm.hide();\n            });\n            confirm.find(\"#close\").click(function () {\n                confirm.remove();\n            });\n            data.html.append(confirm);\n            return false;\n        }\n\n        if (G.isFirefox) {\n            window.location.href = url;\n        } else {\n            chrome.tabs.update({ url: url });\n        }\n        return false;\n    });\n    //播放\n    data.html.find('#play').click(function () {\n        if (isEmpty(G.Player)) { return true; }\n        if (G.Player == \"$shareApi$\" || G.Player == \"${shareApi}\") {\n            navigator.share({ url: data.url });\n            return false;\n        }\n        let url = templates(G.Player, data);\n        if (G.isFirefox) {\n            window.location.href = url;\n            return false;\n        }\n        chrome.tabs.update({ url: url });\n        return false;\n    });\n    //解析\n    data.html.find('#parsing').click(function (e) {\n        openParser(data);\n        return false;\n    });\n    // 多选框 创建checked属性 值和checked状态绑定\n    data._checked = checkboxState;\n    data.html.find(\".DownCheck\").prop(\"checked\", data._checked);\n    data.html.find('input').click(function (event) {\n        data._checked = this.checked;\n        mergeDownButton();\n        event.originalEvent.cancelBubble = true;\n    });\n    Object.defineProperty(data, \"checked\", {\n        get() {\n            return data._checked;\n        },\n        set(newValue) {\n            data._checked = newValue;\n            data.html.find('input').prop(\"checked\", newValue);\n        }\n    });\n\n    // 数据发送\n    data.html.find(\"#send2local\").click(function () {\n        send2local(\"catch\", data, data.tabId).then(function (success) {\n            success && success?.ok && Tips(i18n.hasSent, 1000);\n        }).catch(function (error) {\n            error ? Tips(error, 1000) : Tips(i18n.sendFailed, 1000);\n        });\n        return false;\n    });\n\n    // MQTT 发送\n    data.html.find(\"#mqtt\").click(function () {\n        const $mqttButton = $(this);\n\n        // 防止重复点击\n        if ($mqttButton.hasClass('mqtt-sending')) {\n            return false;\n        }\n\n        // 禁用按钮并添加发送中状态\n        $mqttButton.addClass('mqtt-sending').prop('disabled', true);\n\n        // 1. 点击后，提示 正在发送到MQTT服务器\n        Tips(i18n.sendingToMQTT || \"Sending to MQTT server...\", 2000);\n\n        sendToMQTT(data).then(function (success) {\n            // 5. 已发送消息到 MQTT 服务器\n            Tips(i18n.messageSentToMQTT || \"Message sent to MQTT server\", 2000);\n        }).catch(function (error) {\n            // 失败时显示详细错误信息\n            const errorMsg = error ? error.toString() : (i18n.sendFailed || \"Send failed\");\n            Tips(errorMsg, 10000);\n            console.error(\"MQTT send error:\", error);\n        }).finally(function () {\n            // 恢复按钮状态\n            $mqttButton.removeClass('mqtt-sending').prop('disabled', false);\n        });\n        return false;\n    });\n\n    // 使用Map 储存数据\n    allData.get(currentTab).set(data.requestId, data);\n\n    // 筛选\n    if (!filterExt.has(data.ext)) {\n        filterExt.set(data.ext, true);\n        const html = $(`<label class=\"flexFilter\" id=\"${data.ext}\"><input type=\"checkbox\" checked>${data.ext}</label>`);\n        html.click(function () {\n            filterExt.set(this.id, html.find(\"input\").prop(\"checked\"));\n            getAllData().forEach(function (value) {\n                if (filterExt.get(value.ext)) {\n                    value.checked = true;\n                    value.html.show();\n                } else {\n                    value.checked = false;\n                    value.html.hide();\n                }\n            });\n        });\n        $filter_ext.append(html);\n    }\n    // 如果被筛选出去 直接隐藏\n    if (!filterExt.get(data.ext) || duplicateFilenamesSet?.has(data.name)) {\n        data.html.hide();\n        data.html.find(\"input\").prop(\"checked\", false);\n    }\n    duplicateFilenamesSet && duplicateFilenamesSet.add(data.name);\n\n    return data.html;\n}\n\nfunction AddKey(key) {\n    // 检查key是否合法\n    const base64Key = base64ToHex(key);\n    if (!base64Key) { return; }\n\n    const data = {};\n    data.html = $(`\n        <div class=\"panel\">\n            <div class=\"panel-heading\">\n                <span class=\"name bold\">${key}</span>\n                <img src=\"img/copy.png\" class=\"icon copy\" id=\"copy\" title=\"${i18n.copy}\"/>\n                <img src=\"img/send.svg\" class=\"icon send ${G.send2localManual || G.send2local ? \"\" : \"hide\"}\"\" id=\"send2local\" title=\"${i18n.send2local}\"/>\n            </div>\n            <div class=\"url hide\">\n                Hex: ${base64Key}\n                <br>\n                Base64: ${key}\n            </div>\n        </div>`);\n    data.html.find('.panel-heading').click(function () {\n        data.html.find(\".url\").toggle();\n    });\n    data.html.find('.copy').click(function () {\n        navigator.clipboard.writeText(key);\n        Tips(i18n.copiedToClipboard);\n        return false;\n    });\n    data.html.find(\"#send2local\").click(function () {\n        send2local(\"addKey\", key).then(function (success) {\n            success && success?.ok && Tips(i18n.hasSent, 1000);\n        }).catch(function (error) {\n            error ? Tips(error, 1000) : Tips(i18n.sendFailed, 1000);\n        });\n        return false;\n    });\n    return data.html;\n}\n\n/********************绑定事件********************/\n//标签切换\n$(\".Tabs .TabButton\").click(function () {\n    activeTab = this.id == \"currentTab\";\n    const index = $(this).index();\n    $(\".Tabs .TabButton\").removeClass('Active');\n    $(this).addClass(\"Active\");\n    $(\".container\").removeClass(\"TabShow\").eq(index).addClass(\"TabShow\");\n    UItoggle();\n    $(\"#filter, #unfold\").hide();\n    $(\"#features\").hide();\n});\n// 其他页面\n$('#allTab').click(function () {\n    !allCount && chrome.runtime.sendMessage(chrome.runtime.id, { Message: \"getAllData\" }, function (data) {\n        if (!data) { return; }\n        for (let key in data) {\n            if (key == G.tabId) { continue; }\n            allCount += data[key].length;\n            for (let i = 0; i < data[key].length; i++) {\n                $all.append(AddMedia(data[key][i], false));\n            }\n        }\n        allCount && $allMediaList.append($all);\n        UItoggle();\n    });\n});\n// 下载选中文件\n$('#DownFile').click(function () {\n    const [checkedData, maxSize] = getCheckedData();\n    if (checkedData.length >= 10 && !confirm(i18n(\"confirmDownload\", [checkedData.length]))) {\n        return;\n    }\n    if (G.enableAria2Rpc) {\n        Tips(i18n.hasSent, 2000);\n        checkedData.forEach(function (data) {\n            aria2AddUri(data);\n        });\n        return;\n    }\n    let index = 0;\n    for (let data of checkedData) {\n        if (G.m3u8dl && (data.parsing == \"m3u8\" || data.parsing == \"mpd\") && !data.url.startsWith(\"blob:\")) {\n            const m3u8dlArg = templates(G.m3u8dlArg, data);\n            const url = 'm3u8dl:' + (G.m3u8dl == 1 ? Base64.encode(m3u8dlArg) : m3u8dlArg);\n            chrome.tabs.create({ url: url });\n            continue;\n        }\n        if (G.m3u8AutoDown && data.parsing == \"m3u8\") {\n            openParser(data, { autoDown: true, autoClose: true });\n            continue;\n        }\n        // 以防止popup页面被关闭 丢失下载数据 批量下载前临时修改为 后台下载\n        G.downActive = true;\n\n        index++;\n        setTimeout(function () {\n            chrome.downloads.download({\n                url: data.url,\n                filename: data.downFileName\n            }, function (id) { downData[id] = data; });\n        }, index * 100);\n    };\n});\n// 合并下载\n$mergeDown.click(function () {\n    const [checkedData, maxSize] = getCheckedData();\n    const taskId = Date.parse(new Date());\n    // 都是m3u8 自动合并并发送到ffmpeg\n    if (checkedData.every(data => isM3U8(data))) {\n        checkedData.forEach(function (data) {\n            openParser(data, { ffmpeg: \"merge\", quantity: checkedData.length, taskId: taskId, autoDown: true, autoClose: true });\n        });\n        return true;\n    }\n    catDownload(checkedData, { ffmpeg: \"merge\" })\n});\n// 复制选中文件\n$('#AllCopy').click(function () {\n    const url = [];\n    getData().forEach(function (data) {\n        data.checked && url.push(copyLink(data));\n    });\n    navigator.clipboard.writeText(url.join(\"\\n\"));\n    Tips(i18n.copiedToClipboard);\n});\n// 全选 反选\n$('#AllSelect, #invertSelection').click(function () {\n    checkboxState = !checkboxState;\n    let checked = false;\n    if (this.id == \"AllSelect\") {\n        checked = true;\n        checkboxState = true;\n    }\n    getData().forEach(function (data) {\n        data.checked = checked ? checked : !data.checked;\n    });\n    mergeDownButton();\n});\n// unfoldAll展开全部  unfoldPlay展开可播放 unfoldFilter展开选中的 fold关闭展开\n$('#unfoldAll, #unfoldPlay, #unfoldFilter, #fold').click(function () {\n    $(\"#features\").hide();\n    if (this.id == \"unfoldAll\") {\n        getData().forEach(function (data) {\n            if (data.html.is(\":hidden\")) { return true; }\n            !data.urlPanelShow && data.panelHeading.click();\n        });\n    } else if (this.id == \"unfoldPlay\") {\n        getData().forEach(function (data) {\n            if (data.html.is(\":hidden\")) { return true; }\n            data.isPlay && !data.urlPanelShow && data.panelHeading.click();\n        });\n    } else if (this.id == \"unfoldFilter\") {\n        getData().forEach(function (data) {\n            if (data.html.is(\":hidden\")) { return true; }\n            data.checked && !data.urlPanelShow && data.panelHeading.click();\n        });\n    } else if (this.id == \"fold\") {\n        getData().forEach(function (data) {\n            if (data.html.is(\":hidden\")) { return true; }\n            data.urlPanelShow && data.panelHeading.click();\n        });\n    }\n});\n// 捕捉/录制 展开按钮 筛选按钮 按钮\n// $('#Catch, #openUnfold, #openFilter, #more').click(function () {\n$('#openFilter, #more').click(function () {\n    // const _height = parseInt($(\".container\").css(\"margin-bottom\"));\n    // $(\".container\").css(\"margin-bottom\", ($down[0].offsetHeight + 26) + \"px\");\n    const $panel = $(`#${this.getAttribute(\"panel\")}`);\n    $panel.css(\"bottom\", $down[0].offsetHeight + \"px\");\n    $(\".more\").not($panel).hide();\n    if ($panel.is(\":hidden\")) {\n        $panel.css(\"display\", \"flex\");\n        return;\n    }\n    // $(\".container\").css(\"margin-bottom\", _height);\n    $panel.hide();\n});\n\n// 正则筛选\n$(\"#regular input\").bind('keypress', function (event) {\n    if (event.keyCode == \"13\") {\n        const input = $(this).val();\n        if (input == \"\") {\n            getData().forEach(function (data) {\n                data.checked = true;\n                data.html.show();\n            });\n            return;\n        }\n        const regex = new RegExp($(this).val());\n        getData().forEach(function (data) {\n            if (!regex.test(data.url)) {\n                data.checked = false;\n                data.html.hide();\n            }\n        });\n        $(\"#filter\").hide();\n    }\n});\n\n// 删除重复文件名\n$(\"#duplicateFilenames\").click(function () {\n    duplicateFilenamesSet = new Set();\n    getData().forEach(function (value) {\n        if (duplicateFilenamesSet.has(value.name)) {\n            value.html.hide();\n            value.checked = false;\n            return;\n        }\n        duplicateFilenamesSet.add(value.name);\n    });\n    $(\"#filter\").hide();\n    mergeDownButton();\n});\n\n// 清空数据\n$('#Clear').click(function () {\n    chrome.runtime.sendMessage({ Message: \"clearData\", tabId: G.tabId, type: activeTab });\n    chrome.runtime.sendMessage({ Message: \"ClearIcon\", type: activeTab, tabId: G.tabId });\n    if (activeTab) {\n        currentCount = 0;\n        $current.empty();\n    } else {\n        allCount = 0;\n        $all.empty();\n    }\n    allData.get(activeTab).clear();\n    UItoggle();\n});\n// 模拟手机端\n$(\"#MobileUserAgent\").click(function () {\n    chrome.runtime.sendMessage({ Message: \"mobileUserAgent\", tabId: G.tabId }, function () {\n        G.refreshClear && $('#Clear').click();\n        updateButton();\n    });\n});\n// 自动下载\n$(\"#AutoDown\").click(function () {\n    chrome.runtime.sendMessage({ Message: \"autoDown\", tabId: G.tabId }, function () {\n        updateButton();\n    });\n});\n// 深度搜索 缓存捕捉 注入脚本\n$(\"[type='script']\").click(function () {\n    chrome.runtime.sendMessage({ Message: \"script\", tabId: G.tabId, script: this.id + \".js\" }, function () {\n        G.autoClearMode > 0 && $('#Clear').click();\n        updateButton();\n    });\n});\n// 102以上开启 捕获按钮/注入脚本\nif (G.version >= 102) {\n    $(\"[type='script']\").show();\n}\n// Firefox 关闭一些功能 修复右边滚动条遮挡\nif (G.isFirefox) {\n    $(\"body\").addClass(\"fixFirefoxRight\");\n    $(\".firefoxHide\").each(function () { $(this).hide(); });\n    if (G.version < 128) {\n        $(\".firefoxHideScript\").each(function () { $(this).hide(); });\n    }\n}\n// 跳转页面\n$(\"[go]\").click(function () {\n    let url = this.getAttribute(\"go\");\n    if (url == \"ffmpegURL\") {\n        chrome.tabs.create({ url: G.ffmpegConfig.url })\n        return;\n    }\n    chrome.tabs.create({ url: url });\n});\n// 暂停 启用\n$(\"#enable\").click(function () {\n    chrome.runtime.sendMessage({ Message: \"enable\" }, function (state) {\n        $(\"#enable\").html(state ? i18n.pause : i18n.enable);\n    });\n});\n// 弹出窗口\n$(\"#popup\").click(function () {\n    switch (G.popupMode) {\n        case 0:\n            chrome.tabs.create({ url: `preview.html?tabId=${G.tabId}` });\n            break;\n        case 1:\n            chrome.tabs.create({ url: `popup.html?tabId=${G.tabId}&type=tab` });\n            break;\n        case 2:\n            chrome.windows.create({ url: `preview.html?tabId=${G.tabId}`, type: \"popup\", height: 1080, width: 1920 });\n            break;\n        case 3:\n            chrome.windows.create({ url: `popup.html?tabId=${G.tabId}&type=window`, type: \"popup\", height: 1080, width: 1920 });\n            break;\n        default:\n            chrome.tabs.create({ url: `preview.html?tabId=${G.tabId}` });\n            break;\n    }\n});\n$(\"#currentPage\").click(function () {\n    chrome.tabs.query({ active: true, currentWindow: false }, function (tabs) {\n        chrome.tabs.update({ url: `popup.html?tabId=${tabs[0].id}${_type ? \"&type=\" + _type : \"\"}` });\n    });\n});\n\n// 手动发送\n$(\"#send2localSelect\").click(function () {\n    getData().forEach(function (item) {\n        if (item.checked) {\n            send2local(\"catch\", item, item.tabId).then(function (success) {\n                success && success?.ok && Tips(i18n.hasSent, 1000);\n            }).catch(function (error) {\n                error ? Tips(error, 1000) : Tips(i18n.sendFailed, 1000);\n            });\n        }\n    });\n});\nasync function getPageDOM() {\n    try {\n        const result = await new Promise((resolve, reject) => {\n            chrome.tabs.sendMessage(G.tabId, { Message: \"getPage\" }, { frameId: 0 }, (response) => {\n                if (chrome.runtime.lastError) {\n                    reject(null);\n                } else {\n                    resolve(response);\n                }\n            });\n        });\n\n        return new DOMParser().parseFromString(result, 'text/html');\n    } catch (error) {\n        console.error('Error getting page:', error);\n        return null;\n    }\n}\n// 一些需要等待G变量加载完整的操作\nconst interval = setInterval(async function () {\n    if (!G.initSyncComplete || !G.initLocalComplete || !G.tabId) { return; }\n    clearInterval(interval);\n\n    if (G.popup && !_tabId) {\n        closeTab();\n        $(\"#popup\").click();\n        return;\n    }\n    // 侧边面板模式 body 宽度100%\n    if (_tabId) {\n        G.tabId = _tabId;\n        $(\"body\").css(\"width\", \"100%\");\n        $(\"#down\").css(\"justify-content\", \"center\").find(\"button\").css(\"margin-left\", \"5px\");\n        $(\"#popup\").hide();\n        _type == \"window\" && $(\"#currentPage\").show();\n    }\n\n    // 获取页面DOM\n    if (G.getHtmlDOM) {\n        pageDOM = await getPageDOM();\n    }\n    // 填充数据\n    chrome.runtime.sendMessage(chrome.runtime.id, { Message: \"getData\", tabId: G.tabId }, function (data) {\n        if (!data || data === \"OK\") {\n            $tips.html(i18n.noData);\n            $tips.attr(\"data-i18n\", \"noData\");\n            return;\n        }\n        currentCount = data.length;\n        if (currentCount >= 500 && confirm(i18n(\"confirmLoading\", [currentCount]))) {\n            $mediaList.append($current);\n            UItoggle();\n            return;\n        }\n        for (let key = 0; key < currentCount; key++) {\n            $current.append(AddMedia(data[key]));\n        }\n        $mediaList.append($current);\n        UItoggle();\n    });\n    // 监听资源数据\n    chrome.runtime.onMessage.addListener(function (Message, sender, sendResponse) {\n        if (!Message.Message || !Message.data) { return; }\n        // 添加资源\n        if (Message.Message == \"popupAddData\") {\n            const html = AddMedia(Message.data, Message.data.tabId == G.tabId);\n            if (Message.data.tabId == G.tabId) {\n                !currentCount && $mediaList.append($current);\n                currentCount++;\n                $current.append(html);\n                UItoggle();\n            } else if (allCount) {\n                allCount++;\n                $all.append(html);\n                UItoggle();\n            }\n            sendResponse(\"OK\");\n            return true;\n        }\n        // 添加疑似密钥\n        if (Message.Message == \"popupAddKey\") {\n            $(\"#maybeKeyTab\").show();\n            chrome.tabs.query({}, function (tabs) {\n                let tabId = -1;\n                for (let item of tabs) {\n                    if (item.url == Message.url) {\n                        tabId = item.id;\n                        break;\n                    }\n                }\n                if (tabId == -1 || tabId == G.tabId) {\n                    $maybeKey.append(AddKey(Message.data));\n                }\n                !$(\"#maybeKey .panel\").length && $(\"#maybeKey\").append($maybeKey);\n            });\n            sendResponse(\"OK\");\n            return true;\n        }\n    });\n    // 获取模拟手机 自动下载 捕获 状态\n    updateButton();\n\n    // 上一次设定的倍数\n    $(\"#playbackRate\").val(G.playbackRate);\n\n    loadCSS();\n\n    const observer = new MutationObserver(updateDownHeight);\n    observer.observe($down[0], { childList: true, subtree: true, attributes: true });\n    setInterval(() => { updateDownHeight(); }, 233);\n    // 疑似密钥\n    chrome.webNavigation.getAllFrames({ tabId: G.tabId }, function (frames) {\n        if (!frames) { return; }\n        for (let frame of frames) {\n            chrome.tabs.sendMessage(G.tabId, { Message: \"getKey\" }, { frameId: frame.frameId }, function (result) {\n                if (chrome.runtime.lastError || !result || result.length == 0) { return; }\n                $(\"#maybeKeyTab\").show();\n                for (let key of result) {\n                    $maybeKey.append(AddKey(key));\n                }\n                $(\"#maybeKey\").append($maybeKey);\n                UItoggle();\n            });\n        }\n    });\n\n    // 是否屏蔽网站\n    chrome.runtime.sendMessage(chrome.runtime.id, { Message: \"damnUrlHas\" }, function (response) {\n        if (response && G.damn) {\n            $tips.html(i18n(\"isBlockedSite\"));\n        }\n    });\n}, 0);\n/********************绑定事件END********************/\nwindow.addEventListener('beforeunload', function () {\n    chrome.runtime.sendMessage(chrome.runtime.id, { Message: \"clearRedundant\" });\n});\n\n// 按钮状态更新\nfunction updateButton() {\n    chrome.runtime.sendMessage({ Message: \"getButtonState\", tabId: G.tabId }, function (state) {\n        for (let key in state) {\n            const $DOM = $(`#${key}`);\n            if (key == \"MobileUserAgent\") {\n                $DOM.html(state.MobileUserAgent ? i18n.closeSimulation : i18n.simulateMobile);\n                continue;\n            }\n            if (key == \"AutoDown\") {\n                $DOM.html(state.AutoDown ? i18n.closeDownload : i18n.autoDownload);\n                continue;\n            }\n            if (key == \"enable\") {\n                $DOM.html(state.enable ? i18n.pause : i18n.enable);\n                continue;\n            }\n            const script = G.scriptList.get(key + \".js\");\n            $DOM.html(state[key] ? script.off : script.name);\n        }\n    });\n}\n/* 格式判断 */\nfunction isPlay(data) {\n    if (G.Player && !isJSON(data) && !isPicture(data)) { return true; }\n    const typeArray = ['video/ogg', 'video/mp4', 'video/webm', 'audio/ogg', 'audio/mp3', 'audio/wav', 'audio/m4a', 'video/3gp', 'video/mpeg', 'video/mov'];\n    return isMediaExt(data.ext) || typeArray.includes(data.type) || isM3U8(data);\n}\n\n// 猫抓下载器\nlet catDownloadIsProcessing = false;\nfunction catDownload(data, extra = {}) {\n    // 防止连续多次提交\n    if (catDownloadIsProcessing) {\n        setTimeout(() => {\n            catDownload(data, extra);\n        }, 233);\n        return;\n    }\n    catDownloadIsProcessing = true;\n    if (!Array.isArray(data)) { data = [data]; }\n\n    // 储存数据到临时变量 提高检索速度\n    localStorage.setItem('downloadData', JSON.stringify(data));\n\n    // 如果大于2G 询问是否使用流式下载\n    if (!extra.ffmpeg && !G.downStream && Math.max(...data.map(item => item._size)) > G.chromeLimitSize && confirm(i18n(\"fileTooLargeStream\", [\"2G\"]))) {\n        extra.downStream = 1;\n    }\n    // 发送消息给下载器\n    chrome.runtime.sendMessage(chrome.runtime.id, { Message: \"catDownload\", data: data }, (message) => {\n        // 不存在下载器或者下载器出错 新建一个下载器\n        if (chrome.runtime.lastError || !message || message.message != \"OK\") {\n            createCatDownload(data, extra);\n            return;\n        }\n        catDownloadIsProcessing = false;\n    });\n}\nfunction createCatDownload(data, extra) {\n    chrome.tabs.get(G.tabId, function (tab) {\n        const arg = {\n            url: `/downloader.html?${new URLSearchParams({\n                requestId: data.map(item => item.requestId).join(\",\"),\n                ...extra\n            })}`,\n            index: tab.index + 1,\n            active: !G.downActive\n        };\n        chrome.tabs.create(arg, (tab) => {\n            // 循环获取tab.id 的状态 准备就绪 重置任务状态\n            const interval = setInterval(() => {\n                chrome.tabs.get(tab.id, (tab) => {\n                    if (chrome.runtime.lastError || tab.status == \"complete\") {\n                        clearInterval(interval);\n                        catDownloadIsProcessing = false;\n                    }\n                });\n            });\n        });\n    });\n}\n\n// 提示\nfunction Tips(text, delay = 200) {\n    // 获取当前提示元素\n    const $tips = $('#TipsFixed');\n\n    // 停止当前所有动画\n    $tips.stop(true, true);\n\n    // 设置新内容并显示\n    $tips\n        .html(text)\n        .fadeIn(500)\n        .delay(delay)\n        .fadeOut(500);\n}\n/*\n* 有资源 隐藏无资源提示\n* 更新数量显示\n* 如果标签是其他设置 隐藏底部按钮\n*/\nfunction UItoggle() {\n    const size = getData().size;\n    size > 0 ? $tips.hide() : $tips.show().html(i18n.noData);\n    $currentCount.text(currentCount ? `[${currentCount}]` : \"\");\n    $allCount.text(allCount ? `[${allCount}]` : \"\");\n    const id = $('.TabShow').attr(\"id\");\n    if (id != \"mediaList\" && id != \"allMediaList\") {\n        $tips.hide();\n        $down.hide();\n    } else if ($down.is(\":hidden\")) {\n        $down.show();\n    }\n    // 更新图标\n    $(\".faviconFlag\").each(function () {\n        const data = getData(this.getAttribute(\"requestId\"));\n        if (data && data.webUrl && favicon.has(data.webUrl)) {\n            this.setAttribute(\"src\", favicon.get(data.webUrl));\n            this.classList.remove(\"faviconFlag\");\n        }\n    });\n    size >= 2 ? mergeDownButton() : $mergeDown.attr('disabled', true);\n}\n// 检查是否符合条件 更改 合并下载 按钮状态\nfunction mergeDownButtonCheck(data) {\n    if (!data.type) {\n        return isMediaExt(data.ext);\n    }\n    return isMediaExt(data.ext) || data.type.startsWith(\"video\") || data.type.startsWith(\"audio\") || data.type.endsWith(\"octet-stream\");\n}\nfunction mergeDownButton() {\n    const [checkedData, maxSize] = getCheckedData();\n    if (checkedData.length != 2 || (!G.isFirefox && maxSize > G.chromeLimitSize)) {\n        // $mergeDown.hide();\n        $mergeDown.attr('disabled', true);\n        return;\n    }\n    if (checkedData.every(mergeDownButtonCheck) || checkedData.every(data => isM3U8(data))) {\n        // $mergeDown.show();\n        $mergeDown.removeAttr('disabled');\n    }\n}\n// 获取当前标签 所有选择的文件\nfunction getCheckedData() {\n    const checkedData = [];\n    let maxSize = 0;\n    getData().forEach(function (data) {\n        if (data.checked) {\n            const size = data._size ?? 0;\n            maxSize = size > maxSize ? size : maxSize;\n            checkedData.push(data);\n        }\n    });\n    return [checkedData, maxSize];\n}\n// 获取当前标签的资源列表 存在requestId返回该资源\nfunction getData(requestId = false) {\n    if (requestId) {\n        return allData.get(activeTab).get(requestId);\n    }\n    return allData.get(activeTab);\n}\n// 获取所有资源列表\nfunction getAllData() {\n    const data = [];\n    data.push(...allData.get(true).values());\n    data.push(...allData.get(false).values());\n    return data;\n}\n\n// 更新底部按钮高度\nfunction updateDownHeight() {\n    $(\".container\").css(\"margin-bottom\", ($down[0].offsetHeight + 2) + \"px\");\n}\n\nfunction base64ToHex(base64) {\n    let binaryString;\n    try {\n        binaryString = atob(base64);\n    } catch (error) {\n        console.error(\"Invalid Base64 string:\", error, base64);\n        return false;\n    }\n    let hexString = '';\n    for (let i = 0; i < binaryString.length; i++) {\n        let hex = binaryString.charCodeAt(i).toString(16);\n        if (hex.length === 1) {\n            hex = '0' + hex;\n        }\n        hexString += hex;\n    }\n    return hexString;\n}"
  },
  {
    "path": "js/preview.js",
    "content": "class FilePreview {\n\n    MAX_CONCURRENT = 16;   // 最大并行生成预览数\n    MAX_LIST_SIZE = 128;  // 最大文件列表长度\n\n    constructor() {\n        this.fileItems = [];         // 文件列表\n        this.originalItems = [];     // 原始文件列表\n        this.regexFilters = null;    // 正则过滤\n        this.pushDebounce = null;   // 添加文件防抖\n        this.alertTimer = null;     // 提示信息定时器\n        this.isDragging = false;    // 是否正在拖动\n        this.previewHLS = null;     // 全屏预览视频HLS工具\n        this.catDownloadIsProcessing = false; // 猫抓下载器是否正在处理\n\n        this.showTitle = false; // 是否显示标题\n        this.deleteDuplicateFilenames = false; // 是否删除重复文件名\n\n        this._tabId = -1; // 当前标签ID\n        this.currentRange = null; // 当前显示范围\n        this.currentPage = 1; // 当前页码\n\n        // 初始化\n        this.init();\n    }\n    /**\n     * 初始化\n     */\n    async init() {\n        this.parseParams();             // url解析参数\n        this.tab = await chrome.tabs.getCurrent();  // 获取当前标签\n        this.setupEventListeners();     // 设置事件监听\n        await this.loadFileItems();     // 载入数据\n        this.setupFilters();            // 设置 后缀/类型 筛选\n        this.setOptions();              // 设置选项\n        this.updateFileList();          // 渲染文件列表\n        this.startPreviewGeneration();  // 开始预览生成\n        this.setupSelectionBox();       // 框选\n        this.srciptList();              // 脚本列表\n        this.updateSrciptButton();      // 更新按钮状态\n        this.checkVersion();            // 检查版本\n    }\n    /**\n     * 解析参数\n     */\n    parseParams() {\n        // 获取tabId\n        const params = new URL(location.href).searchParams;\n        this._tabId = parseInt(params.get(\"tabId\"));\n        if (isNaN(this._tabId)) {\n            this.alert(i18n.noData, 1500);\n            return;\n        }\n\n        // 显示范围\n        this.currentRange = params.get(\"range\")?.split(\"-\").map(Number);\n        if (this.currentRange) {\n            this.currentRange = { start: this.currentRange[0], end: this.currentRange[1] || undefined };\n        }\n\n        // 分页\n        this.currentPage = params.get(\"page\");\n        this.currentPage = this.currentPage ? parseInt(this.currentPage) : 1;\n    }\n    /**\n     * 设置按钮、键盘 、等事件监听\n     */\n    setupEventListeners() {\n        // 全选\n        document.querySelector('#select-all').addEventListener('click', () => this.toggleSelection('all'));\n        // 反选\n        document.querySelector('#select-reverse').addEventListener('click', () => this.toggleSelection('reverse'));\n        // 下载选中\n        document.querySelector('#download-selected').addEventListener('click', () => this.downloadSelected());\n        // 合并下载\n        document.querySelector('#merge-download').addEventListener('click', () => this.mergeDownload());\n        // 点击非视频区域 关闭视频\n        document.querySelectorAll('.preview-container').forEach(container => {\n            container.addEventListener('click', (event) => {\n                if (event.target.closest('video, img')) { return; }\n                this.closePreview()\n            });\n        });\n        // 按键盘ESC关闭视频\n        document.addEventListener('keydown', (event) => {\n            if (event.key === 'Escape') {\n                this.closePreview();\n                return;\n            }\n            // ctrl + a\n            if ((event.ctrlKey || event.metaKey) && event.key === 'a' && event.target.tagName != \"INPUT\") {\n                this.toggleSelection('all');\n                event.preventDefault();\n            }\n        });\n        // 排序按钮\n        document.querySelectorAll('.sort-options input').forEach(input => {\n            input.addEventListener('change', () => this.updateFileList());\n        });\n        // 正则过滤 监听回车\n        document.querySelector('#regular').addEventListener('keypress', (e) => {\n            if (e.keyCode == 13) {\n                const value = e.target.value.trim();\n                try {\n                    this.regexFilters = value ? new RegExp(value) : null;\n                } catch (error) {\n                    this.regexFilters = null;\n                    this.alert(i18n.noMatch);\n                }\n                this.updateFileList();\n            }\n        });\n        // 复制\n        document.querySelector('#copy-selected').addEventListener('click', () => this.copy());\n        // 清理数据\n        document.querySelector('#clear').addEventListener('click', () => this.clearData());\n        // 删除\n        document.querySelector('#delete-selected').addEventListener('click', () => this.deleteItem());\n        // debug\n        document.querySelector('#debug').addEventListener('click', () => console.dir(this.fileItems));\n        // 显示标题\n        document.querySelector('#showTitle').addEventListener('change', (e) => {\n            (chrome.storage.session ?? chrome.storage.local).set({ previewShowTitle: e.target.checked });\n            this.showTitle = e.target.checked;\n            this.fileItems.forEach(item => {\n                item.html.querySelector('.file-title').classList.toggle('hide', !e.target.checked);\n            });\n            this.updateFileList();\n        });\n        document.querySelector('#deleteDuplicateFilenames').addEventListener('change', (e) => {\n            (chrome.storage.session ?? chrome.storage.local).set({ previewDeleteDuplicateFilenames: e.target.checked });\n            this.deleteDuplicateFilenames = e.target.checked;\n            this.updateFileList();\n        });\n        // aria2\n        if (G.enableAria2Rpc) {\n            const aria2 = document.querySelector(\"#aria2-selected\");\n            aria2.classList.remove(\"hide\");\n            aria2.addEventListener('click', () => {\n                this.getSelectedItems().forEach(item => this.aria2(item));\n            });\n        }\n        // 发送\n        if (G.send2localManual) {\n            const send = document.querySelector(\"#send-selected\");\n            send.classList.remove(\"hide\");\n            send.addEventListener('click', () => {\n                this.getSelectedItems().forEach(item => this.send(item));\n            });\n        }\n\n        // 默认弹出模式\n        document.querySelector('#defaultPopup').addEventListener('change', (e) => {\n            chrome.storage.sync.set({ popup: e.target.checked });\n        });\n    }\n    // 全选/反选\n    toggleSelection(type) {\n        this.fileItems.forEach(item => {\n            item.selected = type === 'all' ? true :\n                type === 'reverse' ? !item.selected : false;\n        });\n        this.updateButtonStatus();\n    }\n    /**\n     * 获取选中元素 转为对象\n     */\n    getSelectedItems() {\n        return this.fileItems.filter(item => item.selected);\n    }\n    /**\n     * 更新按钮状态\n     */\n    updateButtonStatus() {\n        const selectedItems = this.getSelectedItems();\n\n        const hasItems = selectedItems.length > 0;\n        const canMerge = selectedItems.length === 2 && (\n            selectedItems.every(item => (item.size ?? 0) <= G.chromeLimitSize && isMedia(item)) ||\n            selectedItems.every(isM3U8)\n        );\n\n        document.querySelector('#delete-selected').disabled = !hasItems;\n        document.querySelector('#merge-download').disabled = !canMerge;\n        document.querySelector('#copy-selected').disabled = !hasItems;\n        document.querySelector('#download-selected').disabled = !hasItems;\n        document.querySelector('#aria2-selected').disabled = !hasItems;\n        document.querySelector('#send-selected').disabled = !hasItems;\n    }\n    /**\n     * 合并下载\n     */\n    mergeDownload() {\n        chrome.runtime.sendMessage({\n            Message: \"catCatchFFmpeg\",\n            action: \"openFFmpeg\",\n            extra: i18n.waitingForMedia\n        });\n        const checkedData = this.getSelectedItems();\n        // 都是m3u8 自动合并并发送到ffmpeg\n        if (checkedData.every(data => isM3U8(data))) {\n            const taskId = Date.parse(new Date());\n            checkedData.forEach((data) => {\n                this.openM3U8(data, { ffmpeg: \"merge\", quantity: checkedData.length, taskId: taskId, autoDown: true, autoClose: true });\n            });\n            return;\n        }\n        this.catDownload(checkedData, { ffmpeg: \"merge\" });\n    }\n\n    /**\n     * 下载文件\n     * @param {Object} data 下载数据\n     */\n    downloadItem(data) {\n        if (G.m3u8dl && isM3U8(data)) {\n            if (!data.url.startsWith(\"blob:\")) {\n                const m3u8dlArg = data.m3u8dlArg ?? templates(G.m3u8dlArg, data);\n                const url = 'm3u8dl:' + (G.m3u8dl == 1 ? Base64.encode(m3u8dlArg) : m3u8dlArg);\n                if (url.length >= 2046) {\n                    navigator.clipboard.writeText(m3u8dlArg);\n                    alert(i18n.M3U8DLparameterLong);\n                    return;\n                }\n                if (G.isFirefox) {\n                    window.location.href = url;\n                    return;\n                }\n                chrome.tabs.update({ url: url });\n                return;\n            }\n        }\n        if (G.m3u8AutoDown && isM3U8(data)) {\n            this.openM3U8(data, { taskId: Date.parse(new Date()), autoDown: true, autoClose: true });\n            return;\n        }\n        this.catDownload(data);\n    }\n    /**\n     * 删除文件\n     * @param {Object|null} data \n     */\n    deleteItem(data = null) {\n        data = data ? [data] : this.getSelectedItems();\n        data.forEach(item => {\n            const index = this.originalItems.findIndex(originalItem => originalItem.requestId === item.requestId);\n            if (index !== -1) {\n                this.originalItems.splice(index, 1);\n            }\n        });\n        this.updateFileList();\n    }\n    /**\n     * 复制文件链接\n     * @param {Object|null} item \n     */\n    copy(data = null) {\n        data = data ? [data] : this.getSelectedItems();\n        const url = [];\n        data.forEach(function (item) {\n            url.push(copyLink(item));\n        });\n        navigator.clipboard.writeText(url.join(\"\\n\"));\n        this.alert(i18n.copiedToClipboard);\n    }\n    mqtt(data) {\n        this.alert(i18n.sending2MQTT);\n        sendToMQTT(data, { alert: this.alert }).then((success) => {\n            // 5. 已发送消息到 MQTT 服务器\n            this.alert(i18n.messageSentToMQTT || \"Message sent to MQTT server\", 2000);\n        }).catch((error) => {\n            // 失败时显示详细错误信息\n            const errorMsg = error ? error.toString() : (i18n.sendFailed || \"Send failed\");\n            this.alert(errorMsg, 10000);\n            console.error(\"MQTT send error:\", error);\n        });\n    }\n    /**\n     * 下载选中\n     */\n    downloadSelected() {\n        const data = this.getSelectedItems();\n        data.length && this.catDownload(data);\n    }\n    /**\n     * 发送到aria2\n     * @param {Object} data 文件对象\n     */\n    aria2(data) {\n        aria2AddUri(data, (success) => {\n            this.alert(success, 1000);\n        }, (msg) => {\n            this.alert(msg, 1500);\n        });\n    }\n    /**\n     * 调用第三方工具\n     * @param {Object} data 文件对象\n     */\n    invoke(data) {\n        const url = templates(G.invokeText, data);\n        if (G.isFirefox) {\n            window.location.href = url;\n        } else {\n            chrome.tabs.update({ url: url });\n        }\n    }\n    /**\n     * 发送到远程或本地地址\n     * @param {Object} data 文件对象\n     */\n    send(data) {\n        send2local(\"catch\", data, this._tabId).then((success) => {\n            success && success?.ok && this.alert(i18n.hasSent, 1000);\n        }).catch((error) => {\n            error ? this.alert(error, 1500) : this.alert(i18n.sendFailed, 1500);\n        });\n    }\n    /**\n     * 更新文件列表\n     */\n    updateFileList() {\n        this.fileItems = [...this.originalItems];\n\n        // 删除重复的文件名\n        if (this.deleteDuplicateFilenames) {\n            const uniqueNames = new Set();\n            this.fileItems = this.fileItems.filter(item => {\n                if (uniqueNames.has(item.name)) {\n                    return false;\n                }\n                uniqueNames.add(item.name);\n                return true;\n            });\n        }\n\n        // 获取勾选扩展\n        const selectedExts = Array.from(document.querySelectorAll('input[name=\"ext\"]:checked'))\n            .map(checkbox => checkbox.value);\n\n        //勾选类型\n        const selectedTyps = Array.from(document.querySelectorAll('input[name=\"type\"]:checked'))\n            .map(checkbox => checkbox.value);\n\n        // 应用 正则 and 扩展过滤\n        this.fileItems = this.fileItems.filter(item =>\n            selectedExts.includes(item.ext) && selectedTyps.includes(item.type) &&\n            (!this.regexFilters || this.regexFilters.test(item.url))\n        );\n        // 排序\n        const order = document.querySelector('input[name=\"sortOrder\"]:checked').value === 'asc' ? 1 : -1;\n        const field = document.querySelector('input[name=\"sortField\"]:checked').value;\n        if (field === 'name') {\n            this.fileItems.sort((a, b) => order * a[field].localeCompare(b[field]));\n        } else if (field == 'duration') {\n            this.fileItems.sort((a, b) => {\n                // If both have invalid duration (-1), maintain original order\n                if (a[field] === -1 && b[field] === -1) return 0;\n                // Items with duration -1 always go to the end\n                if (a[field] === -1) return 1;\n                if (b[field] === -1) return -1;\n                // Normal comparison for valid durations\n                return order * (a[field] - b[field]);\n            });\n        } else {\n            this.fileItems.sort((a, b) => order * (a[field] - b[field]));\n        }\n\n        // 更新显示\n        this.renderFileItems();\n        this.updateButtonStatus();\n    }\n    /**\n     * 创建文件元素\n     * @param {Object} item 数据\n     * @param {Number} index 索引\n     */\n    createFileElement(item, index) {\n        if (item.html) { return item.html; }\n        item.html = document.createElement('div');\n        item.html.setAttribute('data-index', index);\n        item.html.className = 'file-item';\n        item.html.innerHTML = `\n            <div class=\"file-title ${this.showTitle ? \"\" : \"hide\"}\">${item.title}</div>\n            <div class=\"file-name\">${item.name}</div>\n            <div class=\"preview-container\">\n                <img src=\"${item.favIconUrl || 'img/icon.png'}\" class=\"preview-image icon\">\n            </div>\n            <div class=\"bottom-row\">\n                <img src=\"img/regex.png\" class=\"${item.isRegex ? \"\" : \"hide\"}\" title=\"${i18n.regexTitle}\" style=\"width: 23px;\">\n                <div class=\"file-info\">${item.ext}</div>\n            </div>\n            <div class=\"actions\">\n                <img src=\"img/copy.png\" class=\"icon copy\" title=\"${i18n.copy}\">\n                <img src=\"img/delete.svg\" class=\"icon delete\" title=\"${i18n.delete}\">\n                <img src=\"img/download.svg\" class=\"icon download\" title=\"${i18n.download}\">\n                <img src=\"img/mqtt.svg\" class=\"icon mqtt ${G.mqttEnable ? \"\" : \"hide\"}\" title=\"${i18n.send2MQTT}\">\n            </div>`;\n        // 添加文件信息\n        if (item.size && item.size >= 1024) {\n            item.html.querySelector('.file-info').textContent += ` / ${byteToSize(item.size)}`;\n        }\n        item.html.addEventListener('click', (event) => {\n            if (event.target.closest('.icon') || this.isDragging) { return; }\n            item.selected = !item.selected;\n            this.updateButtonStatus();\n        });\n        // 复制图标\n        item.html.querySelector('.copy').addEventListener('click', () => this.copy(item));\n        // 删除图标\n        item.html.querySelector('.delete').addEventListener('click', () => this.deleteItem(item));\n        // 下载图标\n        item.html.querySelector('.download').addEventListener('click', () => this.downloadItem(item));\n        // MQTT图标\n        item.html.querySelector('.mqtt').addEventListener('click', () => this.mqtt(item));\n        // 选中状态 添加对应class\n        item._selected = false;\n        Object.defineProperty(item, \"selected\", {\n            get: () => item._selected,\n            set(newValue) {\n                item._selected = newValue;\n                item.html.classList.toggle('selected', newValue);\n            }\n        });\n        // 图片预览\n        if (isPicture(item)) {\n            const previewImage = item.html.querySelector('.preview-image');\n            previewImage.onload = () => {\n                item.html.querySelector('.file-info').textContent += ` / ${previewImage.naturalWidth}*${previewImage.naturalHeight}`;\n            };\n            previewImage.src = item.url;\n            // 点击预览图片\n            previewImage.addEventListener('click', (event) => {\n                event.stopPropagation();\n                const container = document.querySelector('.image-container');\n                container.querySelector('img').src = item.url;\n                container.classList.remove('hide');\n            });\n        }\n\n        // 添加一些图标 和 事件\n        const actions = item.html.querySelector('.actions');\n\n        if (isM3U8(item)) {\n            const m3u8 = document.createElement('img');\n            m3u8.src = 'img/parsing.png';\n            m3u8.className = 'icon m3u8';\n            m3u8.title = i18n.parser;\n            m3u8.addEventListener('click', () => this.openM3U8(item));\n            actions.appendChild(m3u8);\n        }\n\n        // 发送到aria2\n        if (G.enableAria2Rpc) {\n            const aria2 = document.createElement('img');\n            aria2.src = 'img/aria2.png';\n            aria2.className = 'icon aria2';\n            aria2.title = \"aria2\";\n            aria2.addEventListener('click', () => this.aria2(item));\n            actions.appendChild(aria2);\n        }\n\n        // 调用第三方工具\n        if (G.invoke) {\n            const invoke = document.createElement('img');\n            invoke.src = 'img/invoke.svg';\n            invoke.className = 'icon invoke';\n            invoke.title = i18n.invoke;\n            invoke.addEventListener('click', () => this.invoke(item));\n            actions.appendChild(invoke);\n        }\n\n        // 发送到远程或本地地址\n        if (G.send2localManual) {\n            const send = document.createElement('img');\n            send.src = 'img/send.svg';\n            send.className = 'icon send';\n            send.title = i18n.send2local;\n            send.addEventListener('click', () => this.send(item));\n            actions.appendChild(send);\n        }\n\n        return item.html;\n    }\n    /**\n     * 设置复选框\n     * @param {String} filterId 过滤器的DOM ID\n     * @param {Array} items 数据项\n     * @param {String} property 数据项的属性\n     */\n    setupFilters(filterId, property) {\n        if (arguments.length === 0) {\n            this.setupFilters('extensionFilters', 'ext');\n            this.setupFilters('typeFilters', 'type');\n            return;\n        }\n        const uniqueValues = [...new Set(this.originalItems.map(item => item[property]))];\n        const filterContainer = document.querySelector(`#${filterId}`);\n        uniqueValues.forEach(value => {\n            if (filterContainer.querySelector(`input[value=\"${value}\"]`)) return;\n            const label = document.createElement('label');\n            label.innerHTML = `<input type=\"checkbox\" name=\"${property}\" value=\"${value}\" checked>${value == 'Unknown' ? value : value.toLowerCase()}`;\n            label.querySelector('input').addEventListener('click', () => this.updateFileList());\n            filterContainer.appendChild(label);\n        });\n    }\n\n    setOptions() {\n        if (G.previewShowTitle) {\n            document.querySelector('#showTitle').checked = true;\n            this.showTitle = true;\n        }\n        if (G.previewDeleteDuplicateFilenames) {\n            document.querySelector('#deleteDuplicateFilenames').checked = true;\n            this.deleteDuplicateFilenames = true;\n        }\n    }\n\n    /**\n     * 渲染文件列表\n     */\n    renderFileItems() {\n        const fragment = document.createDocumentFragment();\n        this.fileItems.forEach((item, index) => {\n            fragment.appendChild(this.createFileElement(item, index));\n        });\n        const container = document.querySelector('#file-container');\n        container.innerHTML = '';\n        container.appendChild(fragment);\n    }\n    /**\n     * 修整数据\n     * @param {Object} data 数据\n     */\n    trimData(data) {\n        data._title = data.title.replace(/[/\\\\]/g, \"_\");\n        data.title = stringModify(data.title);\n\n        data.name = isEmpty(data.name) ? data.title + '.' + data.ext : decodeURIComponent(stringModify(data.name));\n\n        data.downFileName = G.TitleName ? templates(G.downFileName, data) : data.name;\n        data.downFileName = filterFileName(data.downFileName);\n        if (isEmpty(data.downFileName)) {\n            data.downFileName = data.name;\n        }\n        data.ext = data.ext ? data.ext : 'Unknown';\n        data.type = data.type ? data.type : 'Unknown';\n        data.duration = data.duration ? data.duration : -1;\n        return data;\n    }\n    /**\n     * 载入数据\n     */\n    async loadFileItems() {\n        this.originalItems = await chrome.runtime.sendMessage(chrome.runtime.id, { Message: \"getData\", tabId: this._tabId }) || [];\n        if (this.originalItems.length == 0) {\n            this.alert(i18n.noData, 1500);\n            return;\n        }\n        // 设置分页\n        if (this.originalItems.length > this.MAX_LIST_SIZE) {\n            this.setupPage(this.originalItems.length);\n            this.originalItems = this.originalItems.slice((this.currentPage - 1) * this.MAX_LIST_SIZE, this.currentPage * this.MAX_LIST_SIZE);\n        }\n        // 显示范围\n        if (this.currentRange) {\n            this.originalItems = this.originalItems.slice(this.currentRange.start, this.currentRange.end ?? this.originalItems.length);\n        }\n        this.originalItems = this.originalItems.map(data => this.trimData(data));\n        this.fileItems = [...this.originalItems];\n        setHeaders(this.fileItems, null, this.tab.id);\n\n    }\n    /**\n     * 关闭预览视频\n     */\n    closePreview() {\n        document.querySelector('.play-container').classList.add('hide');\n        const video = document.querySelector('#video-player');\n        video.pause();\n        video.src = '';\n        this.previewHLS && this.previewHLS.destroy();\n\n        const imageContainer = document.querySelector('.image-container');\n        imageContainer.classList.add('hide');\n    }\n    /**\n     * 播放文件\n     * @param {Object} item \n     */\n    playItem(item) {\n        const video = document.querySelector('#video-player');\n        const container = document.querySelector('.play-container');\n        if (isM3U8(item)) {\n            this.previewHLS = new Hls({ enableWorker: false });\n            this.previewHLS.loadSource(item.url);\n            this.previewHLS.attachMedia(video);\n            this.previewHLS.on(Hls.Events.ERROR, (event, data) => {\n                this.previewHLS.stopLoad();\n                this.previewHLS.destroy();\n            });\n            this.previewHLS.on(Hls.Events.MEDIA_ATTACHED, () => {\n                container.classList.remove('hide');\n                video.play();\n            });\n        } else {\n            video.src = item.url;\n            container.classList.remove('hide');\n            video.play();\n        }\n    }\n    /**\n     * 生成预览video标签\n     * @param {Object} item 数据\n     */\n    async generatePreview(item) {\n        return new Promise((resolve, reject) => {\n            const getVideoInfo = (video) => {\n                video.pause();\n                videoInfo.height = video.videoHeight;\n                videoInfo.width = video.videoWidth;\n\n                if (video.duration && video.duration != Infinity) {\n                    videoInfo._duration = video.duration;\n                    videoInfo.duration = secToTime(video.duration);\n                }\n\n                // 判断是否为音频文件\n                if (item.type?.startsWith('audio/') || ['mp3', 'wav', 'm4a', 'aac', 'ogg'].includes(item.ext)) {\n                    videoInfo.type = 'audio';\n                    videoInfo.video = null;\n                    videoInfo.height = 0;\n                    videoInfo.width = 0;\n                }\n                resolve(videoInfo);\n            };\n            const video = document.createElement('video');\n            video.muted = true;\n            video.playsInline = true;\n            video.loop = true;\n            video.preload = 'metadata';\n            video.addEventListener('loadedmetadata', () => {\n                video.currentTime = 0.5;\n                if (video.videoHeight && video.videoWidth) {\n                    getVideoInfo(video);\n                } else {\n                    setTimeout(getVideoInfo, 500, video);\n                }\n            });\n\n            let hls = null;\n\n            const cleanup = () => {\n                if (hls) hls.destroy();\n                video.remove();\n            };\n\n            const videoInfo = { video: video, height: 0, width: 0, duration: 0, type: 'video' };\n            // 处理HLS视频\n            if (isM3U8(item)) {\n                if (!Hls.isSupported()) {\n                    return reject(new Error('HLS is not supported'));\n                }\n\n                hls = new Hls({ enableWorker: false });\n                hls.loadSource(item.url);\n                hls.attachMedia(video);\n                videoInfo.type = 'hlsVideo';\n\n                hls.on(Hls.Events.ERROR, (event, data) => {\n                    cleanup();\n                    reject(data);\n                });\n            }\n            // 处理普通视频\n            else {\n                video.src = item.url;\n                video.addEventListener('error', () => {\n                    cleanup();\n                    reject(new Error('Video load failed'));\n                });\n            }\n        });\n    }\n    /**\n     * 设置预览video标签到对应位置 以及添加鼠标悬停事件\n     * @param {Object} item data\n     */\n    setPerviewVideo(item) {\n        // 视频放入预览容器 增加class\n        const container = item.html.querySelector('.preview-container');\n        container.classList.add('video-preview');\n\n        if (item.previewVideo.type == 'audio' || (item.previewVideo.width == 0 && item.previewVideo.height == 0)) {\n            // 如果是音频文件，使用音乐图标\n            container.innerHTML = '<img src=\"img/music.svg\" class=\"preview-music icon\" />';\n        } else {\n            if (container.querySelector('video')) return;\n            // 如果是视频文件，使用视频预览\n            container.appendChild(item.previewVideo.video);\n            // 鼠标悬停事件\n            item.html.addEventListener('mouseenter', () => {\n                item.previewVideo.video.play();\n            });\n            item.html.addEventListener('mouseleave', () => {\n                item.previewVideo.video.pause();\n            });\n            // 填写视频信息\n            item.html.querySelector('.file-info').textContent += ` / ${item.previewVideo.width}*${item.previewVideo.height}`;\n        }\n        // 点击视频 全屏播放 阻止冒泡 以免选中\n        container.querySelectorAll(\"video, .preview-music\").forEach((element) => {\n            element.addEventListener('click', (event) => {\n                event.stopPropagation();\n                this.playItem(item);\n            });\n        });\n        // 填写时长\n        if (item.previewVideo.duration) {\n            item.duration = item.previewVideo._duration;\n            item.html.querySelector('.file-info').textContent += ` / ${item.previewVideo.duration}`;\n        }\n\n        // 删除 preview-image\n        item.html.querySelector('.preview-image')?.remove();\n    }\n    /**\n     * 多线程 开始生成预览video标签\n     */\n    async startPreviewGeneration() {\n        const pendingItems = this.fileItems.filter(item =>\n            !item.previewVideo &&\n            !item.previewVideoError &&\n            (item.type?.startsWith('video/') ||\n                item.type?.startsWith('audio/') ||\n                isMediaExt(item.ext) ||\n                isM3U8(item))\n        );\n\n        const processItem = async () => {\n            while (pendingItems.length) {\n                const item = pendingItems.shift();\n                if (!item?.url) continue;\n                try {\n                    item.previewVideo = await this.generatePreview(item);\n                    this.setPerviewVideo(item);\n                    // console.log('Preview generated for:', item.url);\n                } catch (e) {\n                    item.previewVideoError = true;\n                    // console.warn('Failed to generate preview for:', item.url, e);\n                }\n            }\n        };\n        await Promise.all(Array(this.MAX_CONCURRENT).fill().map(processItem));\n    }\n    /**\n     * 猫抓下载器\n     * @param {Object} data \n     * @param {Object} extra \n     */\n    catDownload(data, extra = {}) {\n        // 防止连续多次提交\n        if (this.catDownloadIsProcessing) {\n            setTimeout(() => {\n                catDownload(data, extra);\n            }, 233);\n            return;\n        }\n        this.catDownloadIsProcessing = true;\n        if (!Array.isArray(data)) { data = [data]; }\n\n        // 储存数据到临时变量 提高检索速度\n        localStorage.setItem('downloadData', JSON.stringify(data));\n\n        // 如果大于2G 询问是否使用流式下载\n        if (!extra.ffmpeg && !G.downStream && Math.max(...data.map(item => item._size)) > G.chromeLimitSize && confirm(i18n(\"fileTooLargeStream\", [\"2G\"]))) {\n            extra.downStream = 1;\n        }\n        // 发送消息给下载器\n        chrome.runtime.sendMessage(chrome.runtime.id, { Message: \"catDownload\", data: data }, (message) => {\n            // 不存在下载器或者下载器出错 新建一个下载器\n            if (chrome.runtime.lastError || !message || message.message != \"OK\") {\n                chrome.tabs.create({\n                    url: `/downloader.html?${new URLSearchParams({\n                        requestId: data.map(item => item.requestId).join(\",\"),\n                        ...extra\n                    })}`,\n                    index: this.tab.index + 1,\n                    active: !G.downActive\n                }, (tab) => {\n                    const listener = (tabId, info) => {\n                        if (tab && tabId === tab.id && info.status === \"complete\") {\n                            chrome.tabs.onUpdated.removeListener(listener);\n                            this.catDownloadIsProcessing = false;\n                        }\n                    };\n                    chrome.tabs.onUpdated.addListener(listener);\n                });\n                return;\n            }\n            this.catDownloadIsProcessing = false;\n        });\n    }\n    /**\n     * 设置框选\n     */\n    setupSelectionBox() {\n        const selectionBox = document.getElementById('selection-box');\n        const container = document.querySelector('body');\n        // let isDragging = false;\n        let isSelecting = false;\n        const startPoint = { x: 0, y: 0 };\n\n        container.addEventListener('mousedown', (e) => {\n            if (e.button == 2) return;\n            // 限定起始位范围\n            if (e.target.closest('.icon, .preview-image, video, button, input')) return;\n\n            isSelecting = true;\n            startPoint.x = e.pageX;\n            startPoint.y = e.pageY;\n        });\n\n        document.addEventListener('mousemove', (e) => {\n            if (!isSelecting) return;\n\n            const currentPoint = {\n                x: e.pageX,\n                y: e.pageY\n            };\n\n            // 计算移动距离，只有真正拖动时才显示选择框\n            const moveDistance = Math.sqrt(\n                Math.pow(currentPoint.x - startPoint.x, 2) +\n                Math.pow(currentPoint.y - startPoint.y, 2)\n            );\n\n            // 如果移动距离大于5像素，认为是拖动而不是点击\n            if (!this.isDragging && moveDistance > 5) {\n                this.isDragging = true;\n                selectionBox.style.display = 'block';\n            }\n\n            if (!this.isDragging) return;\n\n            // 计算选择框的位置和大小\n            const left = Math.min(startPoint.x, currentPoint.x);\n            const top = Math.min(startPoint.y, currentPoint.y);\n            const width = Math.abs(currentPoint.x - startPoint.x);\n            const height = Math.abs(currentPoint.y - startPoint.y);\n\n            selectionBox.style.left = `${left}px`;\n            selectionBox.style.top = `${top}px`;\n            selectionBox.style.width = `${width}px`;\n            selectionBox.style.height = `${height}px`;\n\n            // 检查每个file-item是否在选择框内\n            this.fileItems.forEach(item => {\n                const rect = item.html.getBoundingClientRect();\n                if (rect.left + window.scrollX < left + width &&\n                    rect.left + rect.width + window.scrollX > left &&\n                    rect.top + window.scrollY < top + height &&\n                    rect.top + rect.height + window.scrollY > top) {\n                    item.selected = true;\n                } else {\n                    item.selected = false;\n                }\n            });\n        });\n\n        document.addEventListener('mouseup', (e) => {\n            if (e.button == 2 || !isSelecting) return;\n\n            isSelecting = false;\n            setTimeout(() => { this.isDragging = false; }, 10);\n            selectionBox.style.display = 'none';\n            selectionBox.style.width = '0';\n            selectionBox.style.height = '0';\n            this.updateButtonStatus();\n        });\n    }\n\n    /**\n     * 打开m3u8解析器\n     * @param {Object} data \n     * @param {Object} options \n     */\n    openM3U8(data, options = {}) {\n        const url = `/m3u8.html?${new URLSearchParams({\n            url: data.url,\n            title: data.title,\n            filename: data.downFileName,\n            tabid: data.tabId == -1 ? this._tabId : data.tabId,\n            initiator: data.initiator,\n            requestHeaders: data.requestHeaders ? JSON.stringify(data.requestHeaders) : undefined,\n            ...Object.fromEntries(Object.entries(options).map(([key, value]) => [key, typeof value === 'boolean' ? 1 : value])),\n        })}`\n        chrome.tabs.create({ url: url, index: this.tab.index + 1, active: !options.autoDown });\n    }\n    /**\n     * 提示信息\n     * @param {String} message 提示信息\n     * @param {Number} sec 显示时间\n     */\n    alert = (message, sec = 1000) => {\n        let toast = document.querySelector('.alert-box');\n        if (!toast) {\n            toast = document.createElement('div');\n            toast.className = 'alert-box';\n            document.body.appendChild(toast);\n        }\n        // 显示期间新消息顶替\n        clearTimeout(this.alertTimer);\n        toast.classList.remove('active');\n\n        toast.textContent = message;\n        toast.classList.add('active');\n        this.alertTimer = setTimeout(() => {\n            toast.classList.remove('active');\n        }, sec);\n    }\n    /**\n     * 添加文件\n     * @param {Object} data \n     */\n    push(data) {\n        if (this.originalItems.length >= this.MAX_LIST_SIZE) {\n            return;\n        }\n        setHeaders(data, null, this.tab.id);\n        this.originalItems.push(this.trimData(data));\n\n        // this.startPreviewGeneration(); 防抖\n        clearTimeout(this.pushDebounce);\n        this.pushDebounce = setTimeout(() => {\n            this.setupFilters();\n            this.updateFileList();\n            this.startPreviewGeneration();\n        }, 1000);\n    }\n\n    /**\n     * 设置分页\n     * @param {Number} fileLength 文件数\n     */\n    setupPage(fileLength) {\n        const url = new URL(location.href);\n        document.querySelector('.pagination').classList.remove('hide'); // 显示页面组件\n        const maxPage = Math.ceil(fileLength / this.MAX_LIST_SIZE); // 最大页数\n\n        // 设置页码\n        document.querySelector('.page-numbers').textContent = `${this.currentPage} / ${maxPage}`;\n\n        // 上一页按钮\n        if (this.currentPage != 1) {\n            const prev = document.querySelector('#prev-page');\n            prev.disabled = false;\n            prev.addEventListener('click', () => {\n                url.searchParams.set('page', this.currentPage - 1);\n                chrome.tabs.update({ url: url.toString() });\n            });\n        }\n\n        // 下一页按钮\n        if (this.currentPage != maxPage) {\n            const next = document.querySelector('#next-page');\n            next.disabled = false;\n            next.addEventListener('click', () => {\n                url.searchParams.set('page', this.currentPage + 1);\n                chrome.tabs.update({ url: url.toString() });\n            });\n        }\n    }\n\n    // 清理数据\n    clearData() {\n        chrome.runtime.sendMessage({ Message: \"clearData\", type: true, tabId: this._tabId });\n        chrome.runtime.sendMessage({ Message: \"ClearIcon\", type: true, tabId: this._tabId });\n        this.originalItems = [];\n        document.querySelector('#extensionFilters').innerHTML = '';\n        this.updateFileList();\n    }\n\n    // 脚本\n    srciptList() {\n        document.querySelectorAll(\"[type='script']\").forEach((script) => {\n            script.addEventListener('click', (e) => {\n                chrome.runtime.sendMessage({ Message: \"script\", tabId: this._tabId, script: e.target.id + \".js\" }, () => {\n                    G.autoClearMode > 0 && this.clearData();\n                    this.updateSrciptButton();\n                });\n            });\n        });\n    }\n\n    // 更新脚本按钮状态\n    updateSrciptButton() {\n        chrome.runtime.sendMessage({ Message: \"getButtonState\", tabId: this._tabId }, (state) => {\n            Object.entries(state).forEach(([key, value]) => {\n                const element = document.getElementById(key);\n                if (!element) return;\n                const script = G.scriptList.get(`${key}.js`);\n                if (script) {\n                    element.textContent = value ? script.off : script.name;\n                }\n            });\n        });\n\n        document.querySelector('#defaultPopup').checked = G.popup;\n    }\n\n    // 版本检测\n    checkVersion() {\n        if (G.version < 102 || (G.isFirefox && G.version < 128)) {\n            document.querySelectorAll(\"[type='script']\").forEach(script => script.style.display = 'none');\n        }\n    }\n}\n\nawaitG(() => {\n    loadCSS();\n\n    // 实例化 FilePreview\n    const filePreview = new FilePreview();\n\n    // 监听新数据\n    chrome.runtime.onMessage.addListener(function (Message, sender, sendResponse) {\n        if (!Message.Message || !Message.data || !filePreview || Message.data.tabId != filePreview._tabId) { return; }\n        // 添加资源\n        if (Message.Message == \"popupAddData\") {\n            filePreview.push(Message.data);\n            return;\n        }\n    });\n});"
  },
  {
    "path": "js/pupup-utils.js",
    "content": "// 复制选项\nfunction copyLink(data) {\n    let text = data.url;\n    if (data.parsing == \"m3u8\") {\n        text = G.copyM3U8;\n    } else if (data.parsing == \"mpd\") {\n        text = G.copyMPD;\n    } else {\n        text = G.copyOther;\n    }\n    return templates(text, data);\n}\nfunction isM3U8(data) {\n    return (\n        data.ext == \"m3u8\" ||\n        data.ext == \"m3u\" ||\n        data.type?.endsWith(\"/vnd.apple.mpegurl\") ||\n        data.type?.endsWith(\"/x-mpegurl\") ||\n        data.type?.endsWith(\"/mpegurl\") ||\n        data.type?.endsWith(\"/octet-stream-m3u8\")\n    )\n}\nfunction isMPD(data) {\n    return (data.ext == \"mpd\" ||\n        data.type == \"application/dash+xml\"\n    )\n}\nfunction isJSON(data) {\n    return (data.ext == \"json\" ||\n        data.type == \"application/json\" ||\n        data.type == \"text/json\"\n    )\n}\nfunction isPicture(data) {\n    return (data.type?.startsWith(\"image/\") ||\n        data.ext == \"jpg\" ||\n        data.ext == \"png\" ||\n        data.ext == \"jpeg\" ||\n        data.ext == \"bmp\" ||\n        data.ext == \"gif\" ||\n        data.ext == \"webp\" ||\n        data.ext == \"svg\"\n    )\n}\nfunction isMediaExt(ext) {\n    return ['ogg', 'ogv', 'mp4', 'webm', 'mp3', 'wav', 'm4a', '3gp', 'mpeg', 'mov', 'm4s', 'aac'].includes(ext);\n}\nfunction isMedia(data) {\n    return isMediaExt(data.ext) || data.type?.startsWith(\"video/\") || data.type?.startsWith(\"audio/\");\n}\n/**\n * ari2a RPC发送一套资源\n * @param {object} data 资源对象\n * @param {Function} success 成功运行函数\n * @param {Function} error 失败运行函数\n */\nfunction aria2AddUri(data, success, error) {\n    const json = {\n        \"jsonrpc\": \"2.0\",\n        \"id\": \"cat-catch-\" + data.requestId,\n        \"method\": \"aria2.addUri\",\n        \"params\": []\n    };\n    if (G.aria2RpcToken) {\n        json.params.push(`token:${G.aria2RpcToken}`);\n    }\n    const params = { out: data.downFileName };\n    if (G.enableAria2RpcReferer) {\n        params.header = [];\n        params.header.push(\"User-Agent: \" + (G.userAgent ? G.userAgent : navigator.userAgent));\n        if (data.requestHeaders?.referer) {\n            params.header.push(\"Referer: \" + data.requestHeaders.referer);\n        }\n        if (data.cookie) {\n            params.header.push(\"Cookie: \" + data.cookie);\n        }\n        if (data.requestHeaders?.authorization) {\n            params.header.push(\"Authorization: \" + data.requestHeaders.authorization);\n        }\n    }\n    json.params.push([data.url], params);\n    fetch(G.aria2Rpc, {\n        method: \"POST\",\n        headers: {\n            \"Content-Type\": \"application/json; charset=utf-8\"\n        },\n        body: JSON.stringify(json)\n    }).then(response => {\n        return response.json();\n    }).then(data => {\n        success && success(data);\n    }).catch(errMsg => {\n        error && error(errMsg);\n    });\n}\n\n// MQTT 相关功能\n/**\n * 发送数据到 MQTT 服务器\n * @param {Object} data - 要发送的媒体数据\n * @returns {Promise} - 返回发送结果的 Promise\n */\nfunction sendToMQTT(data, config) {\n    return new Promise((resolve, reject) => {\n        if (!G.mqttEnable) {\n            reject(\"MQTT is not enabled\");\n            return;\n        }\n\n        // 使用配置的标题长度，如果未设置则默认为100\n        const titleLength = G.mqttTitleLength || 100;\n        data.title = data.title.slice(0, titleLength) || \"\";\n        data.action = \"media_found\";\n        data = trimData(data);\n\n        // 创建 MQTT 连接并发送数据\n        connectAndSendMQTT(data, config)\n            .then(() => {\n                resolve(true);\n            })\n            .catch((error) => {\n                console.error(\"MQTT send error:\", error);\n                reject(\"MQTT send failed: \" + error.message);\n            });\n    });\n}\n\n/**\n * 连接到 MQTT 服务器并发送消息\n * @param {Object} data - 要发送的数据\n * @returns {Promise} - 连接和发送的 Promise\n */\nfunction connectAndSendMQTT(data, config) {\n    return new Promise((resolve, reject) => {\n        try {\n            // 构建 MQTT 连接 URL\n            const protocol = G.mqttProtocol;\n            const broker = G.mqttBroker;\n            const port = G.mqttPort;\n            const path = G.mqttPath;\n\n            if (!protocol || !broker || !port || !path) {\n                throw new Error(\"MQTT connection parameters are missing\");\n            }\n\n            const mqttUrl = `${protocol}://${broker}:${port}${path}`;\n\n            // 创建 MQTT 客户端选项\n            const options = {\n                clientId: `${G.mqttClientId || \"cat-catch-client\"}-${Math.random().toString(16).slice(2)}`,\n                clean: true,\n                connectTimeout: 10000,\n                reconnectPeriod: 0 // 不自动重连，用完即断\n            };\n\n            // 添加用户名和密码（如果有）\n            if (G.mqttUser) {\n                options.username = G.mqttUser;\n            }\n            if (G.mqttPassword) {\n                options.password = G.mqttPassword;\n            }\n\n            const mqttLib = window.mqtt || (typeof mqtt !== 'undefined' ? mqtt : null);\n            if (!mqttLib) {\n                throw new Error(\"MQTT library not found. Please check if lib/mqtt.min.js is loaded correctly.\");\n            }\n            if (!mqttLib.connect) {\n                throw new Error(\"MQTT.connect method not found. Available methods: \" + Object.keys(mqttLib));\n            }\n\n            // 如果提供了提示回调函数，则使用它\n            if (typeof config?.alert === 'function') {\n                Tips = config.alert;\n            }\n            // 2. 创建连接阶段提示：正在连接 MQTT 服务器\n            Tips(i18n.connectingToMQTT || \"Connecting to MQTT server...\", 2000);\n\n            const client = mqttLib.connect(mqttUrl, options);\n            // 连接成功\n            client.on('connect', () => {\n\n                const topic = G.mqttTopic || \"cat-catch/media\";\n                const qos = parseInt(G.mqttQos) || 2;\n\n                // 处理自定义数据格式\n                let message;\n                if (G.mqttDataFormat?.trim()) {\n                    message = templates(G.mqttDataFormat, data);\n                } else {\n                    // 使用默认JSON格式\n                    message = JSON.stringify(data);\n                }\n\n                // 3. 正在发送消息到 MQTT 服务器\n                Tips(i18n.sendingMessageToMQTT || \"Sending message to MQTT server...\", 2000);\n\n                // 发送消息\n                client.publish(topic, message, { qos: qos }, (error) => {\n                    if (error) {\n                        console.error(\"MQTT publish error:\", error);\n                        reject(error);\n                    } else {\n                        resolve();\n                    }\n                });\n            });\n\n            // 连接错误\n            client.on('error', (error) => {\n                console.error(\"MQTT connection error:\", error);\n                reject(error);\n            });\n\n            // 连接超时\n            setTimeout(() => {\n                if (!client.connected) {\n                    client.end();\n                    reject(new Error(\"MQTT connection timeout\"));\n                }\n            }, 6000);\n\n            // client.on('close', () => {\n            //     console.log('MQTT connection closed');\n            // });            \n\n        } catch (error) {\n            console.error(\"MQTT setup error:\", error);\n            reject(error);\n        }\n    });\n}"
  },
  {
    "path": "json.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n  <meta charset=\"UTF-8\" />\n  <title>titleJson</title>\n  <link rel=\"stylesheet\" type=\"text/css\" href=\"css/public.css\" media=\"all\" />\n  <link rel=\"stylesheet\" type=\"text/css\" href=\"css/options.css\" media=\"all\" />\n  <script src=\"lib/jquery.min.js\"></script>\n  <script src=\"js/init.js\"></script>\n  <script src=\"js/firefox.js\"></script>\n  <script src=\"lib/jquery.json-viewer.js\"></script>\n  <script src=\"js/function.js\"></script>\n  <script src=\"js/json.js\"></script>\n</head>\n\n<body>\n  <div class=\"wrapper1024\">\n\n    <section id=\"jsonCustom\" class=\"hide\">\n      <h1 class=\"optionsTitle\" data-i18n=\"titleJson\"></h1>\n      <div class=\"optionBox\">\n        <span class=\"explain\">json</span>\n        <textarea id=\"jsonText\" spellcheck=\"false\" data-type=\"link\" class=\"width100\"></textarea>\n        <div class=\"line\"></div>\n        <span class=\"explain\">json url</span>\n        <input type=\"text\" id=\"jsonUrl\" placeholder=\"jsonURL\" class=\"fullInput\" />\n        <button id=\"format\" type=\"button\" data-i18n=\"jsonFormatter\"></button>\n      </div>\n    </section>\n\n    <section id=\"main\">\n      <h1 class=\"optionsTitle\" data-i18n=\"titleJson\"></h1>\n      <div class=\"optionBox\">\n        <div class=\"block\">\n          <pre id=\"json-renderer\" data-i18n=\"fileLoading\"></pre>\n        </div>\n        <button id=\"collapsed\" data-i18n=\"expandAllNodes\"></button>\n      </div>\n    </section>\n  </div>\n  <script src=\"js/i18n.js\"></script>\n</body>\n\n</html>"
  },
  {
    "path": "justfile",
    "content": "# 构建脚本（crx）\n\n# 设置默认shell为bash\nset shell := [\"bash\", \"-c\"]\n\n# 默认任务：显示帮助\ndefault:\n    @just --list\n\n# 安装依赖\ninstall:\n    npm install\n    npm install -g crx3\n\n# 清理构建目录\nclean:\n    rm -rf build dist web-ext-artifacts\n    rm -f *.crx *.zip private-key.pem\n\n# 验证manifest文件\nvalidate:\n    @echo \"验证 manifest.json...\"\n    @node -e \"const manifest = require('./manifest.json'); console.log('Extension name:', manifest.name); console.log('Version:', manifest.version); if (!manifest.manifest_version || !manifest.name || !manifest.version) { throw new Error('Invalid manifest.json'); }\"\n\n# 准备构建目录\nprepare: validate\n    @echo \"准备构建目录...\"\n    mkdir -p build\n    cp -r ./{catch-script,css,img,js,lib,_locales} build/\n    cp -r ./*.{js,html} build/\n    @echo \"✅ 文件复制完成\"\n\n# 检查图标文件\ncheck-icons:\n    @echo \"检查图标文件...\"\n    @if [ ! -d \"img\" ]; then echo \"❌ img/ 目录不存在\"; exit 1; fi\n    @if [ ! -f \"img/icon.png\" ]; then echo \"❌ 缺少图标: img/icon.png\"; exit 1; fi\n    @if [ ! -f \"img/icon128.png\" ]; then echo \"❌ 缺少图标: img/icon128.png\"; exit 1; fi\n    @echo \"✅ 所有图标文件存在\"\n\n# 生成私钥\ngenerate-key:\n    @echo \"生成私钥...\"\n    @if [ ! -f \"private-key.pem\" ]; then \\\n        openssl genrsa -out private-key.pem 2048; \\\n        echo \"✅ 私钥已生成\"; \\\n    else \\\n        echo \"✅ 私钥已存在\"; \\\n    fi\n\n# 构建ZIP文件\nbuild-zip: prepare check-icons\n    @echo \"构建 ZIP 文件...\"\n    @cd build && \\\n        VERSION=$(node -p \"require('./manifest.json').version\") && \\\n        zip -r \"../cat-catch${VERSION}.zip\" . && \\\n        echo \"✅ ZIP 文件已生成: cat-catch${VERSION}.zip\"\n\n# 构建CRX文件\nbuild-crx: prepare check-icons generate-key\n    @echo \"构建 CRX 文件...\"\n    @VERSION=$(node -p \"require('./manifest.json').version\") && \\\n    crx3 -p private-key.pem -o \"cat-catch${VERSION}.crx\" build/ && \\\n    echo \"✅ CRX 文件已生成: cat-catch${VERSION}.crx\"\n\n# 快速构建（仅ZIP）\nquick: build-zip\n    @echo \"🚀 快速构建完成！\"\n\n# 完整构建（CRX + ZIP）\nbuild: build-crx build-zip\n    @echo \"🎉 构建完成！\"\n    @ls -la *.crx *.zip 2>/dev/null || true\n\n# 开发模式 - 自动重载\ndev-watch: prepare\n    @echo \"🔄 开发模式 - 自动构建\"\n    @echo \"================================\"\n    @echo \"监听文件变化并自动重新构建到 build/ 目录\"\n    @echo \"请在Chrome中加载 build/ 目录，然后刷新扩展\"\n    @echo \"\"\n    @if command -v inotifywait >/dev/null 2>&1; then \\\n        while true; do \\\n            inotifywait -r -e modify,create,delete src/ && \\\n            echo \"🔄 检测到文件变化，重新构建...\" && \\\n            just prepare; \\\n        done \\\n    else \\\n        echo \"❌ 需要安装 inotify-tools: sudo apt install inotify-tools\"; \\\n    fi\n\n# 检查扩展\nlint:\n    @echo \"检查扩展...\"\n    @echo \"验证 manifest.json 格式...\"\n    @node -e \"const manifest = require('./manifest.json'); console.log('✅ Manifest 格式正确'); console.log('扩展名:', manifest.name); console.log('版本:', manifest.version);\"\n    @echo \"检查必需文件...\"\n    @if [ -f \"popup.html\" ]; then echo \"✅ popup.html 存在\"; else echo \"❌ popup.html 缺失\"; fi\n    @if [ -f \"options.html\" ]; then echo \"✅ options.html 存在\"; else echo \"❌ options.html 缺失\"; fi\n    @if [ -f \"js/background.js\" ]; then echo \"✅ background.js 存在\"; else echo \"❌ background.js 缺失\"; fi\n    @if [ -f \"js/content-script.js\" ]; then echo \"✅ content.js 存在\"; else echo \"❌ content.js 缺失\"; fi\n    @if [ -f \"js/popup.js\" ]; then echo \"✅ popup.js 存在\"; else echo \"❌ popup.js 缺失\"; fi\n    @if [ -f \"js/options.js\" ]; then echo \"✅ options.js 存在\"; else echo \"❌ options.js 缺失\"; fi\n    @echo \"✅ Chrome扩展检查完成\"\n\n# 显示版本信息\nversion:\n    @node -p \"'当前版本: ' + require('./manifest.json').version\"\n\n# 显示项目状态\nstatus:\n    @echo \"📊 项目状态：\"\n    @node -p \"'版本: ' + require('./manifest.json').version\"\n    @echo \"图标状态:\"\n    @if [ -f \"img/icon.png\" ]; then echo \"  ✅ img/icon.png\"; else echo \"  ❌ img/icon.png\"; fi\n    @if [ -f \"img/icon128.png\" ]; then echo \"  ✅ img/icon128.png\"; else echo \"  ❌ img/icon128.png\"; fi\n    @echo \"构建文件:\"\n    @ls -la *.crx *.zip 2>/dev/null || echo \"  无构建文件\"\n\n# 完整的发布流程\nrelease: clean install validate build\n    @echo \"🎉 发布包已准备完成！\"\n    @echo \"文件列表：\"\n    @ls -la *.crx *.zip\n"
  },
  {
    "path": "lib/StreamSaver.js",
    "content": "/*! streamsaver. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */\n\n/* global chrome location ReadableStream define MessageChannel TransformStream */\n\n;((name, definition) => {\n  typeof module !== 'undefined'\n    ? module.exports = definition()\n    : typeof define === 'function' && typeof define.amd === 'object'\n      ? define(definition)\n      : this[name] = definition()\n})('streamSaver', () => {\n  'use strict'\n\n  const global = typeof window === 'object' ? window : this\n  if (!global.HTMLElement) console.warn('streamsaver is meant to run on browsers main thread')\n\n  let mitmTransporter = null\n  let supportsTransferable = false\n  const test = fn => { try { fn() } catch (e) {} }\n  const ponyfill = global.WebStreamsPolyfill || {}\n  const isSecureContext = global.isSecureContext\n  // TODO: Must come up with a real detection test (#69)\n  let useBlobFallback = /constructor/i.test(global.HTMLElement) || !!global.safari || !!global.WebKitPoint\n  const downloadStrategy = isSecureContext || 'MozAppearance' in document.documentElement.style\n    ? 'iframe'\n    : 'navigate'\n\n  const streamSaver = {\n    createWriteStream,\n    WritableStream: global.WritableStream || ponyfill.WritableStream,\n    supported: true,\n    version: { full: '2.0.5', major: 2, minor: 0, dot: 5 },\n    mitm: 'https://jimmywarting.github.io/StreamSaver.js/mitm.html?version=2.0.0'\n  }\n\n  /**\n   * create a hidden iframe and append it to the DOM (body)\n   *\n   * @param  {string} src page to load\n   * @return {HTMLIFrameElement} page to load\n   */\n  function makeIframe (src) {\n    if (!src) throw new Error('meh')\n    const iframe = document.createElement('iframe')\n    iframe.hidden = true\n    iframe.src = src\n    iframe.loaded = false\n    iframe.name = 'iframe'\n    iframe.isIframe = true\n    iframe.postMessage = (...args) => iframe.contentWindow.postMessage(...args)\n    iframe.addEventListener('load', () => {\n      iframe.loaded = true\n    }, { once: true })\n    document.body.appendChild(iframe)\n    return iframe\n  }\n\n  /**\n   * create a popup that simulates the basic things\n   * of what a iframe can do\n   *\n   * @param  {string} src page to load\n   * @return {object}     iframe like object\n   */\n  function makePopup (src) {\n    const options = 'width=200,height=100'\n    const delegate = document.createDocumentFragment()\n    const popup = {\n      frame: global.open(src, 'popup', options),\n      loaded: false,\n      isIframe: false,\n      isPopup: true,\n      remove () { popup.frame.close() },\n      addEventListener (...args) { delegate.addEventListener(...args) },\n      dispatchEvent (...args) { delegate.dispatchEvent(...args) },\n      removeEventListener (...args) { delegate.removeEventListener(...args) },\n      postMessage (...args) { popup.frame.postMessage(...args) }\n    }\n\n    const onReady = evt => {\n      if (evt.source === popup.frame) {\n        popup.loaded = true\n        global.removeEventListener('message', onReady)\n        popup.dispatchEvent(new Event('load'))\n      }\n    }\n\n    global.addEventListener('message', onReady)\n\n    return popup\n  }\n\n  try {\n    // We can't look for service worker since it may still work on http\n    new Response(new ReadableStream())\n    if (isSecureContext && !('serviceWorker' in navigator)) {\n      useBlobFallback = true\n    }\n  } catch (err) {\n    useBlobFallback = true\n  }\n\n  test(() => {\n    // Transferable stream was first enabled in chrome v73 behind a flag\n    const { readable } = new TransformStream()\n    const mc = new MessageChannel()\n    mc.port1.postMessage(readable, [readable])\n    mc.port1.close()\n    mc.port2.close()\n    supportsTransferable = true\n    // Freeze TransformStream object (can only work with native)\n    Object.defineProperty(streamSaver, 'TransformStream', {\n      configurable: false,\n      writable: false,\n      value: TransformStream\n    })\n  })\n\n  function loadTransporter () {\n    if (!mitmTransporter) {\n      mitmTransporter = isSecureContext\n        ? makeIframe(streamSaver.mitm)\n        : makePopup(streamSaver.mitm)\n    }\n  }\n\n  /**\n   * @param  {string} filename filename that should be used\n   * @param  {object} options  [description]\n   * @param  {number} size     deprecated\n   * @return {WritableStream<Uint8Array>}\n   */\n  function createWriteStream (filename, options, size) {\n    let opts = {\n      size: null,\n      pathname: null,\n      writableStrategy: undefined,\n      readableStrategy: undefined\n    }\n\n    let bytesWritten = 0 // by StreamSaver.js (not the service worker)\n    let downloadUrl = null\n    let channel = null\n    let ts = null\n\n    // normalize arguments\n    if (Number.isFinite(options)) {\n      [ size, options ] = [ options, size ]\n      console.warn('[StreamSaver] Deprecated pass an object as 2nd argument when creating a write stream')\n      opts.size = size\n      opts.writableStrategy = options\n    } else if (options && options.highWaterMark) {\n      console.warn('[StreamSaver] Deprecated pass an object as 2nd argument when creating a write stream')\n      opts.size = size\n      opts.writableStrategy = options\n    } else {\n      opts = options || {}\n    }\n    if (!useBlobFallback) {\n      loadTransporter()\n\n      channel = new MessageChannel()\n\n      // Make filename RFC5987 compatible\n      filename = encodeURIComponent(filename.replace(/\\//g, ':'))\n        .replace(/['()]/g, escape)\n        .replace(/\\*/g, '%2A')\n\n      const response = {\n        transferringReadable: supportsTransferable,\n        pathname: opts.pathname || Math.random().toString().slice(-6) + '/' + filename,\n        headers: {\n          'Content-Type': 'application/octet-stream; charset=utf-8',\n          'Content-Disposition': \"attachment; filename*=UTF-8''\" + filename\n        }\n      }\n\n      if (opts.size) {\n        response.headers['Content-Length'] = opts.size\n      }\n\n      const args = [ response, '*', [ channel.port2 ] ]\n\n      if (supportsTransferable) {\n        const transformer = downloadStrategy === 'iframe' ? undefined : {\n          // This transformer & flush method is only used by insecure context.\n          transform (chunk, controller) {\n            if (!(chunk instanceof Uint8Array)) {\n              throw new TypeError('Can only write Uint8Arrays')\n            }\n            bytesWritten += chunk.length\n            controller.enqueue(chunk)\n\n            if (downloadUrl) {\n              location.href = downloadUrl\n              downloadUrl = null\n            }\n          },\n          flush () {\n            if (downloadUrl) {\n              location.href = downloadUrl\n            }\n          }\n        }\n        ts = new streamSaver.TransformStream(\n          transformer,\n          opts.writableStrategy,\n          opts.readableStrategy\n        )\n        const readableStream = ts.readable\n\n        channel.port1.postMessage({ readableStream }, [ readableStream ])\n      }\n\n      channel.port1.onmessage = evt => {\n        // Service worker sent us a link that we should open.\n        if (evt.data.download) {\n          // Special treatment for popup...\n          if (downloadStrategy === 'navigate') {\n            mitmTransporter.remove()\n            mitmTransporter = null\n            if (bytesWritten) {\n              location.href = evt.data.download\n            } else {\n              downloadUrl = evt.data.download\n            }\n          } else {\n            if (mitmTransporter.isPopup) {\n              mitmTransporter.remove()\n              mitmTransporter = null\n              // Special case for firefox, they can keep sw alive with fetch\n              if (downloadStrategy === 'iframe') {\n                makeIframe(streamSaver.mitm)\n              }\n            }\n\n            // We never remove this iframes b/c it can interrupt saving\n            makeIframe(evt.data.download)\n          }\n        } else if (evt.data.abort) {\n          chunks = []\n          channel.port1.postMessage('abort') //send back so controller is aborted\n          channel.port1.onmessage = null\n          channel.port1.close()\n          channel.port2.close()\n          channel = null\n        }\n      }\n\n      if (mitmTransporter.loaded) {\n        mitmTransporter.postMessage(...args)\n      } else {\n        mitmTransporter.addEventListener('load', () => {\n          mitmTransporter.postMessage(...args)\n        }, { once: true })\n      }\n    }\n\n    let chunks = []\n\n    return (!useBlobFallback && ts && ts.writable) || new streamSaver.WritableStream({\n      write (chunk) {\n        if (!(chunk instanceof Uint8Array)) {\n          throw new TypeError('Can only write Uint8Arrays')\n        }\n        if (useBlobFallback) {\n          // Safari... The new IE6\n          // https://github.com/jimmywarting/StreamSaver.js/issues/69\n          //\n          // even though it has everything it fails to download anything\n          // that comes from the service worker..!\n          chunks.push(chunk)\n          return\n        }\n\n        // is called when a new chunk of data is ready to be written\n        // to the underlying sink. It can return a promise to signal\n        // success or failure of the write operation. The stream\n        // implementation guarantees that this method will be called\n        // only after previous writes have succeeded, and never after\n        // close or abort is called.\n\n        // TODO: Kind of important that service worker respond back when\n        // it has been written. Otherwise we can't handle backpressure\n        // EDIT: Transferable streams solves this...\n        channel.port1.postMessage(chunk)\n        bytesWritten += chunk.length\n\n        if (downloadUrl) {\n          location.href = downloadUrl\n          downloadUrl = null\n        }\n      },\n      close () {\n        if (useBlobFallback) {\n          const blob = new Blob(chunks, { type: 'application/octet-stream; charset=utf-8' })\n          const link = document.createElement('a')\n          link.href = URL.createObjectURL(blob)\n          link.download = filename\n          link.click()\n        } else {\n          channel.port1.postMessage('end')\n        }\n      },\n      abort () {\n        chunks = []\n        channel.port1.postMessage('abort')\n        channel.port1.onmessage = null\n        channel.port1.close()\n        channel.port2.close()\n        channel = null\n      }\n    }, opts.writableStrategy)\n  }\n\n  return streamSaver\n})\n"
  },
  {
    "path": "lib/base64.js",
    "content": "class Base64 {\n  /**\n   * 将字符串编码为Base64（支持UTF-8）\n   * @param {string} str - 需要编码的原始字符串\n   * @returns {string} Base64编码结果\n   */\n  static encode(str) {\n    // 使用TextEncoder将字符串转换为UTF-8字节数组\n    const encoder = new TextEncoder();\n    const data = encoder.encode(str);\n\n    // 将字节数组转换为二进制字符串\n    let binary = '';\n    data.forEach(byte => binary += String.fromCharCode(byte));\n\n    // 使用浏览器内置方法进行Base64编码\n    return btoa(binary);\n  }\n\n  /**\n   * 解码Base64字符串为原始字符串（支持UTF-8）\n   * @param {string} base64Str - Base64编码字符串\n   * @returns {string} 解码后的原始字符串\n   */\n  static decode(base64Str) {\n    // 解码Base64得到二进制字符串\n    const binaryStr = atob(base64Str);\n\n    // 将二进制字符串转换为字节数组\n    const bytes = new Uint8Array(binaryStr.length);\n    for (let i = 0; i < binaryStr.length; i++) {\n      bytes[i] = binaryStr.charCodeAt(i);\n    }\n\n    // 使用TextDecoder将字节数组转换为UTF-8字符串\n    const decoder = new TextDecoder();\n    return decoder.decode(bytes);\n  }\n}\nwindow.Base64 = Base64;"
  },
  {
    "path": "lib/jquery.json-viewer.js",
    "content": "/**\n * jQuery json-viewer\n * @author: Alexandre Bodelot <alexandre.bodelot@gmail.com>\n * @link: https://github.com/abodelot/jquery.json-viewer\n */\n(function($) {\n\n  /**\n   * Check if arg is either an array with at least 1 element, or a dict with at least 1 key\n   * @return boolean\n   */\n  function isCollapsable(arg) {\n    return arg instanceof Object && Object.keys(arg).length > 0;\n  }\n\n  /**\n   * Check if a string looks like a URL, based on protocol\n   * This doesn't attempt to validate URLs, there's no use and syntax can be too complex\n   * @return boolean\n   */\n  function isUrl(string) {\n    var protocols = ['http', 'https', 'ftp', 'ftps'];\n    for (var i = 0; i < protocols.length; ++i) {\n      if (string.startsWith(protocols[i] + '://')) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  /**\n   * Return the input string html escaped\n   * @return string\n   */\n  function htmlEscape(s) {\n    return s.replace(/&/g, '&amp;')\n      .replace(/</g, '&lt;')\n      .replace(/>/g, '&gt;')\n      .replace(/'/g, '&apos;')\n      .replace(/\"/g, '&quot;');\n  }\n\n  /**\n   * Transform a json object into html representation\n   * @return string\n   */\n  function json2html(json, options) {\n    var html = '';\n    if (typeof json === 'string') {\n      // Escape tags and quotes\n      json = htmlEscape(json);\n\n      if (options.withLinks && isUrl(json)) {\n        html += '<a href=\"' + json + '\" class=\"json-string\" target=\"_blank\">' + json + '</a>';\n      } else {\n        // Escape double quotes in the rendered non-URL string.\n        json = json.replace(/&quot;/g, '\\\\&quot;');\n        html += '<span class=\"json-string\">\"' + json + '\"</span>';\n      }\n    } else if (typeof json === 'number' || typeof json === 'bigint') {\n      html += '<span class=\"json-literal\">' + json + '</span>';\n    } else if (typeof json === 'boolean') {\n      html += '<span class=\"json-literal\">' + json + '</span>';\n    } else if (json === null) {\n      html += '<span class=\"json-literal\">null</span>';\n    } else if (json instanceof Array) {\n      if (json.length > 0) {\n        html += '[<ol class=\"json-array\">';\n        for (var i = 0; i < json.length; ++i) {\n          html += '<li>';\n          // Add toggle button if item is collapsable\n          if (isCollapsable(json[i])) {\n            html += '<a href class=\"json-toggle\"></a>';\n          }\n          html += json2html(json[i], options);\n          // Add comma if item is not last\n          if (i < json.length - 1) {\n            html += ',';\n          }\n          html += '</li>';\n        }\n        html += '</ol>]';\n      } else {\n        html += '[]';\n      }\n    } else if (typeof json === 'object') {\n      // Optional support different libraries for big numbers\n      // json.isLosslessNumber: package lossless-json\n      // json.toExponential(): packages bignumber.js, big.js, decimal.js, decimal.js-light, others?\n      if (options.bigNumbers && (typeof json.toExponential === 'function' || json.isLosslessNumber)) {\n        html += '<span class=\"json-literal\">' + json.toString() + '</span>';\n      } else {\n        var keyCount = Object.keys(json).length;\n        if (keyCount > 0) {\n          html += '{<ul class=\"json-dict\">';\n          for (var key in json) {\n            if (Object.prototype.hasOwnProperty.call(json, key)) {\n              key = htmlEscape(key);\n              var keyRepr = options.withQuotes ?\n                '<span class=\"json-string\">\"' + key + '\"</span>' : key;\n\n              html += '<li>';\n              // Add toggle button if item is collapsable\n              if (isCollapsable(json[key])) {\n                html += '<a href class=\"json-toggle\">' + keyRepr + '</a>';\n              } else {\n                html += keyRepr;\n              }\n              html += ': ' + json2html(json[key], options);\n              // Add comma if item is not last\n              if (--keyCount > 0) {\n                html += ',';\n              }\n              html += '</li>';\n            }\n          }\n          html += '</ul>}';\n        } else {\n          html += '{}';\n        }\n      }\n    }\n    return html;\n  }\n\n  /**\n   * jQuery plugin method\n   * @param json: a javascript object\n   * @param options: an optional options hash\n   */\n  $.fn.jsonViewer = function(json, options) {\n    // Merge user options with default options\n    options = Object.assign({}, {\n      collapsed: false,\n      rootCollapsable: true,\n      withQuotes: false,\n      withLinks: true,\n      bigNumbers: false\n    }, options);\n\n    // jQuery chaining\n    return this.each(function() {\n\n      // Transform to HTML\n      var html = json2html(json, options);\n      if (options.rootCollapsable && isCollapsable(json)) {\n        html = '<a href class=\"json-toggle\"></a>' + html;\n      }\n\n      // Insert HTML in target DOM element\n      $(this).html(html);\n      $(this).addClass('json-document');\n\n      // Bind click on toggle buttons\n      $(this).off('click');\n      $(this).on('click', 'a.json-toggle', function() {\n        var target = $(this).toggleClass('collapsed').siblings('ul.json-dict, ol.json-array');\n        target.toggle();\n        if (target.is(':visible')) {\n          target.siblings('.json-placeholder').remove();\n        } else {\n          var count = target.children('li').length;\n          var placeholder = count + (count > 1 ? ' items' : ' item');\n          target.after('<a href class=\"json-placeholder\">' + placeholder + '</a>');\n        }\n        return false;\n      });\n\n      // Simulate click on toggle button when placeholder is clicked\n      $(this).on('click', 'a.json-placeholder', function() {\n        $(this).siblings('a.json-toggle').click();\n        return false;\n      });\n\n      if (options.collapsed == true) {\n        // Trigger click to collapse all nodes\n        $(this).find('a.json-toggle').click();\n      }\n    });\n  };\n})(jQuery);\n"
  },
  {
    "path": "lib/m3u8-decrypt.js",
    "content": "class AESDecryptor {\n  constructor() {\n    this.rcon = [0x0, 0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36];\n    this.subMix = [\n      new Uint32Array(256),\n      new Uint32Array(256),\n      new Uint32Array(256),\n      new Uint32Array(256),\n    ];\n    this.invSubMix = [\n      new Uint32Array(256),\n      new Uint32Array(256),\n      new Uint32Array(256),\n      new Uint32Array(256),\n    ];\n    this.sBox = new Uint32Array(256);\n    this.invSBox = new Uint32Array(256);\n    this.key = new Uint32Array(0);\n    this.ksRows = 0;\n    this.keySize = 0;\n    this.initTable();\n  }\n  removePadding(array) {\n    const outputBytes = array.byteLength;\n    const paddingBytes =\n      outputBytes && new DataView(array).getUint8(outputBytes - 1);\n    if (paddingBytes) {\n      return array.slice(0, outputBytes - paddingBytes);\n    }\n    return array;\n  }\n  // Using view.getUint32() also swaps the byte order.\n  uint8ArrayToUint32Array_(arrayBuffer) {\n    const view = new DataView(arrayBuffer);\n    const newArray = new Uint32Array(4);\n    for (let i = 0; i < 4; i++) {\n      newArray[i] = view.getUint32(i * 4);\n    }\n    return newArray;\n  }\n  initTable() {\n    const sBox = this.sBox;\n    const invSBox = this.invSBox;\n    const subMix = this.subMix;\n    const subMix0 = subMix[0];\n    const subMix1 = subMix[1];\n    const subMix2 = subMix[2];\n    const subMix3 = subMix[3];\n    const invSubMix = this.invSubMix;\n    const invSubMix0 = invSubMix[0];\n    const invSubMix1 = invSubMix[1];\n    const invSubMix2 = invSubMix[2];\n    const invSubMix3 = invSubMix[3];\n    const d = new Uint32Array(256);\n    let x = 0;\n    let xi = 0;\n    let i = 0;\n    for (i = 0; i < 256; i++) {\n      if (i < 128) {\n        d[i] = i << 1;\n      } else {\n        d[i] = (i << 1) ^ 0x11b;\n      }\n    }\n    for (i = 0; i < 256; i++) {\n      let sx = xi ^ (xi << 1) ^ (xi << 2) ^ (xi << 3) ^ (xi << 4);\n      sx = (sx >>> 8) ^ (sx & 0xff) ^ 0x63;\n      sBox[x] = sx;\n      invSBox[sx] = x;\n      // Compute multiplication\n      const x2 = d[x];\n      const x4 = d[x2];\n      const x8 = d[x4];\n      // Compute sub/invSub bytes, mix columns tables\n      let t = (d[sx] * 0x101) ^ (sx * 0x1010100);\n      subMix0[x] = (t << 24) | (t >>> 8);\n      subMix1[x] = (t << 16) | (t >>> 16);\n      subMix2[x] = (t << 8) | (t >>> 24);\n      subMix3[x] = t;\n      // Compute inv sub bytes, inv mix columns tables\n      t = (x8 * 0x1010101) ^ (x4 * 0x10001) ^ (x2 * 0x101) ^ (x * 0x1010100);\n      invSubMix0[sx] = (t << 24) | (t >>> 8);\n      invSubMix1[sx] = (t << 16) | (t >>> 16);\n      invSubMix2[sx] = (t << 8) | (t >>> 24);\n      invSubMix3[sx] = t;\n      // Compute next counter\n      if (!x) {\n        x = xi = 1;\n      } else {\n        x = x2 ^ d[d[d[x8 ^ x2]]];\n        xi ^= d[d[xi]];\n      }\n    }\n  }\n  expandKey(keyBuffer) {\n    // convert keyBuffer to Uint32Array\n    const key = this.uint8ArrayToUint32Array_(keyBuffer);\n    let sameKey = true;\n    let offset = 0;\n    while (offset < key.length && sameKey) {\n      sameKey = key[offset] === this.key[offset];\n      offset++;\n    }\n    if (sameKey) {\n      return;\n    }\n    this.key = key;\n    const keySize = (this.keySize = key.length);\n    if (keySize !== 4 && keySize !== 6 && keySize !== 8) {\n      throw new Error(\"Invalid aes key size=\" + keySize);\n    }\n    const ksRows = (this.ksRows = (keySize + 6 + 1) * 4);\n    let ksRow;\n    let invKsRow;\n    const keySchedule = (this.keySchedule = new Uint32Array(ksRows));\n    const invKeySchedule = (this.invKeySchedule = new Uint32Array(ksRows));\n    const sbox = this.sBox;\n    const rcon = this.rcon;\n    const invSubMix = this.invSubMix;\n    const invSubMix0 = invSubMix[0];\n    const invSubMix1 = invSubMix[1];\n    const invSubMix2 = invSubMix[2];\n    const invSubMix3 = invSubMix[3];\n    let prev;\n    let t;\n    for (ksRow = 0; ksRow < ksRows; ksRow++) {\n      if (ksRow < keySize) {\n        prev = keySchedule[ksRow] = key[ksRow];\n        continue;\n      }\n      t = prev;\n      if (ksRow % keySize === 0) {\n        // Rot word\n        t = (t << 8) | (t >>> 24);\n        // Sub word\n        t =\n          (sbox[t >>> 24] << 24) |\n          (sbox[(t >>> 16) & 0xff] << 16) |\n          (sbox[(t >>> 8) & 0xff] << 8) |\n          sbox[t & 0xff];\n        // Mix Rcon\n        t ^= rcon[(ksRow / keySize) | 0] << 24;\n      } else if (keySize > 6 && ksRow % keySize === 4) {\n        // Sub word\n        t =\n          (sbox[t >>> 24] << 24) |\n          (sbox[(t >>> 16) & 0xff] << 16) |\n          (sbox[(t >>> 8) & 0xff] << 8) |\n          sbox[t & 0xff];\n      }\n      keySchedule[ksRow] = prev = (keySchedule[ksRow - keySize] ^ t) >>> 0;\n    }\n    for (invKsRow = 0; invKsRow < ksRows; invKsRow++) {\n      ksRow = ksRows - invKsRow;\n      if (invKsRow & 3) {\n        t = keySchedule[ksRow];\n      } else {\n        t = keySchedule[ksRow - 4];\n      }\n      if (invKsRow < 4 || ksRow <= 4) {\n        invKeySchedule[invKsRow] = t;\n      } else {\n        invKeySchedule[invKsRow] =\n          invSubMix0[sbox[t >>> 24]] ^\n          invSubMix1[sbox[(t >>> 16) & 0xff]] ^\n          invSubMix2[sbox[(t >>> 8) & 0xff]] ^\n          invSubMix3[sbox[t & 0xff]];\n      }\n      invKeySchedule[invKsRow] = invKeySchedule[invKsRow] >>> 0;\n    }\n  }\n  // Adding this as a method greatly improves performance.\n  networkToHostOrderSwap(word) {\n    return (\n      (word << 24) |\n      ((word & 0xff00) << 8) |\n      ((word & 0xff0000) >> 8) |\n      (word >>> 24)\n    );\n  }\n  decrypt(inputArrayBuffer, offset, aesIV, removePKCS7Padding) {\n    const nRounds = this.keySize + 6;\n    const invKeySchedule = this.invKeySchedule;\n    const invSBOX = this.invSBox;\n    const invSubMix = this.invSubMix;\n    const invSubMix0 = invSubMix[0];\n    const invSubMix1 = invSubMix[1];\n    const invSubMix2 = invSubMix[2];\n    const invSubMix3 = invSubMix[3];\n    const initVector = this.uint8ArrayToUint32Array_(aesIV);\n    let initVector0 = initVector[0];\n    let initVector1 = initVector[1];\n    let initVector2 = initVector[2];\n    let initVector3 = initVector[3];\n    const inputInt32 = new Int32Array(inputArrayBuffer);\n    const outputInt32 = new Int32Array(inputInt32.length);\n    let t0, t1, t2, t3;\n    let s0, s1, s2, s3;\n    let inputWords0, inputWords1, inputWords2, inputWords3;\n    let ksRow, i;\n    const swapWord = this.networkToHostOrderSwap;\n    while (offset < inputInt32.length) {\n      inputWords0 = swapWord(inputInt32[offset]);\n      inputWords1 = swapWord(inputInt32[offset + 1]);\n      inputWords2 = swapWord(inputInt32[offset + 2]);\n      inputWords3 = swapWord(inputInt32[offset + 3]);\n      s0 = inputWords0 ^ invKeySchedule[0];\n      s1 = inputWords3 ^ invKeySchedule[1];\n      s2 = inputWords2 ^ invKeySchedule[2];\n      s3 = inputWords1 ^ invKeySchedule[3];\n      ksRow = 4;\n      // Iterate through the rounds of decryption\n      for (i = 1; i < nRounds; i++) {\n        t0 =\n          invSubMix0[s0 >>> 24] ^\n          invSubMix1[(s1 >> 16) & 0xff] ^\n          invSubMix2[(s2 >> 8) & 0xff] ^\n          invSubMix3[s3 & 0xff] ^\n          invKeySchedule[ksRow];\n        t1 =\n          invSubMix0[s1 >>> 24] ^\n          invSubMix1[(s2 >> 16) & 0xff] ^\n          invSubMix2[(s3 >> 8) & 0xff] ^\n          invSubMix3[s0 & 0xff] ^\n          invKeySchedule[ksRow + 1];\n        t2 =\n          invSubMix0[s2 >>> 24] ^\n          invSubMix1[(s3 >> 16) & 0xff] ^\n          invSubMix2[(s0 >> 8) & 0xff] ^\n          invSubMix3[s1 & 0xff] ^\n          invKeySchedule[ksRow + 2];\n        t3 =\n          invSubMix0[s3 >>> 24] ^\n          invSubMix1[(s0 >> 16) & 0xff] ^\n          invSubMix2[(s1 >> 8) & 0xff] ^\n          invSubMix3[s2 & 0xff] ^\n          invKeySchedule[ksRow + 3];\n        // Update state\n        s0 = t0;\n        s1 = t1;\n        s2 = t2;\n        s3 = t3;\n        ksRow = ksRow + 4;\n      }\n      // Shift rows, sub bytes, add round key\n      t0 =\n        (invSBOX[s0 >>> 24] << 24) ^\n        (invSBOX[(s1 >> 16) & 0xff] << 16) ^\n        (invSBOX[(s2 >> 8) & 0xff] << 8) ^\n        invSBOX[s3 & 0xff] ^\n        invKeySchedule[ksRow];\n      t1 =\n        (invSBOX[s1 >>> 24] << 24) ^\n        (invSBOX[(s2 >> 16) & 0xff] << 16) ^\n        (invSBOX[(s3 >> 8) & 0xff] << 8) ^\n        invSBOX[s0 & 0xff] ^\n        invKeySchedule[ksRow + 1];\n      t2 =\n        (invSBOX[s2 >>> 24] << 24) ^\n        (invSBOX[(s3 >> 16) & 0xff] << 16) ^\n        (invSBOX[(s0 >> 8) & 0xff] << 8) ^\n        invSBOX[s1 & 0xff] ^\n        invKeySchedule[ksRow + 2];\n      t3 =\n        (invSBOX[s3 >>> 24] << 24) ^\n        (invSBOX[(s0 >> 16) & 0xff] << 16) ^\n        (invSBOX[(s1 >> 8) & 0xff] << 8) ^\n        invSBOX[s2 & 0xff] ^\n        invKeySchedule[ksRow + 3];\n      // Write\n      outputInt32[offset] = swapWord(t0 ^ initVector0);\n      outputInt32[offset + 1] = swapWord(t3 ^ initVector1);\n      outputInt32[offset + 2] = swapWord(t2 ^ initVector2);\n      outputInt32[offset + 3] = swapWord(t1 ^ initVector3);\n      // reset initVector to last 4 unsigned int\n      initVector0 = inputWords0;\n      initVector1 = inputWords1;\n      initVector2 = inputWords2;\n      initVector3 = inputWords3;\n      offset = offset + 4;\n    }\n    return removePKCS7Padding\n      ? this.removePadding(outputInt32.buffer)\n      : outputInt32.buffer;\n  }\n  destroy() {\n    this.key = undefined;\n    this.keySize = undefined;\n    this.ksRows = undefined;\n    this.sBox = undefined;\n    this.invSBox = undefined;\n    this.subMix = undefined;\n    this.invSubMix = undefined;\n    this.keySchedule = undefined;\n    this.invKeySchedule = undefined;\n    this.rcon = undefined;\n  }\n}\n"
  },
  {
    "path": "lib/third-party-libraries.md",
    "content": "lib/mux.min.js\nSource: https://github.com/videojs/mux.js\nLicense: Apache-2.0 license\nVersion: 7.1.0\nBuild:\n`https://github.com/videojs/mux.js/releases/tag/v7.1.0`\n`Source code` -> `npm run build` -> `dist/mux.min.js`\n\nlib/mpd-parser.min.js\nSource: https://github.com/videojs/mpd-parser\nLicense: Apache-2.0 license\nVersion: 1.3.1\nDownload:\n`https://github.com/videojs/mpd-parser/releases/tag/v1.3.1` -> `mpd-parser.min.js`\n\nlib/jquery.qrcode.min.js\nSource: https://github.com/jeromeetienne/jquery-qrcode\nLicense: MIT license\nVersion: 1.0\nDownload:\nhttps://github.com/jeromeetienne/jquery-qrcode/releases/tag/v1.0\n`Source code` -> `jquery.qrcode.min.js`\n\nlib/jquery.min.js\nSource: https://github.com/jquery/jquery\nLicense: MIT license\nVersion: 3.7.1\nDownload:\n`https://jquery.com/download/` -> `https://code.jquery.com/jquery-3.7.1.min.js`\n\nlib/hls.min.js\nSource: https://github.com/video-dev/hls.js\nLicense: Apache-2.0 license\nVersion: 1.6.15\nDownload:\n`https://github.com/video-dev/hls.js/releases/tag/v1.6.15` -> `release.zip` -> `dist/hls.min.js`\n\nlib/mqtt.min.js\nSource: https://github.com/mqttjs/MQTT.js\nLicense: MIT license\nVersion: 5.14.1\nBuild:\n`https://github.com/mqttjs/MQTT.js/releases/tag/v5.14.1`\n`Source code` -> `npm run build` -> `dist/mqtt.min.js`\n\nlib/jquery.json-viewer.js\nSource: https://github.com/abodelot/jquery.json-viewer\nLicense: MIT license\nVersion: 1.5.0\nDownload:\n`https://github.com/abodelot/jquery.json-viewer/releases/tag/v1.5.0`\n`Source code` -> `json-viewer/jquery.json-viewer.js`\n\nlib/StreamSaver.js\nSource: https://github.com/jimmywarting/StreamSaver.js\nLicense: MIT license\nVersion: 2.0.6\nDownload:\n`https://github.com/jimmywarting/StreamSaver.js/releases/tag/2.0.6`\n`Source code` -> `StreamSaver.js`\n\nlib/m3u8-decrypt.js\nSource: https://github.com/video-dev/hls.js/blob/master/src/crypt/aes-decryptor.ts\nLicense: Apache-2.0 license\n"
  },
  {
    "path": "m3u8.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n  <meta charset=\"UTF-8\" />\n  <title>titleM3U8</title>\n  <link rel=\"stylesheet\" type=\"text/css\" href=\"css/public.css\" media=\"all\" />\n  <link rel=\"stylesheet\" type=\"text/css\" href=\"css/options.css\" media=\"all\" />\n  <script src=\"lib/jquery.min.js\"></script>\n  <script src=\"js/init.js\"></script>\n  <script src=\"js/firefox.js\"></script>\n  <script src=\"lib/mux.min.js\"></script>\n  <script src=\"js/function.js\"></script>\n</head>\n\n<body>\n  <div class=\"m3u8_wrapper wrapper1080\">\n\n    <section id=\"loading\">\n      <div class=\"optionBox\" data-i18n=\"loading\"></div>\n    </section>\n\n    <section id=\"m3u8Custom\" class=\"hide\">\n      <h1 class=\"optionsTitle\" data-i18n=\"titleM3U8\"></h1>\n      <div class=\"optionBox\">\n        <textarea id=\"m3u8Text\" spellcheck=\"false\" data-type=\"link\" data-i18n-placeholder=\"m3u8Placeholder\"\n          class=\"width100\"></textarea>\n        <input type=\"text\" id=\"baseUrl\" placeholder=\"BaseURL\" class=\"fullInput\" />\n        <div class=\"line\"></div>\n        <input type=\"text\" id=\"referer\" placeholder=\"Referer\" class=\"fullInput\" />\n        <button id=\"parse\" type=\"button\" data-i18n=\"parser\"></button>\n        <input id=\"uploadM3U8\" type=\"file\" />\n      </div>\n    </section>\n\n    <section id=\"more_m3u8\" class=\"hide\">\n      <h1 class=\"optionsTitle\" data-i18n=\"selectVideo\"></h1>\n      <span class=\"explain\" data-i18n=\"nextLevelTip\"></span>\n      <div class=\"optionBox\">\n        <div id=\"next_m3u8\"></div>\n      </div>\n    </section>\n    <section id=\"more_audio\" class=\"hide\">\n      <h1 class=\"optionsTitle\" data-i18n=\"selectAudio\"></h1>\n      <span class=\"explain\" data-i18n=\"multipleAudiosTip\"></span>\n      <div class=\"optionBox\">\n        <div id=\"next_audio\"></div>\n      </div>\n    </section>\n    <section id=\"more_subtitle\" class=\"hide\">\n      <h1 class=\"optionsTitle\" data-i18n=\"multipleSubtitles\"></h1>\n      <span class=\"explain\" data-i18n=\"multipleSubtitlesTip\"></span>\n      <div class=\"optionBox\">\n        <div id=\"next_subtitle\"></div>\n      </div>\n    </section>\n\n    <section id=\"more_options\" class=\"hide\">\n      <div class=\"optionBox\">\n        <button id=\"more_options_merge\" class=\"button2\" data-i18n=\"onlineMerge\"></button>\n      </div>\n    </section>\n\n    <section id=\"m3u8\" class=\"hide\">\n      <h1 class=\"optionsTitle\" data-i18n=\"titleM3U8\"></h1>\n      <span class=\"explain\" id=\"key\"></span>\n      <div class=\"optionBox\">\n        <div class=\"block\">\n          <p data-i18n-outer=\"m3u8Url\"></p>\n          <p><a id=\"m3u8_url\"></a></p>\n        </div>\n\n        <div class=\"block\" id=\"tips\"></div>\n        <div class=\"block hide\" id=\"maybeKey\">\n          <select class=\"m3u8Key select\">\n            <option value=\"tips\" data-i18n=\"possibleKey\"></option>\n          </select>\n          <button id=\"searchingForRealKey\" data-i18n=\"searchingForRealKey\"></button>\n        </div>\n\n        <div class=\"videoInfo flex\">\n          <div id=\"count\"></div>\n          <div id=\"info\"></div>\n          <div id=\"estimateFileSize\"></div>\n        </div>\n        <div class=\"block\" id=\"textarea\">\n          <details>\n            <summary data-i18n=\"viewSlices\" class=\"button\"></summary>\n            <textarea id=\"media_file\" spellcheck=\"false\" data-type=\"link\" class=\"width100\"\n              data-i18n=\"loading\"></textarea>\n            <div id=\"downList\" data-i18n=\"waitDownload\"></div>\n            <div class=\"merge\">\n              <button id=\"downText\" data-i18n=\"downloadSegmentList\"></button>\n              <button id=\"originalM3U8\" data-i18n=\"originalM3u8\"></button>\n              <button id=\"localFile\" class=\"hide\" data-i18n=\"localM3u8\"></button>\n              <button id=\"downProgress\" data-i18n=\"downloadProgress\"></button>\n              <button id=\"getTs\" data-i18n=\"segmentList\"></button>\n            </div>\n          </details>\n        </div>\n        <video id=\"video\" class=\"hide\" controls></video>\n        <div class=\"block\" id=\"button\">\n          <div class=\"merge\">\n            <button id=\"tsAddArg\" data-i18n=\"getParameters\"></button>\n            <button id=\"setRequestHeaders\" data-i18n=\"requestHeaders\"></button>\n            <button id=\"play\" data-switch=\"on\" data-i18n=\"play\"></button>\n            <button id=\"m3u8DL\" class=\"button2\" data-i18n=\"invokeM3u8DL\"></button>\n            <button id=\"copyM3U8dl\" class=\"hide\" data-i18n=\"copyCommand\"></button>\n            <button id=\"setM3u8dl\" data-i18n=\"previewCommand\"></button>\n            <label class=\"m3u8checkbox textColor\">\n              <p data-i18n-outer=\"addSettingParameters\"></p><input type=\"checkbox\" id=\"addParam\" save=\"change\" />\n            </label>\n            <button id=\"invoke\" class=\"button2\" data-i18n=\"invoke\"></button>\n            <button class=\"openDir hide\" data-i18n=\"downloadDir\"></button>\n            <button id=\"sendFfmpeg\" class=\"hide button\" data-i18n=\"sendFfmpeg\"></button>\n          </div>\n          <textarea id=\"m3u8dlArg\" type=\"text\" class=\"m3u8dlArg hide\"></textarea>\n          <div class=\"line\"></div>\n          <div class=\"merge customKey\">\n            <input type=\"text\" id=\"customFilename\" spellcheck=\"false\" data-i18n-placeholder=\"customSaveFileName\"\n              size=\"30\" />\n            <input type=\"text\" id=\"customKey\" spellcheck=\"false\" data-i18n-placeholder=\"customKeyPlaceholder\"\n              size=\"60\" />\n            <input type=\"text\" id=\"customIV\" spellcheck=\"false\" placeholder=\"IV\" size=\"30\" />\n            <button id=\"uploadKey\" data-i18n=\"uploadKey\"></button>\n            <input id=\"uploadKeyFile\" type=\"file\" class=\"hide\" />\n          </div>\n          <div class=\"merge\" id=\"mergeDown\">\n            <div id=\"threadDiv\" class=\"textColor\">\n              <p data-i18n-outer=\"downloadThreads\"></p> <input type=\"number\" value=\"6\" id=\"thread\" spellcheck=\"false\"\n                style=\"width: 35px;\" step=\"1\" min=\"1\" max=\"256\" save=\"change\" />\n            </div>\n            <label class=\"m3u8checkbox textColor\">\n              <p data-i18n-outer=\"ffmpegTranscoding\"></p><input type=\"checkbox\" id=\"ffmpeg\" save=\"change\" />\n            </label>\n            <label class=\"m3u8checkbox textColor\">\n              <p data-i18n-outer=\"mp4Format\"></p><input type=\"checkbox\" id=\"mp4\" save=\"change\" />\n            </label>\n            <label class=\"m3u8checkbox textColor firefoxHide\">\n              <p data-i18n-outer=\"downloadWhileSaving\"></p><input type=\"checkbox\" id=\"StreamSaver\" save=\"change\" />\n            </label>\n            <label class=\"m3u8checkbox textColor\">\n              <p data-i18n-outer=\"audioOnly\"></p><input type=\"checkbox\" id=\"onlyAudio\" save=\"change\" />\n            </label>\n            <label class=\"m3u8checkbox textColor\">\n              <p data-i18n-outer=\"saveAs\"></p><input type=\"checkbox\" id=\"saveAs\" save=\"change\" />\n            </label>\n            <label class=\"m3u8checkbox textColor\">\n              <p data-i18n-outer=\"skipDecryption\"></p><input type=\"checkbox\" id=\"skipDecrypt\" save=\"change\" />\n            </label>\n            <label class=\"m3u8checkbox textColor\">\n              <p data-i18n-outer=\"autoClosePageAfterDownload\"></p><input type=\"checkbox\" id=\"autoClose\" save=\"change\" />\n            </label>\n            <div class=\"rangeDown textColor\">\n              <p data-i18n-outer=\"downloadRange\"></p>\n              <select id=\"cc\" class=\"hide\">\n                <option disabled selected hidden>playlist</option>\n              </select>\n              <div class=\"merge\">\n                <input type=\"text\" id=\"rangeStart\" spellcheck=\"false\" data-i18n-placeholder=\"start\" value=\"1\" />\n                <input type=\"text\" id=\"rangeEnd\" spellcheck=\"false\" data-i18n-placeholder=\"end\" />\n              </div>\n            </div>\n            <button id=\"recorder\" class=\"button2 hide\" data-switch=\"on\" data-switch=\"on\"\n              data-i18n=\"recordLive\"></button>\n            <div class=\"textColor m3u8checkbox\">\n              <p data-i18n-outer=\"retryCount\"></p>(test) <input type=\"number\" value=\"0\" id=\"retryCount\"\n                spellcheck=\"false\" style=\"width: 35px;\" step=\"1\" min=\"0\" />\n            </div>\n            <button id=\"mergeTs\" class=\"button2\" data-i18n=\"mergeDownloads\"></button>\n            <button id=\"errorDownload\" class=\"hide button2\" data-i18n=\"redownloadFailedItems\"></button>\n            <button id=\"ForceDownload\" class=\"hide button2\" data-i18n=\"downloadExistingData\"></button>\n            <button id=\"stopDownload\" class=\"hide button2\" data-i18n=\"stopDownload\"></button>\n          </div>\n          <div style=\"display: flex; margin-top: 10px;\">\n            <div id=\"progress\"></div>\n            <div id=\"fileSize\"></div>\n            <div id=\"fileDuration\"></div>\n          </div>\n          <div class=\"block hide\" id=\"errorTsList\"></div>\n        </div>\n\n      </div>\n    </section>\n  </div>\n  <script src=\"lib/m3u8-decrypt.js\"></script>\n  <script src=\"lib/hls.min.js\"></script>\n  <script src=\"lib/base64.js\"></script>\n  <script src=\"lib/StreamSaver.js\"></script>\n  <script src=\"js/m3u8.downloader.js\"></script>\n  <script src=\"js/m3u8.js\"></script>\n  <script src=\"js/i18n.js\"></script>\n</body>\n\n</html>"
  },
  {
    "path": "manifest.firefox.json",
    "content": "{\n  \"background\": {\n    \"scripts\": [\n      \"js/firefox.js\",\n      \"js/background.js\"\n    ]\n  },\n  \"action\": {\n    \"default_icon\": \"img/icon.png\",\n    \"default_title\": \"__MSG_catCatch__\",\n    \"default_popup\": \"popup.html\"\n  },\n  \"description\": \"__MSG_description__\",\n  \"icons\": {\n    \"64\": \"img/icon.png\",\n    \"128\": \"img/icon128.png\"\n  },\n  \"manifest_version\": 3,\n  \"name\": \"__MSG_catCatch__\",\n  \"homepage_url\": \"https://github.com/xifangczy/cat-catch\",\n  \"options_ui\": {\n    \"page\": \"options.html\",\n    \"open_in_tab\": true\n  },\n  \"permissions\": [\n    \"tabs\",\n    \"webRequest\",\n    \"downloads\",\n    \"storage\",\n    \"webNavigation\",\n    \"alarms\",\n    \"scripting\",\n    \"declarativeNetRequest\"\n  ],\n  \"commands\": {\n    \"_execute_browser_action\": {},\n    \"enable\": {\n      \"description\": \"__MSG_pause__ / __MSG_enable__\"\n    },\n    \"auto_down\": {\n      \"description\": \"__MSG_autoDownload__\"\n    },\n    \"catch\": {\n      \"description\": \"__MSG_cacheCapture__\"\n    },\n    \"m3u8\": {\n      \"description\": \"__MSG_m3u8Parser__\"\n    },\n    \"clear\": {\n      \"description\": \"__MSG_clear__\"\n    },\n    \"reboot\": {\n      \"description\": \"__MSG_restartExtension__\"\n    },\n    \"deepSearch\": {\n      \"description\": \"__MSG_deepSearch__\"\n    }\n  },\n  \"browser_specific_settings\": {\n    \"gecko\": {\n      \"id\": \"xifangczy@gmail.com\",\n      \"strict_min_version\": \"113.0\"\n    }\n  },\n  \"host_permissions\": [\n    \"*://*/*\",\n    \"<all_urls>\"\n  ],\n  \"content_scripts\": [\n    {\n      \"matches\": [\n        \"https://*/*\",\n        \"http://*/*\"\n      ],\n      \"js\": [\n        \"js/content-script.js\"\n      ],\n      \"all_frames\": true,\n      \"run_at\": \"document_start\"\n    }\n  ],\n  \"default_locale\": \"en\",\n  \"version\": \"2.6.7\"\n}"
  },
  {
    "path": "manifest.json",
    "content": "{\n  \"background\": {\n    \"service_worker\": \"js/background.js\"\n  },\n  \"action\": {\n    \"default_icon\": \"img/icon.png\",\n    \"default_title\": \"__MSG_catCatch__\",\n    \"default_popup\": \"popup.html\"\n  },\n  \"side_panel\": {\n    \"default_path\": \"popup.html\"\n  },\n  \"description\": \"__MSG_description__\",\n  \"icons\": {\n    \"64\": \"img/icon.png\",\n    \"128\": \"img/icon128.png\"\n  },\n  \"manifest_version\": 3,\n  \"minimum_chrome_version\": \"93\",\n  \"name\": \"__MSG_catCatch__\",\n  \"homepage_url\": \"https://github.com/xifangczy/cat-catch\",\n  \"options_ui\": {\n    \"page\": \"options.html\",\n    \"open_in_tab\": true\n  },\n  \"permissions\": [\n    \"tabs\",\n    \"webRequest\",\n    \"downloads\",\n    \"storage\",\n    \"webNavigation\",\n    \"alarms\",\n    \"declarativeNetRequest\",\n    \"scripting\",\n    \"sidePanel\"\n  ],\n  \"commands\": {\n    \"_execute_action\": {},\n    \"enable\": {\n      \"description\": \"__MSG_pause__ / __MSG_enable__\"\n    },\n    \"auto_down\": {\n      \"description\": \"__MSG_autoDownload__\"\n    },\n    \"catch\": {\n      \"description\": \"__MSG_cacheCapture__\"\n    },\n    \"m3u8\": {\n      \"description\": \"__MSG_m3u8Parser__\"\n    },\n    \"clear\": {\n      \"description\": \"__MSG_clear__\"\n    },\n    \"reboot\": {\n      \"description\": \"__MSG_restartExtension__\"\n    },\n    \"deepSearch\": {\n      \"description\": \"__MSG_deepSearch__\"\n    }\n  },\n  \"host_permissions\": [\n    \"*://*/*\",\n    \"<all_urls>\"\n  ],\n  \"content_scripts\": [\n    {\n      \"matches\": [\n        \"https://*/*\",\n        \"http://*/*\"\n      ],\n      \"js\": [\n        \"js/content-script.js\"\n      ],\n      \"run_at\": \"document_start\",\n      \"all_frames\": true\n    }\n  ],\n  \"default_locale\": \"en\",\n  \"version\": \"2.6.7\",\n  \"incognito\": \"split\"\n}"
  },
  {
    "path": "mpd.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n  <meta charset=\"UTF-8\" />\n  <title>titledash</title>\n  <link rel=\"stylesheet\" type=\"text/css\" href=\"css/public.css\" media=\"all\" />\n  <link rel=\"stylesheet\" type=\"text/css\" href=\"css/options.css\" media=\"all\" />\n  <script src=\"lib/jquery.min.js\"></script>\n  <script src=\"js/init.js\"></script>\n  <script src=\"js/firefox.js\"></script>\n  <script src=\"lib/mpd-parser.min.js\"></script>\n  <script src=\"js/function.js\"></script>\n  <script src=\"js/mpd.js\"></script>\n</head>\n\n<body>\n  <div class=\"wrapper1024 dash\">\n    <section id=\"loading\">\n      <div class=\"optionBox\" data-i18n=\"loading\"></div>\n    </section>\n\n    <section id=\"mpdCustom\" class=\"hide\">\n      <h1 class=\"optionsTitle\" data-i18n=\"titledash\"></h1>\n      <div class=\"optionBox\">\n        <input type=\"text\" id=\"mpdUrl\" placeholder=\"mpd URL\" class=\"fullInput\" />\n        <input type=\"text\" id=\"referer\" placeholder=\"Referer\" class=\"fullInput\" />\n        <button id=\"parse\" type=\"button\" data-i18n=\"parser\"></button>\n      </div>\n    </section>\n\n    <section id=\"main\" class=\"hide\">\n      <h1 class=\"optionsTitle\" data-i18n=\"titledash\"></h1>\n      <div class=\"optionBox\">\n        <div class=\"block\">\n          mpd url\n          <p><a id=\"mpd_url\"></a></p>\n        </div>\n        <div class=\"block\" id=\"tips\"></div>\n        <div class=\"videoInfo flex\">\n          <div id=\"count\"></div>\n          <div id=\"info\"></div>\n        </div>\n        <div class=\"block\" id=\"textarea\">\n          <textarea id=\"media_file\" spellcheck=\"false\" data-type=\"link\" class=\"width100\"></textarea>\n        </div>\n        <div class=\"flex\">\n          <div>\n            <p data-i18n-outer=\"selectVideo\"></p>: <select id=\"mpdVideoLists\" class=\"select\"></select>\n          </div>\n          <button id=\"getVideo\" data-i18n=\"extractSlices\"></button>\n          <button id=\"videoToM3u8\" data-i18n=\"convertToM3U8\"></button>\n        </div>\n        <div class=\"line\"></div>\n        <div class=\"flex\">\n          <div>\n            <p data-i18n-outer=\"selectAudio\"></p>: <select id=\"mpdAudioLists\" class=\"select\"></select>\n          </div>\n          <button id=\"getAudio\" data-i18n=\"extractSlices\"></button>\n          <button id=\"audioToM3u8\" data-i18n=\"convertToM3U8\"></button>\n        </div>\n        <div style=\"display: flex; margin-top: 10px;\">\n          <div id=\"progress\"></div>\n          <div id=\"fileSize\"></div>\n        </div>\n      </div>\n    </section>\n  </div>\n  <script src=\"js/i18n.js\"></script>\n</body>\n\n</html>"
  },
  {
    "path": "options.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\" />\n  <title>titleOption</title>\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <link rel=\"stylesheet\" type=\"text/css\" href=\"css/public.css\" media=\"all\" />\n  <link rel=\"stylesheet\" type=\"text/css\" href=\"css/options.css\" media=\"all\" />\n  <script src=\"lib/jquery.min.js\" defer></script>\n  <script src=\"js/init.js\" defer></script>\n  <script src=\"js/function.js\" defer></script>\n</head>\n\n<body>\n  <!-- 添加导航栏 -->\n  <nav class=\"sidebar\" aria-label=\"Main Navigation\">\n    <ul>\n      <li><a href=\"#anchorSuffix\" data-i18n=\"suffix\" title=\"suffix\"></a></li>\n      <li><a href=\"#anchorType\" data-i18n=\"type\" title=\"type\"></a></li>\n      <li><a href=\"#anchorRegexMatch\" data-i18n=\"regexMatch\" title=\"regexMatch\"></a></li>\n      <li><a href=\"#anchorBlockUrl\" data-i18n=\"blockUrl\" title=\"blockUrl\"></a></li>\n      <li><a href=\"#anchorScriptSettings\" data-i18n=\"Script\" title=\"Script\"></a></li>\n      <li><a href=\"#anchorCopy\" data-i18n=\"copy\" title=\"copy\"></a></li>\n      <li><a href=\"#anchorAria2Rpc\" title=\"Aria2 RPC\">Aria2 RPC</a></li>\n      <li><a href=\"#anchorSend2local\" data-i18n=\"send2local\" title=\"send2local\"></a></li>\n      <li><a href=\"#anchorM3u8dl\" data-i18n=\"sideurlprotocol\" title=\"URL Protocol m3u8dl\"></a></li>\n      <li><a href=\"#anchorInvokeApp\" data-i18n=\"invokeApp\" title=\"invokeApp\"></a></li>\n      <li><a href=\"#anchorReplaceTags\" data-i18n=\"replaceTags\" title=\"replaceTags\"></a></li>\n      <li><a href=\"#anchorDownloader\" data-i18n=\"downloader\" title=\"downloader\"></a></li>\n      <li><a href=\"#anchorM3u8Parser\" data-i18n=\"m3u8Parser\" title=\"m3u8Parser\"></a></li>\n      <li><a href=\"#anchorOtherSettings\" data-i18n=\"otherSettings\" title=\"otherSettings\"></a></li>\n      <li><a href=\"#anchorCustomCSS\" data-i18n=\"customCSS\" title=\"customCSS\"></a></li>\n      <li><a href=\"#anchorMQTT\" data-i18n=\"MQTT\" title=\"MQTT\"></a></li>\n      <li><a href=\"#anchorOperation\" data-i18n=\"operation\" title=\"operation\"></a></li>\n      <li><a href=\"#anchorAbout\" data-i18n=\"about\" title=\"about\"></a></li>\n    </ul>\n  </nav>\n\n  <div class=\"wrapper options\">\n\n    <!-- 抓取后缀 -->\n    <section id=\"anchorSuffix\">\n      <h1 class=\"optionsTitle\" data-i18n=\"suffix\"></h1>\n      <div class=\"optionBox\">\n        <span class=\"explain\" data-i18n=\"suffixTip\"></span>\n        <table id=\"extList\">\n          <tr>\n            <th data-i18n=\"suffix\"></th>\n            <th data-i18n=\"filterSize\"></th>\n            <th data-i18n=\"enable\"></th>\n            <th data-i18n=\"delete\"></th>\n          </tr>\n        </table>\n        <div class=\"flex-end\">\n          <button type=\"button\" id=\"AddExt\" class=\"button2\" data-i18n=\"addSuffix\" title=\"add Suffix\"></button>\n          <button type=\"button\" id=\"ResetExt\" data-reset=\"Ext\" data-i18n=\"resetSettings\" title=\"reset Suffixs\"></button>\n          <button type=\"button\" id=\"allDisable\" data-switch=\"Ext\" data-i18n=\"disableAll\"\n            title=\"disable All Ext\"></button>\n          <button type=\"button\" id=\"allEnable\" data-switch=\"Ext\" data-i18n=\"enableAll\" title=\"enable All Ext\"></button>\n        </div>\n      </div>\n    </section>\n\n    <!-- 抓取类型 -->\n    <section id=\"anchorType\">\n      <h1 class=\"optionsTitle\" data-i18n=\"type\"></h1>\n      <div class=\"optionBox\">\n        <span class=\"explain\" data-i18n=\"typeTip\"></span>\n        <table id=\"typeList\">\n          <tr>\n            <th data-i18n=\"type\"></th>\n            <th data-i18n=\"filterSize\"></th>\n            <th data-i18n=\"enable\"></th>\n            <th data-i18n=\"delete\"></th>\n          </tr>\n        </table>\n        <div class=\"flex-end\">\n          <button type=\"button\" id=\"AddType\" class=\"button2\" data-i18n=\"addType\" title=\"add Type\"></button>\n          <button type=\"button\" id=\"ResetType\" data-reset=\"Type\" data-i18n=\"resetSettings\" title=\"reset Types\"></button>\n          <button type=\"button\" id=\"allDisable\" data-switch=\"Type\" data-i18n=\"disableAll\"\n            title=\"disable All Types\"></button>\n          <button type=\"button\" id=\"allEnable\" data-switch=\"Type\" data-i18n=\"enableAll\"\n            title=\"enable All Types\"></button>\n        </div>\n      </div>\n    </section>\n\n    <!-- 正则匹配 -->\n    <section id=\"anchorRegexMatch\">\n      <h1 class=\"optionsTitle\">\n        <img src=\"img/regex.png\" style=\"width: 18px\" class=\"regex\" /><span data-i18n-outer=\"regexMatch\"></span> / <span\n          data-i18n-outer=\"blockResource\"></span>\n      </h1>\n      <div class=\"optionBox\">\n        <span class=\"explain\"><b data-i18n=\"blockResource\"></b>\n          <p data-i18n-outer=\"blockResourceTip\"></p>\n        </span><br>\n        <span class=\"explain\"><b data-i18n=\"flag\"></b>\n\n        </span><br>\n        <span class=\"explain\"><b data-i18n=\"suffix\"></b>\n          <p data-i18n-outer=\"regexSuffixTip\"></p>\n        </span><br>\n        <span class=\"explain\"><b data-i18n=\"regexTip\"></b></span><br><br>\n        <table id=\"regexList\">\n          <tr>\n            <th data-i18n=\"flag\"></th>\n            <th data-i18n=\"regexExpression\"></th>\n            <th data-i18n=\"suffix\"></th>\n            <th data-i18n=\"blockResource\"></th>\n            <th data-i18n=\"enable\"></th>\n            <th data-i18n=\"delete\"></th>\n          </tr>\n        </table>\n        <div class=\"flex-end\">\n          <button type=\"button\" id=\"AddRegex\" class=\"button2\" data-i18n=\"addRegex\" title=\"add Regex\"></button>\n          <button type=\"button\" id=\"ResetRegex\" data-reset=\"Regex\" data-i18n=\"resetSettings\"\n            title=\"reset Regex\"></button>\n          <button type=\"button\" id=\"allDisable\" data-switch=\"Regex\" data-i18n=\"disableAll\"\n            title=\"disable All Regex\"></button>\n          <button type=\"button\" id=\"allEnable\" data-switch=\"Regex\" data-i18n=\"enableAll\"\n            title=\"enable All Regex\"></button>\n        </div>\n        <span style=\"font-weight: bold; font-size: 15px\" data-i18n=\"regexTest\"></span><br />\n        <label for=\"testUrl\">URL</label><br />\n        <input type=\"text\" id=\"testUrl\" style=\"width: 590px\" /><br />\n        <label for=\"testRegex\" data-i18n=\"regex\"></label><br />\n        <input type=\"text\" id=\"testRegex\" style=\"width: 590px\" /><br />\n        <label for=\"testFlag\" data-i18n=\"flag\"></label><br />\n        <input type=\"text\" id=\"testFlag\" style=\"width: 20px\" value=\"ig\" maxlength=\"4\" />\n        <span style=\"color: #ff0000\" id=\"testResult\" data-i18n=\"noMatch\"></span>\n      </div>\n    </section>\n\n    <!-- 屏蔽网站 -->\n    <section id=\"anchorBlockUrl\">\n      <h1 class=\"optionsTitle\" data-i18n=\"blockUrl\"></h1>\n      <div class=\"optionBox\">\n        <div class=\"item\">\n          <div data-i18n=\"setWhiteList\"></div>\n          <div class=\"switch\">\n            <label class=\"switchLabel switchRadius\">\n              <input type=\"checkbox\" id=\"blockUrlWhite\" save=\"click\" class=\"switchInput\" />\n              <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n            </label>\n          </div>\n        </div>\n        <span class=\"explain\" data-i18n=\"blockUrlTips\"></span>\n        <table id=\"blockUrlList\">\n          <tr>\n            <th>URL</th>\n            <th data-i18n=\"enable\"></th>\n            <th data-i18n=\"delete\"></th>\n          </tr>\n        </table>\n        <div class=\"flex-end\">\n          <button type=\"button\" id=\"blockAddUrl\" class=\"button2\" data-i18n=\"addUrl\" title=\"add Url\"></button>\n          <button type=\"button\" id=\"ResetBlockUrl\" data-reset=\"blockUrl\" data-i18n=\"resetSettings\"\n            title=\"reset block Url\"></button>\n          <button type=\"button\" id=\"allDisable\" data-switch=\"blockUrl\" data-i18n=\"disableAll\"\n            title=\"disable All block Url\"></button>\n          <button type=\"button\" id=\"allEnable\" data-switch=\"blockUrl\" data-i18n=\"enableAll\"\n            title=\"enable All block Url\"></button>\n        </div>\n      </div>\n    </section>\n\n    <!-- 脚本设置 -->\n    <section id=\"anchorScriptSettings\">\n      <h1 class=\"optionsTitle\" data-i18n=\"Script\"></h1>\n      <div class=\"optionBox\" id=\"scriptOption\">\n        <div class=\"list loose\">\n\n          <div class=\"item\">\n            <div data-i18n=\"alwaysSearch\"></div>\n            <div class=\"switch\">\n              <label class=\"switchLabel switchRadius\">\n                <input type=\"checkbox\" save=\"click\" id=\"deepSearch\" class=\"switchInput\" />\n                <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n              </label>\n            </div>\n          </div>\n\n        </div>\n        <div class=\"flex-end\">\n          <button type=\"button\" class=\"resetOption\" data-i18n=\"resetSettings\"></button>\n        </div>\n      </div>\n    </section>\n\n    <!-- 复制选项 -->\n    <section id=\"anchorCopy\">\n      <h1 class=\"optionsTitle\">\n        <img src=\"img/copy.png\" style=\"width: 18px\" class=\"copy\" alt=\"Copy icon\" />\n        <p data-i18n-outer=\"copy\"></p>\n      </h1>\n      <div class=\"optionBox\">\n        <span class=\"explain\">\n          <p data-i18n-outer=\"copyTip\"></p><br />\n          <a href=\"https://o2bmm.gitbook.io/cat-catch/docs/tag\" target=\"_blank\" data-i18n=\"replaceKeywordList\"></a><br /><br />\n        </span>\n        <div class=\"item\">\n          <div>HLS m3u8</div>\n          <textarea id=\"copyM3U8\" save=\"input\" type=\"text\" class=\"width100\"></textarea>\n        </div>\n        <div class=\"item\" style=\"margin-top: 10px;\">\n          <div>DASH mpd</div>\n          <textarea id=\"copyMPD\" save=\"input\" type=\"text\" class=\"width100\"></textarea>\n        </div>\n        <div class=\"item\" style=\"margin-top: 10px;\">\n          <div data-i18n=\"otherFiles\"></div>\n          <textarea id=\"copyOther\" save=\"input\" type=\"text\" class=\"width100\"></textarea>\n        </div>\n        <div class=\"flex-end\">\n          <button type=\"button\" class=\"resetOption\" data-i18n=\"resetSettings\"></button>\n        </div>\n      </div>\n    </section>\n\n    <!-- Aria2 RPC -->\n    <section id=\"anchorAria2Rpc\">\n      <h1 class=\"optionsTitle\">\n        <img src=\"img/aria2.png\" style=\"width: 18px\" class=\"aria2\" />\n        Aria2 RPC\n      </h1>\n      <div class=\"optionBox\">\n        <span class=\"explain\">\n          <p data-i18n-outer=\"aria2Tip\"></p>\n          <a href=\"https://aria2.github.io/manual/en/html/aria2c.html#rpc-interface\" target=\"_blank\"\n            data-i18n=\"documentation\"></a>\n        </span>\n        <div class=\"list\">\n          <div class=\"item\">\n            <div><span data-i18n-outer=\"enable\"></span> Aria2 RPC</div>\n            <div class=\"switch\">\n              <label class=\"switchLabel switchRadius\">\n                <input type=\"checkbox\" id=\"enableAria2Rpc\" save=\"click\" class=\"switchInput\" />\n                <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n              </label>\n            </div>\n          </div>\n          <div class=\"item\">\n            <div data-i18n=\"autoSetRefererCookieParams\"></div>\n            <div class=\"switch\">\n              <label class=\"switchLabel switchRadius\">\n                <input type=\"checkbox\" id=\"enableAria2RpcReferer\" save=\"click\" class=\"switchInput\" />\n                <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n              </label>\n            </div>\n          </div>\n          <div class=\"item\">\n            <div data-i18n=\"secretKey\"></div>\n            <input id=\"aria2RpcToken\" save=\"input\" type=\"password\" class=\"width100\">\n          </div>\n          <div class=\"item\">\n            <div>Aria2 RPC <span data-i18n-outer=\"address\"></span></div>\n            <input id=\"aria2Rpc\" save=\"input\" type=\"text\" class=\"width100\">\n          </div>\n          <div class=\"flex-end\">\n            <button type=\"button\" class=\"resetOption\" data-i18n=\"resetSettings\"></button>\n          </div>\n        </div>\n      </div>\n    </section>\n\n    <!-- 发送数据 -->\n    <section id=\"anchorSend2local\">\n      <h1 class=\"optionsTitle\">\n        <img src=\"img/send.svg\" style=\"width: 18px\" class=\"regex\" alt=\"Send icon\" />\n        <p data-i18n-outer=\"send2local\"></p>\n      </h1>\n      <div class=\"optionBox\">\n        <span class=\"explain\">\n          <a href=\"https://o2bmm.gitbook.io/cat-catch/docs/settings#send\" target=\"_blank\" data-i18n=\"documentation\"></a>\n        </span>\n        <div class=\"list\">\n          <div class=\"item\">\n            <div><span data-i18n-outer=\"autoSend\"></span></div>\n            <div class=\"switch\">\n              <label class=\"switchLabel switchRadius\">\n                <input type=\"checkbox\" id=\"send2local\" save=\"click\" class=\"switchInput\" />\n                <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n              </label>\n            </div>\n          </div>\n\n          <div class=\"item\">\n            <div><span data-i18n=\"manualSend\"></span></div>\n            <div class=\"switch\">\n              <label class=\"switchLabel switchRadius\">\n                <input type=\"checkbox\" id=\"send2localManual\" save=\"click\" class=\"switchInput\" />\n                <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n              </label>\n            </div>\n          </div>\n\n          <div class=\"item\">\n            <div><span data-i18n-outer=\"requestMethod\"></span> </div>\n            <div class=\"switch switchSelect\">\n              <select id=\"send2localMethod\" class=\"select\" save=\"select\">\n                <option value=\"GET\">GET</option>\n                <option value=\"POST\">POST</option>\n                <option value=\"PUT\">PUT</option>\n              </select>\n            </div>\n          </div>\n\n          <div class=\"item\">\n            <div><span>Content-Type</span> </div>\n            <div class=\"switch switchSelect send2localType\">\n              <select id=\"send2localType\" class=\"select\" save=\"select\">\n                <option value=\"0\">application/json;charset=utf-8</option>\n                <option value=\"1\">multipart/form-data</option>\n                <option value=\"2\">application/x-www-form-urlencoded</option>\n                <option value=\"3\">text/plain</option>\n              </select>\n            </div>\n          </div>\n\n          <div class=\"item\">\n            <div><span data-i18n-outer=\"address\"></span></div>\n            <input id=\"send2localURL\" save=\"input\" type=\"text\" class=\"width100\">\n          </div>\n\n          <div class=\"item\">\n            <div>\n              <p data-i18n=\"requestBody\"></p>\n              <a href=\"https://o2bmm.gitbook.io/cat-catch/docs/tag\" target=\"_blank\" data-i18n=\"replaceKeywordList\"></a>\n            </div>\n            <textarea id=\"send2localBody\" save=\"input\" class=\"width100 break-all\" rows=\"3\"></textarea>\n          </div>\n\n          <div class=\"flex-end\">\n            <button type=\"button\" class=\"resetOption\" data-i18n=\"resetSettings\"></button>\n          </div>\n        </div>\n      </div>\n    </section>\n\n    <!-- m3u8DL -->\n    <section id=\"anchorM3u8dl\">\n      <h1 class=\"optionsTitle\" data-i18n=\"sideurlprotocol\">URL Protocol m3u8dl</h1>\n      <div class=\"optionBox\" id=\"m3u8dlOption\">\n        <span class=\"explain\">\n          <a href=\"https://github.com/nilaoda/N_m3u8DL-CLI\" target=\"_blank\">N_m3u8DL-CLI</a> / <a\n            href=\"https://github.com/nilaoda/N_m3u8DL-RE\" target=\"_blank\">N_m3u8DL-RE</a>\n          <p data-i18n-outer=\"m3u8DLTips\"></p>\n          <a href=\"https://o2bmm.gitbook.io/cat-catch/docs/m3u8dl\" target=\"_blank\" data-i18n=\"documentation\"></a>\n        </span>\n        <div class=\"list\">\n          <div class=\"item\">\n            <div><span data-i18n-outer=\"enable\"></span> m3u8dl:// <span data-i18n-outer=\"download\"></span> m3u8 or mpd\n            </div>\n            <div class=\"switch m3u8DL\">\n              <select id=\"m3u8dl\" class=\"select\" save=\"select\">\n                <option value=\"0\" data-i18n=\"disable\"></option>\n                <option value=\"1\">N_m3u8DL-CLI</option>\n                <option value=\"2\">N_m3u8DL-RE</option>\n              </select>\n            </div>\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"confirmParameters\"></div>\n            <div class=\"switch\">\n              <label class=\"switchLabel switchRadius\">\n                <input type=\"checkbox\" id=\"m3u8dlConfirm\" save=\"click\" class=\"switchInput\" />\n                <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n              </label>\n            </div>\n          </div>\n\n          <!-- <div class=\"item\"></div> -->\n\n          <div class=\"item\">\n            <div style=\"margin-bottom: 5px;margin-top: 5px;\">\n              <p data-i18n=\"parameterSetting\"></p>\n\n              <a href=\"https://o2bmm.gitbook.io/cat-catch/docs/tag\" target=\"_blank\" data-i18n=\"replaceKeywordList\"></a>\n              <a href=\"https://nilaoda.github.io/N_m3u8DL-CLI/Advanced.html\" target=\"_blank\"\n                style=\"margin-left: 10px;\">N_m3u8DL-CLI <span data-i18n-outer=\"parameter\"></span></a>\n\n              <a href=\"https://github.com/nilaoda/N_m3u8DL-RE?tab=readme-ov-file#%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%8F%82%E6%95%B0\"\n                target=\"_blank\" style=\"margin-left: 10px;\">N_m3u8DL-RE <span data-i18n-outer=\"parameter\"></span></a>\n            </div>\n            <textarea id=\"m3u8dlArg\" save=\"input\" type=\"text\" class=\"width100 break-all\" rows=\"3\"></textarea>\n          </div>\n          <div class=\"flex-end\">\n            <button type=\"button\" class=\"resetOption\" data-i18n=\"resetSettings\"></button>\n          </div>\n        </div>\n      </div>\n    </section>\n\n    <!-- 第三方本地程序调用 -->\n    <section id=\"anchorInvokeApp\">\n      <h1 class=\"optionsTitle\">\n        <img src=\"img/invoke.svg\" style=\"width: 18px\" class=\"invoke\" />\n        <p data-i18n-outer=\"invokeApp\"></p>\n      </h1>\n      <div class=\"optionBox\" id=\"invokeOption\">\n        <div class=\"list\">\n          <div class=\"item\">\n            <div data-i18n=\"enable\"></div>\n            <div class=\"switch\">\n              <label class=\"switchLabel switchRadius\">\n                <input type=\"checkbox\" id=\"invoke\" save=\"click\" class=\"switchInput\" />\n                <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n              </label>\n            </div>\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"confirmParameters\"></div>\n            <br>\n            <div class=\"switch\">\n              <label class=\"switchLabel switchRadius\">\n                <input type=\"checkbox\" id=\"invokeConfirm\" save=\"click\" class=\"switchInput\" />\n                <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n              </label>\n            </div>\n          </div>\n\n          <div class=\"item\" style=\"margin-bottom: 5px;margin-top: 5px;\">\n            <div>\n              <p data-i18n=\"parameterSetting\"></p>\n              <a href=\"https://o2bmm.gitbook.io/cat-catch/docs/tag\" target=\"_blank\" data-i18n=\"replaceKeywordList\"></a>\n            </div>\n            <textarea id=\"invokeText\" save=\"input\" type=\"text\" class=\"width100 break-all\" rows=\"3\"></textarea>\n          </div>\n          <div class=\"flex-end\">\n            <button type=\"button\" class=\"resetOption\" data-i18n=\"resetSettings\"></button>\n          </div>\n        </div>\n      </div>\n    </section>\n\n    <!-- 替换标签 -->\n    <section id=\"anchorReplaceTags\">\n      <h1 class=\"optionsTitle\" data-i18n=\"replaceTags\"></h1>\n      <div class=\"optionBox\" id=\"tag\">\n        <span class=\"explain\"></span>\n        <div style=\"margin-bottom: 5px;\">\n          <a href=\"https://o2bmm.gitbook.io/cat-catch/docs/tag\" target=\"_blank\" data-i18n=\"replaceKeywordList\"></a>\n        </div>\n        <div class=\"list\">\n          <div class=\"item\">\n            <div data-i18n=\"customSaveFileName\"></div>\n            <textarea id=\"downFileName\" save=\"input\" type=\"text\" class=\"width100\"></textarea>\n          </div>\n          <div class=\"item\">\n            <div>${userAgent} <span data-i18n-outer=\"userAgentTip\"></span></div>\n            <textarea id=\"userAgent\" save=\"input\" type=\"text\" class=\"width100\"></textarea>\n          </div>\n          <div class=\"item\">\n            <div>${mobileUserAgent} / <span data-i18n-outer=\"simulateMobile\"></span>User Agent</div>\n            <textarea id=\"MobileUserAgent\" save=\"input\" type=\"text\" class=\"width100\"></textarea>\n          </div>\n          <div id=\"testTag\" class=\"hide break-all\">\n            <div class=\"item\">\n              <div>\n                <p data-i18n=\"test\"></p>\n              </div>\n              <textarea id=\"testTextarea\" class=\"width100 break-all\"\n                rows=\"3\">${url} ${referer|exists:'--headers \"Referer:*\"'} ${url|regexp:\"(https?://[^?]*)\"|replace:\"http://\",\"https://\"|to:base64}</textarea>\n            </div>\n            <label for=\"url\">${url}</label><input type=\"text\" class=\"width100\" value=\"https://bmmmd.com/test.m3u8\"\n              id=\"url\">\n            <label for=\"referer\">${referer}</label><input type=\"text\" class=\"width100\" value=\"https://bmmmd.com/\"\n              id=\"referer\">\n            <label for=\"initiator\">${initiator}</label><input type=\"text\" class=\"width100\" value=\"https://bmmmd.com\"\n              id=\"initiator\">\n            <label for=\"webUrl\">${webUrl}</label><input type=\"text\" class=\"width100\" value=\"https://bmmmd.com/test.html\"\n              id=\"webUrl\">\n            <label for=\"title\">${title}</label><input type=\"text\" class=\"width100\" value=\"test Video\" id=\"title\">\n            <span data-i18n-outer=\"result\"></span>:<br><span id=\"tagTestResult\"></span>\n          </div>\n          <div class=\"flex-end\">\n            <button type=\"button\" id=\"showTestTag\" data-i18n=\"test\"></button>\n            <button type=\"button\" class=\"resetOption\" data-i18n=\"resetSettings\"></button>\n          </div>\n        </div>\n      </div>\n    </section>\n\n    <!-- 下载器 -->\n    <section id=\"anchorDownloader\">\n      <h1 class=\"optionsTitle\">\n        <img src=\"img/cat-down.png\" style=\"width: 18px\" class=\"cat-down\" alt=\"Downloader icon\" />\n        <p data-i18n-outer=\"downloader\"></p>\n      </h1>\n      <div class=\"optionBox\" id=\"downOption\">\n        <span class=\"explain\" data-i18n=\"downloaderTip\"></span>\n        <div class=\"list\">\n          <div class=\"item\">\n            <div data-i18n=\"alwaysDisableCatCatcher\"></div>\n            <div class=\"switch\">\n              <label class=\"switchLabel switchRadius\">\n                <input type=\"checkbox\" id=\"catDownload\" save=\"click\" class=\"switchInput\" />\n                <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n              </label>\n            </div>\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"autoClosePageAfterDownload\"></div>\n            <div class=\"switch\">\n              <label class=\"switchLabel switchRadius\">\n                <input type=\"checkbox\" id=\"downAutoClose\" save=\"click\" class=\"switchInput\" />\n                <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n              </label>\n            </div>\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"openDownloaderPageInBackground\"></div>\n            <div class=\"switch\">\n              <label class=\"switchLabel switchRadius\">\n                <input type=\"checkbox\" id=\"downActive\" save=\"click\" class=\"switchInput\" />\n                <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n              </label>\n            </div>\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"downloadWhileSaving\"></div>\n            <div class=\"switch\">\n              <label class=\"switchLabel switchRadius\">\n                <input type=\"checkbox\" id=\"downStream\" save=\"click\" class=\"switchInput\" />\n                <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n              </label>\n            </div>\n          </div>\n\n          <div class=\"flex-end\">\n            <button type=\"button\" class=\"resetOption\" data-i18n=\"resetSettings\"></button>\n          </div>\n        </div>\n      </div>\n    </section>\n\n    <!-- m3u8解析器 -->\n    <section id=\"anchorM3u8Parser\">\n      <h1 class=\"optionsTitle\">\n        <img src=\"img/parsing.png\" style=\"width: 18px\" class=\"parsing\" alt=\"Parser icon\" />\n        <p data-i18n-outer=\"m3u8Parser\"></p>\n      </h1>\n      <div class=\"optionBox\" id=\"m3u8Option\">\n        <div class=\"list\">\n\n          <div class=\"item\">\n            <div><img src=\"img/download.svg\" style=\"width: 18px\" class=\"download\"> <span\n                data-i18n-outer=\"autoDownM3u8Tip\"></span></div>\n            <div class=\"switch\">\n              <label class=\"switchLabel switchRadius\">\n                <input type=\"checkbox\" id=\"m3u8AutoDown\" save=\"click\" class=\"switchInput\" />\n                <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n              </label>\n            </div>\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"downloadThreads\"></div>\n            <div class=\"switch\">\n              <input id=\"M3u8Thread\" save=\"input\" type=\"number\" min=\"1\" max=\"32\" class=\"width3rem\">\n            </div>\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"mp4Format\"></div>\n            <div class=\"switch\">\n              <label class=\"switchLabel switchRadius\">\n                <input type=\"checkbox\" id=\"M3u8Mp4\" save=\"click\" class=\"switchInput\" />\n                <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n              </label>\n            </div>\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"audioOnly\"></div>\n            <div class=\"switch\">\n              <label class=\"switchLabel switchRadius\">\n                <input type=\"checkbox\" id=\"M3u8OnlyAudio\" save=\"click\" class=\"switchInput\" />\n                <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n              </label>\n            </div>\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"skipDecryption\"></div>\n            <div class=\"switch\">\n              <label class=\"switchLabel switchRadius\">\n                <input type=\"checkbox\" id=\"M3u8SkipDecrypt\" save=\"click\" class=\"switchInput\" />\n                <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n              </label>\n            </div>\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"downloadWhileSaving\"></div>\n            <div class=\"switch\">\n              <label class=\"switchLabel switchRadius\">\n                <input type=\"checkbox\" id=\"M3u8StreamSaver\" save=\"click\" class=\"switchInput\" />\n                <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n              </label>\n            </div>\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"ffmpegTranscoding\"></div>\n            <div class=\"switch\">\n              <label class=\"switchLabel switchRadius\">\n                <input type=\"checkbox\" id=\"M3u8Ffmpeg\" save=\"click\" class=\"switchInput\" />\n                <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n              </label>\n            </div>\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"autoClosePageAfterDownload\"></div>\n            <div class=\"switch\">\n              <label class=\"switchLabel switchRadius\">\n                <input type=\"checkbox\" id=\"M3u8AutoClose\" save=\"click\" class=\"switchInput\" />\n                <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n              </label>\n            </div>\n          </div>\n\n          <div class=\"flex-end\">\n            <button type=\"button\" class=\"resetOption\" data-i18n=\"resetSettings\"></button>\n          </div>\n        </div>\n      </div>\n    </section>\n\n    <!-- 其他设置 -->\n    <section id=\"anchorOtherSettings\">\n      <h1 class=\"optionsTitle\" data-i18n=\"otherSettings\"></h1>\n      <div class=\"optionBox\" id=\"OtherOption\">\n        <div class=\"list loose\">\n          <div class=\"item\">\n            <div>\n              <span data-i18n-outer=\"previewMode\"></span> <select id=\"PlayerTemplate\" class=\"select\"></select>\n            </div>\n            <input id=\"Player\" save=\"input\" type=\"text\" class=\"width100\"\n              data-i18n-placeholder=\"previewModePlaceholder\" />\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"customFilenameOption\"></div>\n            <div class=\"switch\">\n              <label class=\"switchLabel switchRadius\">\n                <input type=\"checkbox\" save=\"click\" id=\"TitleName\" class=\"switchInput\" />\n                <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n              </label>\n            </div>\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"saveAsOption\"></div>\n            <div class=\"switch\">\n              <label class=\"switchLabel switchRadius\">\n                <input type=\"checkbox\" id=\"saveAs\" save=\"click\" class=\"switchInput\" />\n                <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n              </label>\n            </div>\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"badgeNumber\"></div>\n            <div class=\"switch\">\n              <label class=\"switchLabel switchRadius\">\n                <input type=\"checkbox\" id=\"badgeNumber\" save=\"click\" class=\"switchInput\" />\n                <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n              </label>\n            </div>\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"iconOption\"></div>\n            <div class=\"switch\">\n              <label class=\"switchLabel switchRadius\">\n                <input type=\"checkbox\" save=\"click\" id=\"ShowWebIco\" class=\"switchInput\" />\n                <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n              </label>\n            </div>\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"clearOption\"></div>\n            <div class=\"switch switchSelect\">\n              <select id=\"autoClearMode\" class=\"select\" save=\"select\">\n                <option value=\"0\" data-i18n=\"doNotClear\"></option>\n                <option value=\"1\" data-i18n=\"normalClear\"></option>\n                <option value=\"2\" data-i18n=\"moreFrequent\"></option>\n              </select>\n            </div>\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"excludeDuplicateResources\"></div>\n            <div class=\"switch\">\n              <label class=\"switchLabel switchRadius\">\n                <input type=\"checkbox\" id=\"checkDuplicates\" save=\"click\" class=\"switchInput\" />\n                <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n              </label>\n            </div>\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"onlineServiceAddress\"></div>\n            <div class=\"switch switchSelect\">\n              <select id=\"onlineServiceAddress\" class=\"select\" save=\"select\">\n                <option value=\"0\" data-i18n=\"withinChina\"></option>\n                <option value=\"1\">cloudflare</option>\n              </select>\n            </div>\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"defaultPopup\"></div>\n            <div class=\"switch\">\n              <label class=\"switchLabel switchRadius\">\n                <input type=\"checkbox\" id=\"popup\" save=\"click\" class=\"switchInput\" />\n                <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n              </label>\n            </div>\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"selectWebpage\" style=\"margin-left: 2rem;\"></div>\n            <div class=\"switch switchSelect\">\n              <select id=\"popupMode\" class=\"select\" save=\"select\">\n                <option value=\"0\" data-i18n=\"dopreview\"></option>\n                <option value=\"1\" data-i18n=\"dopopup\"></option>\n                <option value=\"2\" data-i18n=\"winpreview\"></option>\n                <option value=\"3\" data-i18n=\"winpopup\"></option>\n              </select>\n            </div>\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"useSidePanel\"></div>\n            <div class=\"switch\">\n              <label class=\"switchLabel switchRadius\">\n                <input type=\"checkbox\" id=\"sidePanel\" save=\"click\" class=\"switchInput\" />\n                <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n              </label>\n            </div>\n          </div>\n\n        </div>\n        <div class=\"flex-end\">\n          <button type=\"button\" class=\"resetOption\" data-i18n=\"resetSettings\"></button>\n        </div>\n      </div>\n    </section>\n\n    <!-- 自定义css -->\n    <section id=\"anchorCustomCSS\">\n      <h1 class=\"optionsTitle\" data-i18n=\"customCSS\"></h1>\n      <div class=\"optionBox\">\n        <div class=\"item\">\n          <textarea id=\"css\" save=\"input\" type=\"text\" class=\"width100\" rows=\"10\"></textarea>\n        </div>\n      </div>\n    </section>\n\n    <!-- MQTT Configuration -->\n    <section id=\"anchorMQTT\">\n      <h1 class=\"optionsTitle\">\n        <img src=\"img/mqtt.svg\" style=\"width: 18px\" class=\"mqtt\" />\n        <i18n>MQTT</i18n>\n      </h1>\n      <div class=\"optionBox\" id=\"mqttOption\">\n        <div class=\"list loose\">\n          <div class=\"item\">\n            <div data-i18n=\"enable\"></div>\n            <div class=\"switch\">\n              <label class=\"switchLabel switchRadius\">\n                <input type=\"checkbox\" id=\"mqttEnable\" save=\"click\" class=\"switchInput\" />\n                <span class=\"switchRound switchRadius\"><em class=\"switchRoundBtn switchRadius\"></em></span>\n              </label>\n            </div>\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"mqttBroker\"></div>\n            <input id=\"mqttBroker\" save=\"input\" type=\"text\" class=\"width100\" placeholder=\"test.mosquitto.org\"\n              data-i18n-placeholder=\"mqttBrokerHelp\" />\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"port\"></div>\n            <input id=\"mqttPort\" save=\"input\" type=\"number\" min=\"1\" max=\"65535\" class=\"width100\" placeholder=\"8081\" />\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"mqttPath\"></div>\n            <input id=\"mqttPath\" save=\"input\" type=\"text\" class=\"width100\" placeholder=\"/mqtt\"\n              data-i18n-placeholder=\"mqttPathHelp\" />\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"mqttProtocol\"></div>\n            <div class=\"switch switchSelect\">\n              <select id=\"mqttProtocol\" class=\"select\" save=\"select\">\n                <option value=\"wss\" data-i18n=\"mqttProtocolWss\"></option>\n                <option value=\"ws\" data-i18n=\"mqttProtocolWs\"></option>\n              </select>\n            </div>\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"mqttClientId\"></div>\n            <input id=\"mqttClientId\" save=\"input\" type=\"text\" class=\"width100\" placeholder=\"cat-catch-client\"\n              data-i18n-placeholder=\"mqttClientIdHelp\" />\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"mqttTitleLength\"></div>\n            <input id=\"mqttTitleLength\" save=\"input\" type=\"number\" min=\"1\" max=\"1000\" class=\"width100\" value=\"100\"\n              data-i18n-placeholder=\"mqttTitleLengthHelp\" />\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"mqttUsername\"></div>\n            <input id=\"mqttUser\" save=\"input\" type=\"text\" class=\"width100\"\n              data-i18n-placeholder=\"mqttCredentialsHelp\" />\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"mqttPassword\"></div>\n            <input id=\"mqttPassword\" save=\"input\" type=\"password\" class=\"width100\"\n              data-i18n-placeholder=\"mqttCredentialsHelp\" />\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"mqttTopic\"></div>\n            <input id=\"mqttTopic\" save=\"input\" type=\"text\" class=\"width100\" placeholder=\"cat-catch/media\"\n              data-i18n-placeholder=\"mqttTopicHelp\" />\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"mqttQos\"></div>\n            <div class=\"switch switchSelect\">\n              <select id=\"mqttQos\" class=\"select\" save=\"select\">\n                <option value=\"0\">0 <span data-i18n=\"mqttQos0\"></span></option>\n                <option value=\"1\">1 <span data-i18n=\"mqttQos1\"></span></option>\n                <option value=\"2\">2 <span data-i18n=\"mqttQos2\"></span></option>\n              </select>\n            </div>\n            <div class=\"help-text\" data-i18n=\"mqttQosHelp\"></div>\n          </div>\n\n          <div class=\"item\">\n            <div data-i18n=\"mqttDataFormat\"></div>\n            <textarea id=\"mqttDataFormat\" save=\"input\" class=\"width100\" rows=\"4\"\n              data-i18n-placeholder=\"mqttDataFormatHelp\"></textarea>\n            <div class=\"help-text\">\n              <a href=\"https://o2bmm.gitbook.io/cat-catch/docs/tag\" target=\"_blank\" data-i18n=\"replaceKeywordList\"></a>\n              <span data-i18n=\"mqttDataFormatDefault\"></span>\n            </div>\n          </div>\n\n          <div class=\"flex-end\">\n            <button type=\"button\" class=\"resetOption\" data-i18n=\"resetSettings\"></button>\n          </div>\n        </div>\n      </div>\n    </section>\n\n    <!-- 操作按钮 -->\n    <section id=\"anchorOperation\">\n      <h1 class=\"optionsTitle\" data-i18n=\"operation\"></h1>\n      <div class=\"optionBox\">\n        <div class=\"flex-end\" style=\"justify-content: center\">\n          <input id=\"importOptionsFile\" type=\"file\" class=\"hide\" />\n          <button type=\"button\" id=\"exportOptions\" data-i18n=\"exportSettings\"></button>\n          <button type=\"button\" id=\"importOptions\" data-i18n=\"importConfiguration\"></button>\n          <button type=\"button\" id=\"ClearData\" data-i18n=\"clearCapturedData\"></button>\n          <button type=\"button\" id=\"ResetAllOption\" data-i18n=\"resetAllSettings\"></button>\n          <button type=\"button\" id=\"extensionReload\" data-i18n=\"restartExtension\"></button>\n        </div>\n      </div>\n    </section>\n\n    <!-- 关于 -->\n    <section id=\"anchorAbout\">\n      <h1 class=\"optionsTitle\" data-i18n=\"about\"></h1>\n      <div class=\"optionBox\">\n        <div class=\"item\">\n          <div id=\"version\"></div>\n        </div>\n        <div class=\"item\">\n          <span data-i18n-outer=\"documentation\"></span>:\n          <a href=\"https://o2bmm.gitbook.io/cat-catch/\" target=\"_blank\">https://o2bmm.gitbook.io/cat-catch/</a>\n        </div>\n        <div class=\"item\">\n          Github:\n          <a href=\"https://github.com/xifangczy/cat-catch\" target=\"_blank\">https://github.com/xifangczy/cat-catch</a>\n        </div>\n      </div>\n    </section>\n  </div>\n\n  <script src=\"lib/base64.js\" defer></script>\n  <script src=\"js/options.js\" defer></script>\n  <script src=\"js/i18n.js\" defer></script>\n</body>\n\n</html>\n"
  },
  {
    "path": "popup.html",
    "content": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n\n<head>\n  <meta charset=\"UTF-8\" />\n  <title>catCatch</title>\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <link rel=\"stylesheet\" type=\"text/css\" href=\"css/public.css\" media=\"all\" />\n  <link rel=\"stylesheet\" type=\"text/css\" href=\"css/popup.css\" media=\"all\" />\n  <script src=\"js/init.js\"></script>\n  <script src=\"lib/jquery.min.js\"></script>\n  <script src=\"js/function.js\"></script>\n</head>\n\n<body class=\"popupBody\">\n  <div class=\"Tabs\">\n    <div id=\"currentTab\" class=\"TabButton flex Active\" title=\"Current Page\">\n      <span data-i18n-outer=\"currentPage\"></span>\n      <div id=\"quantity\"></div>\n    </div>\n    <div id=\"allTab\" class=\"TabButton flex\" title=\"Other Pages\">\n      <span data-i18n-outer=\"otherPage\"></span>\n      <div id=\"quantity\"></div>\n    </div>\n    <div id=\"otherTab\" class=\"TabButton\" title=\"Other Features / Media Control\">\n      <span data-i18n-outer=\"otherFeatures\"></span> / <span data-i18n-outer=\"mediaControl\"></span>\n    </div>\n    <div id=\"maybeKeyTab\" class=\"TabButton hide\" title=\"Possible Key\">\n      <span data-i18n-outer=\"possibleKey\"></span>\n    </div>\n  </div>\n  <div id=\"Tips\" data-i18n=\"loadingData\">\n  </div>\n  <div id=\"TipsFixed\">~</div>\n  <div id=\"mediaList\" class=\"container hide TabShow\"></div>\n  <div id=\"allMediaList\" class=\"container hide\"></div>\n  <div id=\"otherOptions\" class=\"container hide\">\n    <div class=\"otherTips\" data-i18n=\"mediaControl\"></div>\n    <div class=\"flexRow\">\n      <b class=\"nowrap\" data-i18n=\"selectWebpage\"></b>\n      <select id=\"videoTabIndex\" title=\"Video Tab\">\n        <option value=\"-1\" id=\"videoTabTips\" data-i18n=\"noMediaDetected\"></option>\n      </select>\n    </div>\n    <div class=\"flexRow\">\n      <b class=\"nowrap\" data-i18n=\"selectMedia\"></b>\n      <select id=\"videoIndex\" title=\"Video\">\n        <option value=\"-1\" id=\"videoIndexTips\" data-i18n=\"noControllableMediaDetected\"></option>\n      </select>\n    </div>\n    <div id=\"PlayControl\" class=\"textColor\">\n      <span data-i18n-outer=\"multiplier\"></span><input id=\"playbackRate\" type=\"number\" value=\"2\" min=\"0.1\" max=\"16\"\n        title=\"Play Back Rate\" step=\"1\" />\n      <button id=\"speed\" class=\"button2\" data-i18n=\"speedPlayback\" title=\"Adjust playback speed\"></button>\n      <button id=\"control\" data-switch=\"play\" data-i18n=\"play\" title=\"Play/Pause media\"></button>\n      <button id=\"pip\" class=\"firefoxHide\" data-i18n=\"pictureInPicture\" title=\"Toggle picture-in-picture mode\"></button>\n      <button id=\"fullScreen\" class=\"firefoxHide\" data-i18n=\"fullscreen\" title=\"Toggle fullscreen mode\"></button>\n      <button id=\"screenshot\" data-switch=\"play\" data-i18n=\"screenshot\"\n        title=\"Take screenshot of current frame\"></button>\n      <label class=\"flexColumn loop\" title=\"Loop\">\n        <div data-i18n=\"loop\"></div><input type=\"checkbox\" id=\"loop\" placeholder=\"Loop\">\n      </label>\n      <label class=\"flexColumn muted\" title=\"Mute\">\n        <div data-i18n=\"mute\"></div><input type=\"checkbox\" id=\"muted\" placeholder=\"Mute\">\n      </label>\n      <div class=\"flexColumn\">\n        <div data-i18n=\"volume\" title=\"Volume\"></div><input type=\"range\" id=\"volume\" class=\"volume\" min=\"0\" max=\"1\"\n          value=\"1\" step=\"0.01\" title=\"Volume Control\" />\n      </div>\n      <div class=\"flexColumn width100\">\n        <div id=\"timeShow\" title=\"Time\"></div><input type=\"range\" id=\"time\" class=\"width100\" min=\"0\" max=\"100\" value=\"0\"\n          step=\"1\" title=\"Time Control\" />\n      </div>\n    </div>\n    <div class=\"line\"></div>\n    <div class=\"otherTips\" data-i18n=\"functionEntry\"></div>\n    <div class=\"otherFeat flexRow\">\n      <button go=\"downloader.html\" class=\"button2\" data-i18n=\"downloader\" title=\"Downloader\"></button>\n      <button go=\"m3u8.html\" class=\"button2\" data-i18n=\"m3u8Parser\" title=\"M3U8 Parser\"></button>\n      <button go=\"mpd.html\" class=\"button2\" data-i18n=\"mpdParser\" title=\"MPD Parser\"></button>\n      <button go=\"json.html\" class=\"button2\" data-i18n=\"jsonFormatter\" title=\"JSON Formatter\"></button>\n      <button go=\"ffmpegURL\" class=\"button2\" title=\"FFmpeg\">FFmpeg</button>\n    </div>\n  </div>\n  <div id=\"maybeKey\" class=\"container hide\"></div>\n  <div id=\"filter\" class=\"more\">\n    <span id=\"regular\" class=\"width100 regular\">\n      <input type=\"text\" data-i18n-placeholder=\"regularFilterPlaceholder\" title=\"Regular Filter\" /><button\n        id=\"regularFilter\" class=\"hide\" data-i18n=\"confirm\" title=\"Confirm\"></button>\n    </span>\n    <div id=\"ext\" title=\"Extensions\"></div>\n    <button id=\"duplicateFilenames\" class=\"button2\" data-i18n=\"deleteDuplicateFilenames\"\n      title=\"Delete Duplicate Filenames\"></button>\n  </div>\n  <div id=\"features\" class=\"more flex-end\">\n    <button id=\"unfoldAll\" data-i18n=\"expandAll\" title=\"Expand All\"></button>\n    <button id=\"unfoldPlay\" data-i18n=\"expandPlayable\" title=\"Expand Playable\"></button>\n    <button id=\"unfoldFilter\" data-i18n=\"expandSelected\" title=\"Expand Selected\"></button>\n    <button id=\"fold\" data-i18n=\"collapseAll\" title=\"Collapse All\"></button>\n    <button id=\"recorder\" class=\"button2 hide firefoxHideScript\" type=\"script\" data-i18n=\"videoRecording\"\n      title=\"Start video recording\"></button>\n    <button id=\"webrtc\" class=\"button2 hide firefoxHideScript\" type=\"script\" data-i18n=\"recordWebRTC\"\n      title=\"Record WebRTC streams\"></button>\n    <button id=\"recorder2\" class=\"button2 hide\" type=\"script\" data-i18n=\"screenCapture\"\n      title=\"Capture screen content\"></button>\n    <button id=\"MobileUserAgent\" class=\"button2 firefoxHideScript\" data-i18n=\"simulateMobile\"\n      title=\"Simulate mobile device\"></button>\n    <button id=\"AutoDown\" class=\"button2\" data-i18n=\"autoDownload\" title=\"Enable automatic downloads\"></button>\n    <button id=\"send2localSelect\" class=\"button2\" data-i18n=\"send2local\"\n      title=\"Send selected items to local storage\"></button>\n  </div>\n  <div id=\"down\">\n    <button id=\"mergeDown\" class=\"button2\" data-i18n=\"onlineMerge\" disabled\n      title=\"Merge selected items for download\"></button>\n    <button id=\"DownFile\" data-i18n=\"download\" title=\"Download selected items\"></button>\n    <button id=\"AllCopy\" data-i18n=\"copy\" title=\"Copy selected items to clipboard\"></button>\n    <button id=\"AllSelect\" data-i18n=\"selectAll\" class=\"hide\" title=\"Select all items\"></button>\n    <button id=\"invertSelection\" data-i18n=\"invertSelection\" title=\"Invert selection\"></button>\n    <button id=\"openFilter\" panel=\"filter\" data-i18n=\"filter\" title=\"Open filter options\"></button>\n    <button id=\"Clear\" data-i18n=\"clear\" title=\"Clear all items\"></button>\n    <button id=\"search\" class=\"button2 hide firefoxHideScript\" type=\"script\" data-i18n=\"deepSearch\"\n      title=\"Perform deep search\"></button>\n    <button id=\"catch\" class=\"button2 hide firefoxHideScript\" type=\"script\" data-i18n=\"cacheCapture\"\n      title=\"Capture cached content\"></button>\n    <button id=\"more\" class=\"button2\" panel=\"features\" data-i18n=\"moreFeatures\" title=\"Show more features\"></button>\n    <button id=\"enable\" class=\"button2\" data-i18n=\"pause\" title=\"Pause/Resume capturing\"></button>\n    <button id=\"Options\" go=\"options.html\" class=\"button2\" data-i18n=\"settings\" title=\"Open settings\"></button>\n    <button id=\"popup\" class=\"button2\" data-i18n=\"popup\" title=\"Open in popup window\"></button>\n    <button id=\"currentPage\" class=\"hide\" data-i18n=\"currentPage\" title=\"Switch to current page\"></button>\n  </div>\n  <!-- <script src=\"js/firefox.js\"></script> -->\n  <script src=\"lib/base64.js\"></script>\n  <script src=\"lib/mqtt.min.js\"></script>\n  <script src=\"js/pupup-utils.js\"></script>\n  <script src=\"js/popup.js\"></script>\n  <script src=\"js/media-control.js\"></script>\n  <script src=\"lib/jquery.qrcode.min.js\"></script>\n  <script src=\"lib/hls.min.js\"></script>\n  <script src=\"js/i18n.js\"></script>\n</body>\n\n</html>"
  },
  {
    "path": "preview.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n    <meta charset=\"UTF-8\" />\n    <title>filter</title>\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"css/public.css\" media=\"all\" />\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"css/preview.css\" media=\"all\" />\n    <script src=\"js/init.js\"></script>\n    <script src=\"js/firefox.js\"></script>\n    <script src=\"js/function.js\"></script>\n</head>\n\n<body>\n    <div class=\"play-container preview-container hide\">\n        <video controls id=\"video-player\"></video>\n    </div>\n    <div class=\"image-container preview-container hide\">\n        <img src=\"\" id=\"image-player\" />\n    </div>\n    <div class=\"container\">\n        <!-- 筛选条件 -->\n        <div class=\"filters\">\n            <div class=\"filter-row\">\n                <span data-i18n=\"suffix\"></span>\n                <div id=\"extensionFilters\"></div>\n            </div>\n\n            <div class=\"filter-row\">\n                <span data-i18n=\"type\"></span>\n                <div id=\"typeFilters\"></div>\n            </div>\n\n            <div class=\"filter-row\">\n                <span data-i18n=\"sort\"></span>\n                <div class=\"sort-options\">\n                    <div class=\"sort-group\">\n                        <label><input type=\"radio\" name=\"sortField\" value=\"getTime\" checked>\n                            <i18n>getTime</i18n>\n                        </label>\n                        <label><input type=\"radio\" name=\"sortField\" value=\"size\">\n                            <i18n>fileSize</i18n>\n                        </label>\n                        <label><input type=\"radio\" name=\"sortField\" value=\"duration\">\n                            <i18n>duration</i18n>\n                        </label>\n                        <!-- <label><input type=\"radio\" name=\"sortField\" value=\"name\">\n                            文件名\n                        </label> -->\n                    </div>\n                    <div class=\"sort-order\">\n                        <label><input type=\"radio\" name=\"sortOrder\" value=\"asc\" data-i18n=\"asc\" checked>\n                            <i18n>asc</i18n>\n                        </label>\n                        <label><input type=\"radio\" name=\"sortOrder\" value=\"desc\" data-i18n=\"desc\">\n                            <i18n>desc</i18n>\n                        </label>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"filter-row\">\n                <span data-i18n=\"option\"></span>\n                <label><input type=\"checkbox\" id=\"showTitle\">\n                    <i18n>title</i18n>\n                </label>\n                <label><input type=\"checkbox\" id=\"deleteDuplicateFilenames\">\n                    <i18n>deleteDuplicateFilenames</i18n>\n                </label>\n            </div>\n\n            <div class=\"filter-row\">\n                <span data-i18n=\"regex\"></span>\n                <input type=\"text\" data-i18n-placeholder=\"regularFilterPlaceholder\" id=\"regular\" />\n            </div>\n\n            <div class=\"filter-row\">\n                <span data-i18n=\"operation\"></span>\n                <button id=\"select-all\" class=\"button\" data-i18n=\"selectAll\"></button>\n                <button id=\"select-reverse\" class=\"button\" data-i18n=\"invertSelection\"></button>\n                <button id=\"download-selected\" class=\"button2\" data-i18n=\"download\" disabled></button>\n                <button id=\"merge-download\" class=\"button2\" data-i18n=\"onlineMerge\" disabled></button>\n                <button id=\"copy-selected\" class=\"button2\" data-i18n=\"copy\" disabled></button>\n                <button id=\"delete-selected\" class=\"button2\" data-i18n=\"delete\" disabled></button>\n                <button id=\"aria2-selected\" class=\"button2 hide\" disabled>aria2</button>\n                <button id=\"send-selected\" class=\"button2 hide\" data-i18n=\"send2local\" disabled></button>\n                <button id=\"clear\" class=\"button\" data-i18n=\"clear\"></button>\n                <button id=\"debug\" class=\"button\">debug</button>\n                <button id=\"search\" class=\"button2\" type=\"script\" data-i18n=\"deepSearch\"></button>\n                <button id=\"catch\" class=\"button2\" type=\"script\" data-i18n=\"cacheCapture\"></button>\n                <label>\n                    <input type=\"checkbox\" name=\"defaultPopup\" id=\"defaultPopup\">\n                    <i18n>defaultPopup</i18n>\n                </label>\n            </div>\n        </div>\n\n\n        <!-- 媒体列表 -->\n        <div id=\"file-container\" class=\"file-grid\"></div>\n        <div id=\"selection-box\"></div>\n\n        <!-- 分页 -->\n        <div class=\"pagination hide\">\n            <button class=\"button\" id=\"prev-page\" disabled>&lt;&lt;</button>\n            <div class=\"page-numbers\"></div>\n            <button class=\"button\" id=\"next-page\" disabled>&gt;&gt;</button>\n        </div>\n    </div>\n\n    <script src=\"lib/base64.js\"></script>\n    <script src=\"lib/hls.min.js\"></script>\n    <script src=\"lib/mqtt.min.js\"></script>\n    <script src=\"js/pupup-utils.js\"></script>\n    <script src=\"js/preview.js\"></script>\n    <script src=\"js/i18n.js\"></script>\n</body>\n\n</html>"
  },
  {
    "path": "tools/sync-locales.js",
    "content": "const fs = require('fs');\nconst path = require('path');\n\n// Paths\nconst localesDir = path.join(__dirname, '../_locales');\nconst enFile = path.join(localesDir, 'en/messages.json');\n\n// Read and parse JSON file\nfunction readJsonFile(filePath) {\n    try {\n        const content = fs.readFileSync(filePath, 'utf8');\n        return JSON.parse(content);\n    } catch (error) {\n        console.error(`Error reading ${filePath}:`, error);\n        return null;\n    }\n}\n\n// Write JSON file with 2-space indentation\nfunction writeJsonFile(filePath, data) {\n    try {\n        const content = JSON.stringify(data, null, 2) + '\\n';\n        fs.writeFileSync(filePath, content, 'utf8');\n        console.log(`Updated: ${filePath}`);\n        return true;\n    } catch (error) {\n        console.error(`Error writing ${filePath}:`, error);\n        return false;\n    }\n}\n\n// Get all locale directories except 'en'\nfunction getLocaleDirs() {\n    return fs.readdirSync(localesDir, { withFileTypes: true })\n        .filter(dirent => dirent.isDirectory() && dirent.name !== 'en')\n        .map(dirent => dirent.name);\n}\n\n// Main function to sync locale files\nfunction syncLocales() {\n    // Read English file as baseline\n    const enMessages = readJsonFile(enFile);\n    if (!enMessages) return;\n\n    // Get all locale directories\n    const locales = getLocaleDirs();\n\n    // Process each locale\n    locales.forEach(locale => {\n        const localeFile = path.join(localesDir, locale, 'messages.json');\n        const localeMessages = readJsonFile(localeFile) || {};\n        const syncedMessages = {};\n        let added = 0;\n        let removed = 0;\n\n        // Create new messages object with English key order\n        Object.keys(enMessages).forEach(key => {\n            if (localeMessages[key]) {\n                // Use existing translation\n                syncedMessages[key] = localeMessages[key];\n            } else {\n                // Add English message as placeholder\n                syncedMessages[key] = enMessages[key];\n                added++;\n            }\n        });\n\n        // Count removed keys (present in locale but not in English)\n        removed = Object.keys(localeMessages).length - (Object.keys(syncedMessages).length - added);\n\n        // Write the synchronized file\n        if (writeJsonFile(localeFile, syncedMessages)) {\n            console.log(`Synced ${locale}: ${added} added, ${removed} removed`);\n        }\n    });\n}\n\n// Run the synchronization\nconsole.log('Starting locale synchronization...');\nsyncLocales();\nconsole.log('Locale synchronization complete!');\n"
  }
]