This commit is contained in:
ChuXun
2025-10-19 20:55:27 +08:00
parent e879ccefb3
commit 53f9554f38
99 changed files with 22308 additions and 2 deletions

2
.gitattributes vendored
View File

@@ -1,2 +0,0 @@
# Auto detect text files and perform LF normalization
* text=auto

3
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
# These are supported funding model platforms
custom: [https://paypal.me/o2bmm]

49
.github/ISSUE_TEMPLATE/bug.yaml vendored Normal file
View File

@@ -0,0 +1,49 @@
name: Bug 报告 Bug Report
description: 创建一个bug报告. File a bug report
body:
- type: input
id: version
attributes:
label: 扩展版本号 extension version
placeholder: e.g. vX.Y.Z
- type: dropdown
id: browser
attributes:
label: 浏览器 browser
options:
- Google Chrome
- Microsoft Edge
- Microsoft Edge (Android)
- Firefox
- Firefox (Android)
- Chromium
- 360浏览器
- 其他基于 Chromium 的浏览器 / Other Chromium-based browsers
validations:
required: true
- type: input
id: browserVersion
attributes:
label: 浏览器版本号 browser version
placeholder: e.g. vX.Y.Z
- type: input
id: url
attributes:
label: 涉及网址 related URL
placeholder: e.g. https://example.com
description: 请提供发生问题的网址 需要授权登陆才能播放的请通过邮箱提交bug
- type: checkboxes
id: checklist
attributes:
label: Checklist
options:
- 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)
required: true
- 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.
required: true
- type: textarea
id: description
attributes:
label: 请详细描述问题 What actually happened?
validations:
required: true

View File

@@ -0,0 +1,10 @@
---
name: 功能 添加/修改/增强 请求
about: 请求一个功能修改或添加
title: ''
labels: ''
assignees: ''
---
** 详细描述想要添 加/修改/增强 的功能 **

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
# 构建产物
build/
dist/
*.crx
*.zip
*.pem

624
CHANGELOG.md Normal file
View File

@@ -0,0 +1,624 @@
## 更新说明
### 2.6.4
[Updated] webrtc 录制脚本 更新
[Updated] 深度搜索脚本 更新
[Updated] 更新日语 感谢@hmaoraze
[Added] 支持 MQTT 协议 感谢@jetsung
[Added] 筛选 删除重复文件名
[Added] 始终打开 深度搜索 选项 (慎用)
[Added] 弹出模式 可选择页面
[Added] 筛选页面 支持时长排序
### 2.6.3
[Fixed] Chromium 114 版本以下缺少 `sidePanel` 功能,导致扩展无法使用
### 2.6.2
[Added] m3u8 解析器 录制失败重试功能 (测试)
[Added] m3u8 解析器 尝试估算文件大小
[Added] 增加 其他设置 `使用侧边栏` 选项。从 popup 模式改为浏览器侧边栏打开扩展 (不支持 firefox)
[Updated] m3u8 预览现在支持 hevc/h265 编码
[Updated] 深度搜索 支持解析 vimeo playlist.json
[Changed] 重构 缓存捕捉 脚本 减少头部数据缺失问题
[Changed] 重构 排除重复的资源 减少资源占用
[Fixed] 缓存捕捉脚本导致视频无法播放问题
[Deleted] m3u8 解析器 删除了旧版本下载器
[Deleted] 启用新弹出页 删除旧弹出页
### 2.6.1
[Changed] 对手机浏览器进行一些适配
### 2.6.0
[Added] 全新的弹出页面(`弹出`按钮) 文件预览/筛选帮助你下载需要的文件 (设置`feat newPopup`关闭新版)
[Changed] 增强数据发送功能,现在能自定义发送数据 感谢 @helson-lin 的支持
[Changed] 正则匹配 现在能获取到请求头
[Changed] 支持夸克浏览器 (部分功能不可用)
[Updated] 深度搜索脚本 找到更多资源
[Fixed] Fifefox 导入功能 bug 导致扩展不可用
[Fixed] 偶尔会弹出多个 ffmpeg 页面的 bug
[Fixed] 下载器 打开`边下边存` 无法自动关闭的 bug
### 2.5.9
[Added] 增加屏蔽网址功能 添加不希望开启扩展的网站 (可设为白名单, 只允许添加网址开启扩展)
[Fixed] 新版下载器 下载大文件时 出错 #610
[Changed] 限制每页面最大储存 9999 条资源
[Changed] 设置增加导航栏
[Changed] 自动下载 允许自定义保存文件名
### 2.5.8
[Changed] 如果资源 url 不存在文件名 尝试使用页面标题作为文件名
### 2.5.7
[Fixed] 自定义保存文件名使用 `/` 无法创建目录
[Changed] firefox 升级 manifest v3
[Changed] firefox 128 以上版本 支持使用深度搜索 缓存录制 等脚本功能
[Fixed] firefox 无法发送到在线 ffmpeg 问题
[Added] 重构 猫抓下载器 如需旧版本请在设置 关闭 `Test version` 选项
[Added] `URL Protocol m3u8dl` `调用程序` 增加下载前确认参数设置
[Added] m3u8 为疑似密钥增加验证密钥功能
[Changed] 增强 深度搜索 现在能找到更多疑似密钥
### 2.5.6
[Fixed] m3u8 解析器 自动关闭 bug #531
[Fixed] chrome 130 自定义 url 新规范导致 `m3u8dl://` 调用失败 #528
[Fixed] m3u8 解析器 文件不正确无法解析 造成死循环占用 CPU 问题
[Changed] 猫抓下载器 添加更多请求头 增加下载成功率
### 2.5.5
[Fixed] 修复一个严重 bug #483
[Added] 在线 ffmpeg 提供服务器选择
[Fixed] m3u8 解析器 文件名存在`|`字符 无法下载问题
[Changed] 发送数据 提供完整请求头
### 2.5.4
[Added] m3u8DL 增加切换 RE 版本 (RE 版 需[URLProtocol](https://github.com/xifangczy/URLProtocol))
[Added] 录制相关脚本 增加码率设置
[Fixed] 深度搜索 脚本错误导致无法使用
[Fixed] m3u8 解析器录制直播 录制时间显示错误
### 2.5.3
[Added] 增加`弹出`模式 (以新窗口打开资源列表页面)
[Added] 增加`调用本地程序`设置, 程序没有调用协议, 可以使用[URLProtocol](https://github.com/xifangczy/URLProtocol)帮助程序注册调用协议。具体使用方法查看 [调用本地协议](https://o2bmm.gitbook.io/cat-catch/docs/invoke)
[Added] 下载器 增加`边下边存`选项 可以用来下载一些直播视频链接
[Added] 现在使用`深度搜索` 或其他脚本得到的疑似密钥, 直接显示在 popup 页面 `疑似密钥` 标签内。
[Added] 增加 葡萄牙语
[Changed] 重写 `录制webRTC` 脚本
[Changed] `m3u8解析器` `下载器`页面内更改设置不会被储存。所有设置更改统一到扩展设置页面。
[Changed] storage.local 更改为 storage.session 以减少 IO 错误导致扩展无法使用.(要求 chrome 104 以上)
[Changed] 优化与 ffmpeg 网页端的通信, 避免多任务时的数据错乱。
(请提前打开 [在线 ffmpeg](https://ffmpeg.bmmmd.com/) ctrl+f5 刷新页面 避免页面缓存造成的问题)
[Changed] 稍微增大一些按钮图标 不再训练大家的鼠标精准度 🙄...如果你不喜欢想还原 设置-自定义 css 填入 `body{font-size:12px;width:550px;}.icon,.favicon{width:18px;height:18px;}.DownCheck{width:15px;height:15px;}`
### 2.5.2
[Added] 添加测试功能 数据发送 嗅探数据和密钥发送到指定地址
[Added] 替换标签 增加 `${origin}`
[Added] 显示 图标数字角标 开关
[Fixed] 猫抓下载器 小部分网站需要指定 range
[Fixed] 修复 标题作为文件名 文件名含有非法字符问题 #339
### 2.5.1
[Added] 多语言 增加繁体中文
[Fixed] 修复 深度搜索 死循环 bug
[Fixed] 兼容低版本 chromium 缺少 API 导致扩展无法使用
[Changed] popup 页面 现在能合并两个 m3u8 文件
### 2.5.0
[Added] 多语言支持
[Changed] m3u8 解析 新下载器 性能优化
[Fixed] 视频捕捉 不使用`从头捕获`也会丢掉头部数据的问题
[Changed] 深度搜索 现在能找到更多密钥
### 2.4.9
[Fixed] `$url$` 标签 修复(自动更新成`${url}`) #281
[Fixed] 修复 加密 m3u8 存在 EXT-X-MAP 标签,解密会失败的 bug
[Added] 设置页面 添加自动合并 m3u8 选项 #286 (测试)
[Added] 增加录制 webRTC 流脚本 更多功能-录制 webRTC (测试)
### 2.4.8
[Fixed] 修复 m3u8 新下载器 ${referer} 标签问题 #272
[Fixed] 修复 m3u8 新下载器 全部重新下载 bug #274
[Fixed] 修复 m3u8 新下载器 下载失败丢失线程 #276
[Fixed] 修复 m3u8 新下载器 勾选 ffmpeg 转码 下载超过 2G 大小 不会强制下载
[Changed] 完善 Aria2 Rpc 协议 增加密钥 和 cookie 支持
[Added] 增加${cookie}标签 如果资源存在 cookie
### 2.4.7
[Fixed] 缓存捕获 延迟获取标题 #241
[Fixed] 特殊字符造成无法下载的问题 #253
[Fixed] m3u8 解析器 没有解析出全部嵌套 m3u8 的 bug #265
[Added] firefox 增加 privacy 协议页面 第一次安装显示
[Added] 增加 Aria2 Rpc 协议下载 感谢 @aar0u
[Changed] 重写录制脚本
[Changed] 增强深度搜索
[Changed] m3u8 解析器 现在可以自定义头属性
[Changed] m3u8 解析器 最大下载线程调整为 6
[Changed] m3u8 解析器 默认开启新下载器
### 2.4.6
[Fixed] 缓存捕获 多个视频问题 #239
[Changed] 更新 mux m3u8-decrypt mpd-parser 版本
[Changed] 设置 刷新跳转清空当前标签抓取的数据 现在可以调节模式
[Changed] firefox 版本要求 113+
[test] m3u8 解析器 增加测试项 `重构的下载器`
### 2.4.5
[Changed] 增强 深度搜索 解决"一次性"m3u8
[Changed] m3u8 解析器 下载范围允许填写时间格式 HH:MM:SS
[Added] 增加 缓存捕获 从头捕获、正则提取文件名、手动填写文件名
[Added] 增加 设置 正则匹配 屏蔽资源功能
[Added] 增加 下载器 后台打开页面设置
[deleted] 删除 "幽灵资源" 设定 不确定来源的资源归于当前标签
[Fixed] 修复 缓存捕获 清理缓存
[Fixed] 修复 正则匹配 有时会匹配失效(lastIndex 没有复位)
[Fixed] 修复 媒体控制 有时检测不到媒体
[Fixed] 修复 重置所有设置 丢失配置
[Fixed] 修复 firefox 兼容问题
### 2.4.4
[Changed] 增强 深度搜索
[Fixed] m3u8 解析器 无限触发错误的 bug
### 2.4.3
[Fixed] 修复 缓存捕获 获取文件名为空
[Changed] 增强 深度搜索 可以搜到更多密钥
[Changed] 增强 注入脚本 现在会注入到所有 iframe
[Changed] 删除 youtube 支持 可以使用缓存捕捉
### 2.4.2
[Added] 设置页面增加 排除重复的资源 选项
[Added] popup 增加暂停抓取按钮
[Changed] 超过 500 条资源 popup 可以中断加载
[Changed] 调整默认配置 默认不启用 ts 文件 删除多余正则
[Changed] 正则匹配的性能优化
[Fixed] 修复 m3u8 解析器录制功能 直播结束导致自动刷新页面丢失已下载数据的问题
[Fixed] 修复 m3u8 解析器边下边存和 mp4 转码一起使用 编码不正确的 bug
[Fixed] 修复 扩展重新启动后 造成的死循环
### 2.4.1
[Added] 捕获脚本 现在可以通过表达式获取文件名
[Changed] 删除 打开自动下载的烦人提示
[Changed] 优化 firefox 下 资源严重占用问题
[Fixed] 猫抓下载器 不再限制 2G 文件大小 #179
### 2.4.0
[Added] 加入自定义 css
[Added] 音频 视频 一键合并
[Added] popup 页面正则筛选
[Added] 自定义快捷键支持
[Added] popup 页面支持正则筛选
[Added] m3u8 碎片文件自定义参数
[Changed] 筛选 现在能隐藏不要的数据 而不是取消勾选
[Changed] 重写优化 popup 大部分代码
[Changed] 重写初始化部分代码
[Changed] m3u8 解析器 默认设置改为 ffmpeg 转码 而不是 mp4 转码
[Changed] 删除 调试模式
[Fixed] 深度搜索 深度判断的 bug
[Fixed] 很多 bug
### 2.3.3
[Changed] 解析器 m3u8DL 默认不载入设置参数 #149
[Changed] 可以同时打开多个捕获脚本
[Changed] popup 页面 css 细节调整 #156
[Fixed] 清空不会删除角标的 bug
[Fixed] 替换标签中 参数内包含 "|" 字符处理不正确的 bug
### 2.3.2
[Changed] 设置 增加自定义文件名 删除标题正则提取
[Added] 支持深色模式 #134
[Added] popup 增加筛选
[Fixed] 修复非加密的 m3u8 无法自定义密钥下载
[Fixed] mp4 转码删除 创建媒体日期 属性 #142
### 2.3.1
[Added] 新的替换标签
[Changed] 边下边存 支持 mp4 转码
[Fixed] 修复 BUG #123 #117 #114 #124
### 2.3.0
[Added] m3u8 解析器 边下边存
[Added] m3u8 解析器 在线 ffmpeg 转码
[Fixed] 特殊文件名 下载所选无法下载
[Fixed] m3u8 解析器 某些情况无法下载文件
[Fixed] Header 属性提取失败
[Fixed] 添加抓取类型出错 #109
[Changed] 修改 标题修剪 默认配置
### 2.2.9
[Fixed] 修复 m3u8DL 调用命令范围参数 --downloadRange 不正确
[Added] 正则修剪标题 [#90](https://github.com/xifangczy/cat-catch/issues/94)
[Added] 下载前选择保存目录 选项
[Fixed] m3u8 解析器 部分情况无法下载 ts 文件
[Changed] `复制所选`按钮 现在能被 `复制选项`设置影响
### 2.2.8
[Changed] m3u8 解析器现在会记忆你设定的参数
[Changed] 幽灵数据 更改为 其他页面(幽灵数据同样归类其他页面)
[Changed] popup 页面的性能优化
[Changed] 增加 始终不启用下载器 选项
[Fixed] 修复 使用第三方下载器猫抓下载器也会被调用
### 2.2.7
[Fixed] 修正 文件大小显示不正确
[Changed] 性能优化
[Fixed] 修复 没有正确清理冗余数据 导致 CPU 占用问题
### 2.2.6
[Added] 深度搜索 尝试收集 m3u8 文件的密钥 具体使用查看 [用户文档](https://o2bmm.gitbook.io/cat-catch/docs/m3u8parse#maybekey)
[Added] popup 资源详情增加二维码按钮
[Added] m3u8 解析器 自定义文件名 只要音频 另存为 m3u8DL 命令完善 部分代码来自 [#80](https://github.com/xifangczy/cat-catch/pull/80)
[Added] 非 Chrome 扩展商店版本 现在支持 Youtube
[Added] Firefox 版 现在支持 m3u8 视频预览
[Fixed] m3u8 解析器 超长名字无法保存文件 [#80](https://github.com/xifangczy/cat-catch/pull/80)
[Fixed] 修正 媒体控制 某些情况检测不到视频
### 2.2.5
[Fixed] 修复 mpd 解析器丢失音轨 [#70](https://github.com/xifangczy/cat-catch/issues/70)
[Changed] 优化在网络状况不佳下的直播 m3u8 录制
[Changed] 更新 深度搜索 search.js 进一步增加分析能力
[Changed] 减少 mp4 转码时内存占用
[Changed] 自定义调用本地播放器的协议
### 2.2.4
[Changed] 更新 hls.js
[Changed] m3u8 文件现在能显示更多媒体信息
[Added] 增加 Dash mpd 文件解析
[Added] 增加 深度搜索 脚本
[Fixed] 修复 捕获按钮偶尔失效
### 2.2.3
[Added] m3u8 解析器增加录制直播
[Added] m3u8 解析器增加处理 EXT-X-MAP 标签
[Added] 新增捕获脚本 recorder2.js 需要 Chromium 104 以上版本
[Added] 增加选项 刷新、跳转到新页面 清空当前标签抓取的数据
[Fixed] 修正 m3u8 解析器使用 mp4 转码生成的文件,媒体时长信息不正确
### 2.2.2
[Changed] m3u8 解析器使用 hls.js 替代,多项改进,自定义功能添加
[Changed] 分离下载器和 m3u8 解析器
[Fixed] 修复 m3u8 解析器`调用N_m3u8DL-CLI下载`按钮失效
[Fixed] 修复幽灵数据随机丢失问题
[Fixed] 修复 m3u8 解析器 key 下载器在某些时候无法下载的问题
### 2.2.1
[Fixed] 修复浏览器字体过大,按钮遮挡资源列表的问题。
[Fixed] 调整关键词替换
[Fixed] 修复 Firefox download API 无法下载 data URL 问题
[Changed] m3u8 解析器多个 KEY 显示问题
[Changed] 视频控制现在可以控制其他页面的视频
[Changed] 视频控制现在可以对视频截图
[Changed] 自定义复制选项增加 其他文件 选项
[Added] m3u8 解析器现在可以转换成 mp4 格式
### 2.2.0
[Fixed] 修复文件名出现 "~" 符号 导致 chrome API 无法下载
[Fixed] 修复 Firefox 中 popup 页面下载按钮被滚动条遮挡
[Fixed] 储存路劲有中文时 m3u8dl 协议调用错误
[Changed] 增加/删除一些默认配置
[Added] 增加操控当前网页视频功能
[Added] 增加自定义复制选项
### 2.1.2
[Changed] 细节调整
### 2.1.1
[Changed] 调整正则匹配 现在能提取多个网址
[Fixed] 修复选择脚本在 m3u8 解析器里不起作用 并提高安全性
[Fixed] m3u8 解析器在 Firefox 中不能正常播放 m3u8 视频
[Fixed] 修复 Firefox 中手机端模拟无法还原的问题
[Fixed] 修复初始化错误 BUG 导致扩展失效
### 2.1.0
[Changed] 新增 referer 获取 不存在再使用 initiator 或者直接使用 url
[Changed] 重新支持 Firefox 需要 93 版本以上
[Changed] chromium 内核的浏览器最低要求降为 93 小部分功能需要 102 版本以上,低版本会隐藏功能按钮
[Fixed] 部分 m3u8 key 文件解析错误问题
[Fixed] 修复 保存文件名使用网页标题 选项在 m3u8 解析器里不起作用
### 2.0.0
[Changed] 模拟手机端,现在会修改 navigator.userAgent 变量
[Added] 视频捕获功能,解决被动嗅探无法下载视频的问题
[Added] 视频录制功能,解决被动嗅探无法下载视频的问题
[Added] 支持 N_m3u8DL-CLI 的 m3u8dl://协议
[Added] m3u8 解析器增强,现在能在线合并下载 m3u8 文件
[Added] popup 页面无法下载的视频,会交给 m3u8 解析器修改 Referer 下载
[Added] popup 页面和 m3u8 页面可以在线预览 m3u8
[Added] json 查看工具,和 m3u8 解析器一样在 popup 页面显示图标进入
[Fixed] 无数 BUG
[Fixed] 解决 1.0.17 以来会丢失数据的问题
[Fixed] 该死的 Service Worker... 现在后台被杀死能立刻唤醒自己... 继续用肮脏的手段对抗 Manifest V3
### 1.0.26
[Fixed] 解决关闭网页不能正确删除当前页面储存的数据问题
### 1.0.25
[Changed] 正则匹配增强
[Changed] Heart Beat
[Added] 手机端模拟,手机环境下有更多资源可以被下载。
[Added] 自动下载
### 1.0.24
[Added] 导入/导出配置
[Added] Heart Beat 解决 Service Worker 休眠问题
[Added] firefox.js 兼容层 并上架 Firefox
### 1.0.23
[Added] 正则匹配
### 1.0.22
[Fixed] 一个严重 BUG导致 Service Worker 无法使用 \*
### 1.0.21
[Added] 自定义抓取类型
[Refactor] 设置页面新界面
### 1.0.20
[Added] 抓取 image/\*类型文件选项
### 1.0.19
[Fixed] 重构导致的许多 BUG \*
### 1.0.18
[Added] 抓取 application/octet-stream 选项
[Refactor] 重构剩余代码
### 1.0.17
[Refactor] Manifest 更新到 V3 部分代码
[Added] 使用 PotPlayer 预览媒体

674
LICENSE Normal file
View File

@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

68
README.md Normal file
View File

@@ -0,0 +1,68 @@
<p align="center"> [中文] | [<a href="README_en.md">English</a>]</p>
# 📑简介
猫抓(cat-catch) 资源嗅探扩展,能够帮你筛选列出当前页面的资源。
# 📖安装地址
## 🐴Chrome
https://chrome.google.com/webstore/detail/jfedfbgedapdagkghmgibemcoggfppbb
## 🦄Edge
https://microsoftedge.microsoft.com/addons/detail/oohmdefbjalncfplafanlagojlakmjci
## 🦊Firefox
https://addons.mozilla.org/addon/cat-catch/ 😂需非国区IP访问
## 📱Edge Android
<img src="https://raw.githubusercontent.com/xifangczy/cat-catch/master/README/edgeqrcode.png" width="20%" />
💔猫抓是开源的任何人都可以下载修改上架到应用商店已经有不少加上广告代码后上架的伪猫抓请注意自己的数据安全。所有安装地址以github和用户文档为准。
# 📒用户文档
https://cat-catch.bmmmd.com/
# 🌏翻译
[![gitlocalized ](https://gitlocalize.com/repo/9392/whole_project/badge.svg)](https://gitlocalize.com/repo/9392?utm_source=badge)
# 📘安装方法
## 应用商店安装
通过安装地址的链接到官方扩展商店即可安装。
## 源码安装
1. Git Clone 代码。
2. 扩展管理页面 打开 "开发者模式"。
3. 点击 "加载已解压的扩展程序" 选中扩展文件夹即可。
## crx安装
1. [Releases](https://github.com/xifangczy/cat-catch/releases) **右键另存为**下载crx文件。
2. 扩展管理页面 打开 "开发者模式"。
3. 将crx文件拖入扩展程序页面即可。
# 📚兼容性说明
1.0.17版本之后需要Chromium内核版本93以上。
低于93请使用1.0.16版本。
要体验完整功能请使用104版本以上。
# 🔍界面
![popup界面](https://raw.githubusercontent.com/xifangczy/cat-catch/master/README/popup.png)
![m3u8解析器界面](https://raw.githubusercontent.com/xifangczy/cat-catch/master/README/m3u8.png)
# 🤚🏻免责
本扩展仅供下载用户拥有版权或已获授权的视频,禁止用于下载受版权保护且未经授权的内容。用户需自行承担使用本工具的全部法律责任,开发者不对用户的任何行为负责。本工具按“原样”提供,开发者不承担任何直接或间接责任。
# 🔒隐私政策
本扩展收集所有信息都在本地储存处理,不会发送到远程服务器,不包含任何跟踪器。
# 💖鸣谢
- [hls.js](https://github.com/video-dev/hls.js)
- [jQuery](https://github.com/jquery/jquery)
- [mux.js](https://github.com/videojs/mux.js)
- [js-base64](https://github.com/dankogai/js-base64)
- [jquery.json-viewer](https://github.com/abodelot/jquery.json-viewer)
- [Momo707577045](https://github.com/Momo707577045)
- [mpd-parser](https://github.com/videojs/mpd-parser)
- [StreamSaver.js](https://github.com/jimmywarting/StreamSaver.js)
# 📜License
GPL-3.0 license
1.0版 使用 MIT许可
2.0版 更改为GPL v3许可
为了资源嗅探扩展有良好发展,希望使用猫抓源码的扩展仍然保持开源。

BIN
README/edgeqrcode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
README/m3u8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
README/popup.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

68
README_en.md Normal file
View File

@@ -0,0 +1,68 @@
<p align="center"> [<a href="README.md">中文</a>] | [English]</p>
# 📑Introduction
Cat-Catch is a resource sniffing extension that can help you filter and list the resources of the current page.
# 📖Installation
## 🐴Chrome
https://chrome.google.com/webstore/detail/jfedfbgedapdagkghmgibemcoggfppbb
## 🦄Edge
https://microsoftedge.microsoft.com/addons/detail/oohmdefbjalncfplafanlagojlakmjci
## 🦊Firefox
https://addons.mozilla.org/addon/cat-catch/ 😂Non-China IP required for access
## 📱Edge Android
<img src="https://raw.githubusercontent.com/xifangczy/cat-catch/master/README/edgeqrcode.png" width="20%" />
💔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.
# 📒Documentation
https://cat-catch.bmmmd.com/
# 🌏Translations
[![gitlocalized ](https://gitlocalize.com/repo/9392/whole_project/badge.svg)](https://gitlocalize.com/repo/9392?utm_source=badge)
# 📘 Installation Methods
## App Store Installation
Install directly from the official extension store using the link provided.
## Source Code Installation
1. Git clone the repository.
2. Open the extensions management page and enable "Developer Mode."
3. Click "Load unpacked" and select the extension folder.
## CRX Installation
1. **Right-click** and save the CRX file from [Releases](https://github.com/xifangczy/cat-catch/releases).
2. Open the extensions management page and enable "Developer Mode."
3. Drag the CRX file into the extensions page.
# 📚Compatibility
After version 1.0.17, Chromium kernel version 93 or above is required.
Use version 1.0.16 if below 93.
For full functionality, use version 104 or above.
# 🔍Screenshot
![popup Screenshot](https://raw.githubusercontent.com/xifangczy/cat-catch/master/README/popup.png)
![m3u8 parser Screenshot](https://raw.githubusercontent.com/xifangczy/cat-catch/master/README/m3u8.png)
# 🤚🏻Disclaimer
This 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.
# 🔒Privacy Policy
The extension collects and processes all information locally without sending it to remote servers and does not include any trackers.
# 💖Acknowledgements
- [hls.js](https://github.com/video-dev/hls.js)
- [jQuery](https://github.com/jquery/jquery)
- [mux.js](https://github.com/videojs/mux.js)
- [js-base64](https://github.com/dankogai/js-base64)
- [jquery.json-viewer](https://github.com/abodelot/jquery.json-viewer)
- [Momo707577045](https://github.com/Momo707577045)
- [mpd-parser](https://github.com/videojs/mpd-parser)
- [StreamSaver.js](https://github.com/jimmywarting/StreamSaver.js)
# 📜License
GPL-3.0 license
Version 1.0 uses the MIT license.
Version 2.0 has changed to the GPL v3 license.
In 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.

1001
_locales/en/messages.json Normal file

File diff suppressed because it is too large Load Diff

1001
_locales/ja/messages.json Normal file

File diff suppressed because it is too large Load Diff

1001
_locales/pt_BR/messages.json Normal file

File diff suppressed because it is too large Load Diff

1001
_locales/zh_CN/messages.json Normal file

File diff suppressed because it is too large Load Diff

1001
_locales/zh_TW/messages.json Normal file

File diff suppressed because it is too large Load Diff

921
catch-script/catch.js Normal file
View File

@@ -0,0 +1,921 @@
(function () {
class CatCatcher {
constructor() {
console.log("catch.js Start");
// 初始化属性
this.enable = true; // 捕获开关
this.language = navigator.language; // 语言设置
this.isComplete = false; // 捕获完成标志
this.catchMedia = []; // 捕获的媒体数据
this.mediaSize = 0; // 捕获的媒体数据大小
this.setFileName = null; // 文件名
this.catCatch = null; // UI元素
// 移动面板相关属性
this.x = 0;
this.y = 0;
// 初始化语言
if (window.CatCatchI18n) {
if (!window.CatCatchI18n.languages.includes(this.language)) {
this.language = this.language.split("-")[0];
if (!window.CatCatchI18n.languages.includes(this.language)) {
this.language = "en";
}
}
}
// 初始化组件
// 删除iframe sandbox属性 避免 issues #576
this.setupIframeProcessing();
// 初始化 Trusted Types
this.initTrustedTypes();
// 创建和设置UI
this.createUI();
// 代理MediaSource方法
this.proxyMediaSourceMethods();
// 自动跳转到缓冲尾
if (localStorage.getItem("CatCatchCatch_autoToBuffered") == "checked") {
const autoToBufferedInterval = setInterval(() => {
const videos = document.querySelectorAll('video');
if (videos.length > 0 && Array.from(videos).some(video => !video.paused && video.readyState > 2)) {
const autoToBufferedElement = this.catCatch.querySelector("#autoToBuffered");
if (autoToBufferedElement) {
autoToBufferedElement.click();
clearInterval(autoToBufferedInterval);
}
}
}, 1000);
}
}
/**
* 设置iframe处理删除sandbox属性
* 解决 issues #576
*/
setupIframeProcessing() {
document.addEventListener('DOMContentLoaded', () => {
const processIframe = (iframe) => {
if (iframe && iframe.hasAttribute && iframe.hasAttribute('sandbox')) {
const clonedIframe = iframe.cloneNode(true);
clonedIframe.removeAttribute('sandbox');
if (iframe.parentNode) {
iframe.parentNode.replaceChild(clonedIframe, iframe);
}
}
};
document.querySelectorAll('iframe').forEach(processIframe);
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.nodeName === 'IFRAME') {
processIframe(node);
} else if (node.querySelectorAll) {
node.querySelectorAll('iframe').forEach(processIframe);
}
});
}
}
});
observer.observe(document.body || document.documentElement, { childList: true, subtree: true });
});
}
/**
* 初始化 Trusted Types
*/
initTrustedTypes() {
let createHTML = (string) => {
try {
const fakeDiv = document.createElement('div');
fakeDiv.innerHTML = string;
createHTML = (string) => string;
} catch (e) {
if (typeof trustedTypes !== 'undefined') {
const policy = trustedTypes.createPolicy('catCatchPolicy', { createHTML: (s) => s });
createHTML = (string) => policy.createHTML(string);
const _innerHTML = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML');
Object.defineProperty(Element.prototype, 'innerHTML', {
set: function (value) {
_innerHTML.set.call(this, createHTML(value));
}
});
} else {
console.warn("trustedTypes不可用跳过安全策略设置");
}
}
};
createHTML("<div></div>");
}
/**
* 创建UI元素
*/
createUI() {
const buttonStyle = 'style="border:solid 1px #000;margin:2px;padding:2px;background:#fff;border-radius:4px;border:solid 1px #c7c7c780;color:#000;"';
const checkboxStyle = 'style="-webkit-appearance: auto;"';
this.catCatch = document.createElement("div");
this.catCatch.setAttribute("id", "CatCatchCatch");
const style = `
display: flex;
flex-direction: column;
align-items: flex-start;`;
this.catCatch.innerHTML = `<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYBAMAAAASWSDLAAAAKlBMVEUAAADLlROxbBlRAD16GS5oAjWWQiOCIytgADidUx/95gHqwwTx0gDZqwT6kfLuAAAACnRSTlMA/vUejV7kuzi8za0PswAAANpJREFUGNNjwA1YSxkYTEqhnKZLLi6F1w0gnKA1shdvHYNxdq1atWobjLMKCOAyC3etlVrUAOH4HtNZmLgoAMKpXX37zO1FwcZAwMDguGq1zKpFmTNnzqx0Bpp2WvrU7ttn9py+I8JgLn1R8Pad22vurNkjwsBReHv33junzuyRnOnMwNCSeFH27K5dq1SNgcZxFMnuWrNq1W5VkNntihdv7ToteGcT0C7mIkE1qbWCYjJnM4CqEoWKdoslChXuUgXJqIcLebiphSgCZRhaPDhcDFhdmUMCGIgEAFA+Uc02aZg9AAAAAElFTkSuQmCC" style="-webkit-user-drag: none;width: 20px;">
<div id="catCatch" style="${style}">
<div id="tips"></div>
<button id="download" ${buttonStyle} data-i18n="downloadCapturedData">下载已捕获的数据</button>
<button id="clean" ${buttonStyle} data-i18n="deleteCapturedData">删除已捕获数据</button>
<div><button id="hide" ${buttonStyle} data-i18n="hide">隐藏</button><button id="close" ${buttonStyle} data-i18n="close">关闭</button></div>
<label><input type="checkbox" id="autoDown" ${localStorage.getItem("CatCatchCatch_autoDown") || ""} ${checkboxStyle}><span data-i18n="automaticDownload">完成捕获自动下载</span></label>
<label><input type="checkbox" id="ffmpeg" ${localStorage.getItem("CatCatchCatch_ffmpeg") || ""} ${checkboxStyle}><span data-i18n="ffmpeg">使用ffmpeg合并</span></label>
<label><input type="checkbox" id="autoToBuffered" ${checkboxStyle}><span data-i18n="autoToBuffered">自动跳转缓冲尾</span></label>
<label><input type="checkbox" id="checkHead" ${checkboxStyle}>清理多余头部数据</label>
<label><input type="checkbox" id="completeClearCache" ${localStorage.getItem("CatCatchCatch_completeClearCache") || ""} ${checkboxStyle}>下载完成后清空数据</label>
<details>
<summary data-i18n="fileName" id="summary">文件名设置</summary>
<div style="font-weight:bold;"><span data-i18n="fileName">文件名</span>: </div><div id="fileName"></div>
<div style="font-weight:bold;"><span data-i18n="selector">表达式</span>: </div><div id="selector">Null</div>
<div style="font-weight:bold;"><span data-i18n="regular">正则</span>: </div><div id="regular">Null</div>
<button id="setSelector" ${buttonStyle} data-i18n="usingSelector">表达式提取</button>
<button id="setRegular" ${buttonStyle} data-i18n="usingRegular">正则提取</button>
<button id="setFileName" ${buttonStyle} data-i18n="customize">手动填写</button>
</details>
<details>
<summary>test</summary>
<button id="test" ${buttonStyle}>test</button>
<button id="restart" ${buttonStyle} data-i18n="capturedBeginning">从头捕获</button>
<label><input type="checkbox" id="restartAlways" ${localStorage.getItem("CatCatchCatch_restart") || ""} ${checkboxStyle}><span data-i18n="alwaysCapturedBeginning">始终从头捕获</span>(beta)</label>
</details>
</div>`;
this.catCatch.style = `
position: fixed;
z-index: 999999;
top: 10%;
left: 90%;
background: rgb(255 255 255 / 85%);
border: solid 1px #c7c7c7;
border-radius: 4px;
color: rgb(26, 115, 232);
padding: 5px 5px 5px 5px;
font-size: 12px;
font-family: "Microsoft YaHei", "Helvetica", "Arial", sans-serif;
user-select: none;`;
// 创建 Shadow DOM
this.createShadowRoot();
// 初始化UI元素引用
this.tips = this.catCatch.querySelector("#tips");
this.fileName = this.catCatch.querySelector("#fileName");
this.selector = this.catCatch.querySelector("#selector");
this.regular = this.catCatch.querySelector("#regular");
if (!this.tips || !this.fileName || !this.selector || !this.regular) {
console.error("UI元素初始化失败找不到必要的DOM元素");
}
// 初始化显示
this.tips.innerHTML = this.i18n("waiting", "等待视频播放");
this.selector.innerHTML = localStorage.getItem("CatCatchCatch_selector") ?? "Null";
this.regular.innerHTML = localStorage.getItem("CatCatchCatch_regular") ?? "Null";
// 绑定事件
this.bindEvents();
// 自动从头捕获设置
if (localStorage.getItem("CatCatchCatch_restart") == "checked") {
this.setupAutoRestart();
}
}
/**
* 创建 Shadow DOM
* 解决 issues #693 安全使用attachShadow 从iframe中获取原生方法
*/
createShadowRoot() {
try {
// 解决 issues #693 安全使用attachShadow 从iframe中获取原生方法
const createSecureShadowRoot = (element, mode = 'closed') => {
const getPristineAttachShadow = () => {
try {
const iframe = document.createElement('iframe');
const parentNode = document.body || document.documentElement;
parentNode.appendChild(iframe);
const pristineMethod = iframe.contentDocument.createElement('div').attachShadow;
iframe.remove();
if (pristineMethod) return pristineMethod;
} catch (e) {
console.log("获取原生attachShadow方法失败:", e);
}
return Element.prototype.attachShadow;
};
const executor = Element.prototype.attachShadow.toString().includes('[native code]')
? Element.prototype.attachShadow.bind(element)
: getPristineAttachShadow().bind(element);
try {
return executor({ mode });
} catch (e) {
console.error('Shadow DOM 创建失败:', e);
// 应急处理:降级方案
return document.createElement('div');
}
};
// 创建 Shadow DOM 放入CatCatch
const divShadow = document.createElement('div');
const shadowRoot = createSecureShadowRoot(divShadow);
shadowRoot.appendChild(this.catCatch);
// 页面插入Shadow DOM
const htmlElement = document.getElementsByTagName('html')[0];
if (htmlElement) {
htmlElement.appendChild(divShadow);
} else {
document.appendChild(divShadow);
}
} catch (error) {
console.error("创建Shadow DOM失败:", error);
// 降级方案直接添加到body
try {
const body = document.body || document.documentElement;
body.appendChild(this.catCatch);
} catch (e) {
console.error("降级添加UI也失败:", e);
}
}
}
/**
* 绑定事件处理函数
*/
bindEvents() {
// 移动面板相关事件
this.catCatch.addEventListener('mousedown', this.handleDragStart.bind(this));
// 设置选项相关事件
const autoDown = this.catCatch.querySelector("#autoDown");
if (autoDown) autoDown.addEventListener('change', this.handleAutoDownChange.bind(this));
const ffmpeg = this.catCatch.querySelector("#ffmpeg");
if (ffmpeg) ffmpeg.addEventListener('change', this.handleFfmpegChange.bind(this));
const restartAlways = this.catCatch.querySelector("#restartAlways");
if (restartAlways) restartAlways.addEventListener('change', this.handleRestartAlwaysChange.bind(this));
// 按钮相关事件
const clean = this.catCatch.querySelector("#clean");
if (clean) clean.addEventListener('click', this.handleClean.bind(this));
const download = this.catCatch.querySelector("#download");
if (download) download.addEventListener('click', this.handleDownload.bind(this));
const hide = this.catCatch.querySelector("#hide");
if (hide) hide.addEventListener('click', this.handleHide.bind(this));
const img = this.catCatch.querySelector("img");
if (img) img.addEventListener('click', this.handleHide.bind(this));
const close = this.catCatch.querySelector("#close");
if (close) close.addEventListener('click', this.handleClose.bind(this));
const restart = this.catCatch.querySelector("#restart");
if (restart) restart.addEventListener('click', this.handleRestart.bind(this));
const setFileName = this.catCatch.querySelector("#setFileName");
if (setFileName) setFileName.addEventListener('click', this.handleSetFileName.bind(this));
const test = this.catCatch.querySelector("#test");
if (test) test.addEventListener('click', this.handleTest.bind(this));
const summary = this.catCatch.querySelector("#summary");
if (summary) summary.addEventListener('click', this.getFileName.bind(this));
const completeClearCache = this.catCatch.querySelector("#completeClearCache");
if (completeClearCache) completeClearCache.addEventListener('click', this.handleCompleteClearCache.bind(this));
// 自动跳转到缓冲节点
// this.autoToBufferedFlag = true;
const autoToBuffered = this.catCatch.querySelector("#autoToBuffered");
if (autoToBuffered) autoToBuffered.addEventListener('click', this.handleAutoToBuffered.bind(this));
// 文件名设置相关事件
const setSelector = this.catCatch.querySelector("#setSelector");
if (setSelector) setSelector.addEventListener('click', this.handleSetSelector.bind(this));
const setRegular = this.catCatch.querySelector("#setRegular");
if (setRegular) setRegular.addEventListener('click', this.handleSetRegular.bind(this));
// i18n 处理
this.applyI18n();
}
/**
* 应用国际化文本
*/
applyI18n() {
if (window.CatCatchI18n) {
this.catCatch.querySelectorAll('[data-i18n]').forEach((element) => {
if (element && element.dataset && element.dataset.i18n) {
element.innerHTML = window.CatCatchI18n[element.dataset.i18n][this.language] || element.innerHTML;
}
});
this.catCatch.querySelectorAll('[data-i18n-outer]').forEach((element) => {
if (element && element.dataset && element.dataset.i18nOuter) {
element.outerHTML = window.CatCatchI18n[element.dataset.i18nOuter][this.language] || element.outerHTML;
}
});
}
}
/**
* 翻译函数
* @param {String} key
* @param {String|null} original 原始文本
* @returns 翻译后的文本
*/
i18n(key, original = "") {
if (!window.CatCatchI18n || !key || !window.CatCatchI18n[key]) { return original; }
return window.CatCatchI18n[key][this.language] || original;
}
/**
* 处理面板拖动事件
* @param {MouseEvent} event
*/
handleDragStart(event) {
this.x = event.pageX - this.catCatch.offsetLeft;
this.y = event.pageY - this.catCatch.offsetTop;
const moveHandler = this.handleMove.bind(this);
document.addEventListener('mousemove', moveHandler);
document.addEventListener('mouseup', () => {
document.removeEventListener('mousemove', moveHandler);
}, { once: true });
}
/**
* 处理面板移动事件
* 通过鼠标事件更新面板位置
* @param {MouseEvent} event
*/
handleMove(event) {
if (!this.catCatch) return;
this.catCatch.style.left = (event.pageX - this.x) + 'px';
this.catCatch.style.top = (event.pageY - this.y) + 'px';
}
handleAutoDownChange(event) {
localStorage.setItem("CatCatchCatch_autoDown", event.target.checked ? "checked" : "");
}
handleFfmpegChange(event) {
localStorage.setItem("CatCatchCatch_ffmpeg", event.target.checked ? "checked" : "");
}
handleRestartAlwaysChange(event) {
localStorage.setItem("CatCatchCatch_restart", event.target.checked ? "checked" : "");
}
/**
* 处理清理缓存事件
* @param {MouseEvent} event
*/
handleClean(event) {
if (window.confirm(this.i18n("clearCacheConfirmation", "确认清除缓存?"))) {
this.clearCache();
const $clean = this.catCatch.querySelector("#clean");
if (!$clean) return;
$clean.innerHTML = this.i18n("cleanupCompleted", "清理完成!");
setTimeout(() => {
if ($clean) $clean.innerHTML = this.i18n("clearCache", "清理缓存");
}, 1000);
}
}
/**
* 处理下载事件
* @param {MouseEvent} event
*/
handleDownload(event) {
try {
if (this.isComplete || window.confirm(this.i18n("downloadConfirmation", "提前下载可能会造成数据混乱.确认?"))) {
this.catchDownload();
}
} catch (error) {
console.error("下载处理失败:", error);
alert(this.i18n("downloadError", "下载过程中出错,请查看控制台"));
}
}
handleHide(event) {
const catCatchElement = this.catCatch.querySelector('#catCatch');
if (catCatchElement.style.display === "none") {
catCatchElement.style.display = "flex";
this.catCatch.style.opacity = "";
} else {
catCatchElement.style.display = "none";
this.catCatch.style.opacity = "0.5";
}
}
handleClose(event) {
if (this.isComplete || window.confirm(this.i18n("closeConfirmation", "确认关闭?"))) {
this.clearCache();
this.enable = false;
this.catCatch.style.display = "none";
window.postMessage({ action: "catCatchToBackground", Message: "script", script: "catch.js", refresh: false });
}
}
/**
* 从头捕获
* @param {MouseEvent} event
*/
handleRestart(event) {
const checkHead = this.catCatch.querySelector("#checkHead");
if (checkHead) checkHead.checked = true;
this.clearCache();
document.querySelectorAll("video").forEach((element) => {
element.currentTime = 0;
element.play();
});
}
handleSetFileName(event) {
this.setFileName = window.prompt(this.i18n("fileName", "输入文件名, 不包含扩展名"), this.setFileName ?? "");
this.getFileName();
}
handleTest(event) {
console.log("捕获的媒体数据:", this.catchMedia);
}
handleCompleteClearCache(event) {
localStorage.setItem("CatCatchCatch_completeClearCache", event.target.checked ? "checked" : "");
}
/**
* 自动缓冲尾
* @param {MouseEvent} event
*/
handleAutoToBuffered(event) {
// if (!this.autoToBufferedFlag) return;
// this.autoToBufferedFlag = false;
const $autoToBuffered = this.catCatch.querySelector("#autoToBuffered");
if (!$autoToBuffered) return;
localStorage.setItem("CatCatchCatch_autoToBuffered", event.target.checked ? "checked" : "");
const videos = document.querySelectorAll("video");
for (let video of videos) {
video.addEventListener("progress", (event) => {
try {
if (video.buffered && video.buffered.length > 0) {
const bufferedEnd = video.buffered.end(0);
if ($autoToBuffered.checked && bufferedEnd < video.duration) {
video.currentTime = bufferedEnd - 5;
}
}
} catch (error) {
console.error("处理缓冲进度失败:", error);
}
});
video.addEventListener("ended", () => {
$autoToBuffered.checked = false;
});
}
}
/**
* CSS选择器 提取文件名
* @param {MouseEvent} event
*/
handleSetSelector(event) {
const result = window.prompt("Selector", localStorage.getItem("CatCatchCatch_selector") ?? "");
if (result == null) return;
if (result == "") {
this.clearFileName("selector");
return;
}
let title;
try {
title = document.querySelector(result);
} catch (e) {
this.clearFileName("selector", this.i18n("fileNameError", "选择器语法错误!"));
return;
}
if (title && title.innerHTML) {
this.selector.innerHTML = this.stringModify(result);
localStorage.setItem("CatCatchCatch_selector", result);
this.getFileName();
} else {
this.clearFileName("selector", this.i18n("fileNameError", "表达式错误, 无法获取或内容为空!"));
}
}
/**
* 正则 提取文件名
* @param {MouseEvent} event
*/
handleSetRegular(event) {
let result = window.prompt(this.i18n("regular", "文件名获取正则"), localStorage.getItem("CatCatchCatch_regular") ?? "");
if (result == null) return;
if (result == "") {
this.clearFileName("regular");
return;
}
try {
new RegExp(result);
this.regular.innerHTML = this.stringModify(result);
localStorage.setItem("CatCatchCatch_regular", result);
this.getFileName();
} catch (e) {
this.clearFileName("regular", this.i18n("fileNameError", "正则表达式错误"));
console.log(e);
}
}
/**
* 核心函数 代理MediaSource方法
*/
proxyMediaSourceMethods() {
// 代理 addSourceBuffer 方法
window.MediaSource.prototype.addSourceBuffer = new Proxy(window.MediaSource.prototype.addSourceBuffer, {
apply: (target, thisArg, argumentsList) => {
try {
const result = Reflect.apply(target, thisArg, argumentsList);
// 标题获取
setTimeout(() => { this.getFileName(); }, 2000);
this.tips.innerHTML = this.i18n("capturingData", "捕获数据中...");
this.catchMedia.push({ mimeType: argumentsList[0], bufferList: [] });
const index = this.catchMedia.length - 1;
// 代理 appendBuffer 方法
result.appendBuffer = new Proxy(result.appendBuffer, {
apply: (target, thisArg, argumentsList) => {
Reflect.apply(target, thisArg, argumentsList);
if (this.enable && argumentsList[0]) {
this.mediaSize += argumentsList[0].byteLength || 0;
if (this.tips) {
this.tips.innerHTML = this.i18n("capturingData", "捕获数据中...") + ": " + this.byteToSize(this.mediaSize);
}
this.catchMedia[index].bufferList.push(argumentsList[0]);
}
}
});
return result;
} catch (error) {
console.error("addSourceBuffer 代理错误:", error);
return Reflect.apply(target, thisArg, argumentsList);
}
}
});
// 代理 endOfStream 方法
window.MediaSource.prototype.endOfStream = new Proxy(window.MediaSource.prototype.endOfStream, {
apply: (target, thisArg, argumentsList) => {
try {
Reflect.apply(target, thisArg, argumentsList);
if (this.enable) {
this.isComplete = true;
if (this.tips) {
this.tips.innerHTML = this.i18n("captureCompleted", "捕获完成");
}
if (localStorage.getItem("CatCatchCatch_autoDown") == "checked") {
setTimeout(() => this.catchDownload(), 500);
}
}
} catch (error) {
console.error("endOfStream 代理错误:", error);
return Reflect.apply(target, thisArg, argumentsList);
}
}
});
}
/**
* 自动从头捕获
* 监控DOM变化自动重置视频播放位置
*/
setupAutoRestart() {
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('video').forEach((video) => this.resetVideoPlayback(video));
// 监控 DOM
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
try {
if (node.tagName === 'VIDEO') {
this.resetVideoPlayback(node);
} else if (node.querySelectorAll) {
node.querySelectorAll('video').forEach(video => this.resetVideoPlayback(video));
}
} catch (error) {
console.error("处理新添加的视频节点失败:", error);
}
});
});
});
observer.observe(document.body || document.documentElement, { childList: true, subtree: true });
});
}
/**
* 重置视频播放位置
* @param {Object} video
*/
resetVideoPlayback(video) {
if (!video) return;
const timer = setInterval(() => {
if (!video.paused) {
video.currentTime = 0;
const checkHead = this.catCatch.querySelector("#checkHead");
if (checkHead) checkHead.checked = true;
this.clearCache();
clearInterval(timer);
}
}, 500);
// 5秒后如果还没有检测到播放就清除定时器
setTimeout(() => clearInterval(timer), 5000);
video.addEventListener('play', () => {
if (!video.isResetCatCatch) {
video.isResetCatCatch = true;
video.currentTime = 0;
const checkHead = this.catCatch.querySelector("#checkHead");
if (checkHead) checkHead.checked = true;
this.clearCache();
}
}, { once: true });
}
/**
* 下载捕获的数据
*/
catchDownload() {
if (this.catchMedia.length == 0) {
alert(this.i18n("noData", "没抓到有效数据"));
return;
}
let downloadWithFFmpeg = this.catchMedia.length >= 2 && localStorage.getItem("CatCatchCatch_ffmpeg") == "checked";
/**
* 检查文件
* 检查是否有头部文件 没有头部文件则提示 不使用ffmpeg合并
* 检查是否有多个头部文件 根据用户选项 是否清理多于头部数据
*/
const checkHead = this.catCatch.querySelector("#checkHead");
// 仅确认一次是否清除多余头部数据
let userConfirmedHeadChoice = false;
for (let key in this.catchMedia) {
if (!this.catchMedia[key]?.bufferList || this.catchMedia[key].bufferList.length <= 1) continue;
let lastHeaderIndex = -1;
// 遍历所有 buffer 寻找最后一个头部
for (let i = 0; i < this.catchMedia[key].bufferList.length; i++) {
const data = new Uint8Array(this.catchMedia[key].bufferList[i]);
// 检查MP4格式的头部 (ftyp)
if (data.length > 8 &&
data[4] === 0x66 && // 'f'
data[5] === 0x74 && // 't'
data[6] === 0x79 && // 'y'
data[7] === 0x70) // 'p'
{
lastHeaderIndex = i; // 持续更新直到找到最后一个头部
}
// 检查WebM格式的头部 (1A 45 DF A3)
else if (data.length > 4 &&
data[0] === 0x1A &&
data[1] === 0x45 &&
data[2] === 0xDF &&
data[3] === 0xA3) {
lastHeaderIndex = i; // 持续更新直到找到最后一个WebM头部
}
}
if (lastHeaderIndex == -1) {
alert(this.i18n("noHead", "没有检测到视频头部数据, 请使用本地工具处理"));
downloadWithFFmpeg = false; // 没有头部数据则不使用ffmpeg合并
}
if (lastHeaderIndex > 0) {
// 只有第一次遇到多余头部且用户尚未选择时才提示
if (!userConfirmedHeadChoice && !checkHead.checked) {
checkHead.checked = window.confirm(this.i18n("headData", "检测到多余头部数据, 是否清除?"));
userConfirmedHeadChoice = true; // 标记已经询问过用户
}
if (checkHead.checked) {
this.catchMedia[key].bufferList.splice(0, lastHeaderIndex); // 移除最后一个头部之前的所有元素
}
}
}
downloadWithFFmpeg ? this.downloadWithFFmpeg() : this.downloadDirect();
if (this.isComplete) {
if (localStorage.getItem("CatCatchCatch_completeClearCache") == "checked") { this.clearCache(); }
if (this.tips) {
this.tips.innerHTML = this.i18n("downloadCompleted", "下载完毕...");
}
}
}
/**
* 使用FFmpeg合并下载捕获的数据
*/
downloadWithFFmpeg() {
const media = [];
for (let item of this.catchMedia) {
if (!item || !item.bufferList || item.bufferList.length === 0) continue;
const mime = (item.mimeType && item.mimeType.split(';')[0]) || 'video/mp4';
const fileBlob = new Blob(item.bufferList, { type: mime });
const type = mime.split('/')[0] || 'video';
media.push({
data: (typeof chrome == "object") ? URL.createObjectURL(fileBlob) : fileBlob,
type: type
});
}
if (media.length === 0) {
alert(this.i18n("noData", "没有有效数据可下载"));
return;
}
const title = this.fileName ? this.fileName.innerHTML.trim() : document.title;
window.postMessage({
action: "catCatchFFmpeg",
use: "catchMerge",
files: media,
title: title,
output: title,
quantity: media.length
});
}
/**
* 直接下载捕获的数据
*/
downloadDirect() {
const a = document.createElement('a');
let downloadCount = 0;
for (let item of this.catchMedia) {
if (!item || !item.bufferList || item.bufferList.length === 0) continue;
const mime = (item.mimeType && item.mimeType.split(';')[0]) || 'video/mp4';
const type = mime.split('/')[0] == "video" ? "mp4" : "mp3";
const fileBlob = new Blob(item.bufferList, { type: mime });
a.href = URL.createObjectURL(fileBlob);
a.download = `${this.fileName ? this.fileName.innerHTML.trim() : document.title}.${type}`;
a.click();
// 释放URL对象以避免内存泄漏
setTimeout(() => URL.revokeObjectURL(a.href), 100);
downloadCount++;
}
a.remove();
if (downloadCount === 0) {
alert(this.i18n("noData", "没有有效数据可下载"));
}
}
clearFileName(obj = "selector", warning = "") {
localStorage.removeItem("CatCatchCatch_" + obj);
const element = obj == "selector" ? this.selector : this.regular;
if (element) element.innerHTML = this.i18n("notSet", "未设置");
this.getFileName();
if (warning) alert(warning);
}
/**
* 清理缓存
*/
clearCache() {
this.mediaSize = 0;
if (this.isComplete) {
this.catchMedia = [];
this.isComplete = false;
return;
}
for (let key in this.catchMedia) {
const media = this.catchMedia[key];
if (media && media.bufferList && media.bufferList.length > 0) {
// 保留第一个buffer块清除其余的
const firstBuffer = media.bufferList[0];
media.bufferList = [firstBuffer];
this.mediaSize += firstBuffer ? (firstBuffer.byteLength || 0) : 0;
}
}
}
byteToSize(byte) {
if (!byte || byte < 1024) return "0KB";
if (byte < 1024 * 1024) {
return (byte / 1024).toFixed(1) + "KB";
} else if (byte < 1024 * 1024 * 1024) {
return (byte / 1024 / 1024).toFixed(1) + "MB";
} else {
return (byte / 1024 / 1024 / 1024).toFixed(1) + "GB";
}
}
/**
* 获取文件名
*/
getFileName() {
try {
if (!this.fileName) return;
if (this.setFileName) {
this.fileName.innerHTML = this.stringModify(this.setFileName);
return;
}
let name = "";
const selectorKey = localStorage.getItem("CatCatchCatch_selector");
if (selectorKey) {
const title = document.querySelector(selectorKey);
if (title && title.innerHTML) {
name = title.innerHTML;
}
}
const regularKey = localStorage.getItem("CatCatchCatch_regular");
if (regularKey) {
const str = name == "" ? document.documentElement.outerHTML : name;
const reg = new RegExp(regularKey, "g");
let result = str.match(reg);
if (result) {
result = result.filter((item) => item !== "");
name = result.join("_");
}
}
this.fileName.innerHTML = name ? this.stringModify(name) : this.stringModify(document.title);
} catch (error) {
console.error("获取文件名失败:", error);
if (this.fileName) this.fileName.innerHTML = this.stringModify(document.title);
}
}
stringModify(str) {
if (!str) return "untitled";
return str.replace(/['\\:\*\?"<\/>\|~]/g, function (m) {
return {
"'": '&#39;',
'\\': '&#92;',
'/': '&#47;',
':': '&#58;',
'*': '&#42;',
'?': '&#63;',
'"': '&quot;',
'<': '&lt;',
'>': '&gt;',
'|': '&#124;',
'~': '_'
}[m];
});
}
}
// 创建并启动CatCatcher实例
const catCatcher = new CatCatcher();
})();

230
catch-script/i18n.js Normal file
View File

@@ -0,0 +1,230 @@
(function () {
if (window.CatCatchI18n) { return; }
window.CatCatchI18n = {
languages: ["en", "zh"],
downloadCapturedData: {
en: "Download the captured data",
zh: "下载已捕获的数据"
},
deleteCapturedData: {
en: "Delete the captured data",
zh: "删除已捕获数据"
},
capturedBeginning: {
en: "Capture from the beginning",
zh: "从头捕获"
},
alwaysCapturedBeginning: {
en: "Always Capture from the beginning",
zh: "始终从头捕获"
},
hide: {
en: "Hide",
zh: "隐藏"
},
close: {
en: "Close",
zh: "关闭"
},
save: {
en: "Save",
zh: "保存"
},
automaticDownload: {
en: "Automatic download",
zh: "完成捕获自动下载"
},
ffmpeg: {
en: "using ffmpeg",
zh: "使用ffmpeg"
},
fileName: {
en: "File name",
zh: "文件名"
},
selector: {
en: "Selector",
zh: "表达式"
},
regular: {
en: "Regular",
zh: "正则"
},
notSet: {
en: "Not set",
zh: "未设置"
},
usingSelector: {
en: "selector",
zh: "表达式提取"
},
usingRegular: {
en: "regular",
zh: "正则提取"
},
customize: {
en: "Customize",
zh: "自定义"
},
cleanHeader: {
en: "Clean up redundant header data",
zh: "清理多余头部数据"
},
clearCache: {
en: "Clear cache",
zh: "清理缓存"
},
cleanupCompleted: {
en: "Cleanup completed",
zh: "清理完成"
},
downloadConfirmation: {
en: "Downloading in advance may cause data confusion. Confirm?",
zh: "提前下载可能会造成数据混乱.确认?"
},
fileNameError: {
en: "Unable to fetch or the content is empty!",
zh: "无法获取或内容为空!"
},
noData: {
en: "No data",
zh: "没抓到有效数据!"
},
waiting: {
en: "Waiting for video to play",
zh: "等待视频播放"
},
capturingData: {
en: "Capturing data",
zh: "捕获数据中"
},
captureCompleted: {
en: "Capture completed",
zh: "捕获完成"
},
downloadCompleted: {
en: "Download completed",
zh: "下载完毕"
},
selectVideo: {
en: "Select Video",
zh: "选择视频"
},
selectAudio: {
en: "Select Audio",
zh: "选择音频"
},
recordEncoding: {
en: "Record Encoding",
zh: "录制编码"
},
readVideo: {
en: "Read Video",
zh: "读取视频"
},
startRecording: {
en: "Start Recording",
zh: "开始录制"
},
stopRecording: {
en: "Stop Recording",
zh: "停止录制"
},
noVideoDetected: {
en: "No video detected, Please read again",
zh: "没有检测到视频, 请重新读取"
},
recording: {
en: "Recording",
zh: "视频录制中"
},
recordingNotSupported: {
en: "recording Not Supported",
zh: "不支持录制"
},
formatNotSupported: {
en: "Format not supported",
zh: "不支持此格式"
},
clickToStartRecording: {
en: "Click to start recording",
zh: "请点击开始录制"
},
sentToFfmpeg: {
en: "Sent to ffmpeg",
zh: "发送到ffmpeg"
},
recordingFailed: {
en: "Recording failed",
zh: "录制失败"
},
scriptNotSupported: {
en: "This script is not supported",
zh: "当前网页不支持此脚本"
},
dragWindow: {
en: "Drag window",
zh: "拖动窗口"
},
autoToBuffered: {
en: "Automatically jump to buffer",
zh: "自动跳转到缓冲尾"
},
save1hour: {
en: "Save once every hour",
zh: "1小时保存一次"
},
recordingChangeEncoding: {
en: "Cannot change encoding during recording",
zh: "录制中不能更改编码"
},
streamEmpty: {
en: "Media stream is empty",
zh: "媒体流为空"
},
notStream: {
en: "Not a media stream object",
zh: "非媒体流对象"
},
notStream: {
en: "Not a media stream object",
zh: "非媒体流对象"
},
streamAdded: {
en: "Stream added",
zh: "流已添加"
},
videoAndAudio: {
en: "Includes both audio and video streams",
zh: "已包含音频和视频流"
},
audioBits: {
en: "Audio bit",
zh: "音频码率"
},
videoBits: {
en: "Video bits",
zh: "视频码率"
},
frameRate: {
en: "frame Rate",
zh: "帧率"
},
noHeader: {
en: "No header data detected, please process with local tools",
zh: "没有检测到视频头部数据, 请使用本地工具处理"
},
headData: {
en: "Multiple header data found in media file, Clear it?",
zh: "检测到多余头部数据, 是否清除?"
},
clearCacheConfirmation: {
en: "Are you sure you want to clear the cache?",
zh: "确定要清除缓存吗?"
},
closeConfirmation: {
en: "Are you sure you want to close?",
zh: "确定要关闭吗?"
}
};
})();

266
catch-script/recorder.js Normal file
View File

@@ -0,0 +1,266 @@
(function () {
console.log("recorder.js Start");
if (document.getElementById("catCatchRecorder")) { return; }
// let language = "en";
let language = navigator.language.replace("-", "_");
if (window.CatCatchI18n) {
if (!window.CatCatchI18n.languages.includes(language)) {
language = language.split("_")[0];
if (!window.CatCatchI18n.languages.includes(language)) {
language = "en";
}
}
}
const buttonStyle = 'style="border:solid 1px #000;margin:2px;padding:2px;background:#fff;border-radius:4px;border:solid 1px #c7c7c780;color:#000;"';
const checkboxStyle = 'style="-webkit-appearance: auto;"';
const CatCatch = document.createElement("div");
CatCatch.setAttribute("id", "catCatchRecorder");
CatCatch.innerHTML = `<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYBAMAAAASWSDLAAAAKlBMVEUAAADLlROxbBlRAD16GS5oAjWWQiOCIytgADidUx/95gHqwwTx0gDZqwT6kfLuAAAACnRSTlMA/vUejV7kuzi8za0PswAAANpJREFUGNNjwA1YSxkYTEqhnKZLLi6F1w0gnKA1shdvHYNxdq1atWobjLMKCOAyC3etlVrUAOH4HtNZmLgoAMKpXX37zO1FwcZAwMDguGq1zKpFmTNnzqx0Bpp2WvrU7ttn9py+I8JgLn1R8Pad22vurNkjwsBReHv33junzuyRnOnMwNCSeFH27K5dq1SNgcZxFMnuWrNq1W5VkNntihdv7ToteGcT0C7mIkE1qbWCYjJnM4CqEoWKdoslChXuUgXJqIcLebiphSgCZRhaPDhcDFhdmUMCGIgEAFA+Uc02aZg9AAAAAElFTkSuQmCC" style="-webkit-user-drag: none;width: 20px;">
<div id="tips"></div>
<span data-i18n="selectVideo">选择视频</span> <select id="videoList" style="max-width: 200px;"></select>
<span data-i18n="recordEncoding">录制编码</span> <select id="mimeTypeList" style="max-width: 200px;"></select>
<label><input type="checkbox" id="ffmpeg" ${checkboxStyle}><span data-i18n="ffmpeg">使用ffmpeg转码</span></label>
<label>
<select id="videoBits">
<option value="2500000" data-i18n="videoBits">视频码率</option>
<option value="2500000">2.5 Mbps</option>
<option value="5000000">5 Mbps</option>
<option value="8000000">8 Mbps</option>
<option value="16000000">16 Mbps</option>
</select>
<select id="audioBits">
<option value="128000" data-i18n="audioBits">视频码率</option>
<option value="128000">128 kbps</option>
<option value="256000">256 kbps</option>
</select>
<select id="frameRate">
<option value="0" data-i18n="frameRate">帧率</option>
<option value="25">25 FPS</option>
<option value="30">30 FPS</option>
<option value="60">60 FPS</option>
<option value="120">120 FPS</option>
</select>
</label>
<div>
<button id="getVideo" ${buttonStyle} data-i18n="readVideo">读取视频</button>
<button id="start" ${buttonStyle} data-i18n="startRecording">开始录制</button>
<button id="stop" ${buttonStyle} data-i18n="stopRecording">停止录制</button>
<button id="hide" ${buttonStyle} data-i18n="hide">隐藏</button>
<button id="close" ${buttonStyle} data-i18n="close">关闭</button>
</div>`;
CatCatch.style = `
position: fixed;
z-index: 999999;
top: 10%;
left: 80%;
background: rgb(255 255 255 / 85%);
border: solid 1px #c7c7c7;
border-radius: 4px;
color: rgb(26, 115, 232);
padding: 5px 5px 5px 5px;
font-size: 12px;
font-family: "Microsoft YaHei", "Helvetica", "Arial", sans-serif;
user-select: none;
display: flex;
align-items: flex-start;
justify-content: space-evenly;
flex-direction: column;
line-height: 20px;`;
// 创建 Shadow DOM 放入CatCatch
const divShadow = document.createElement('div');
const shadowRoot = divShadow.attachShadow({ mode: 'closed' });
shadowRoot.appendChild(CatCatch);
// 页面插入Shadow DOM
document.getElementsByTagName('html')[0].appendChild(divShadow);
const $tips = CatCatch.querySelector("#tips");
const $videoList = CatCatch.querySelector("#videoList");
const $mimeTypeList = CatCatch.querySelector("#mimeTypeList");
const $start = CatCatch.querySelector("#start");
const $stop = CatCatch.querySelector("#stop");
let videoList = [];
$tips.innerHTML = i18n("noVideoDetected", "没有检测到视频, 请重新读取");
let recorder = {};
let option = { mimeType: 'video/webm;codecs=vp9,opus' };
CatCatch.querySelector("#hide").addEventListener('click', function (event) {
CatCatch.style.display = "none";
});
CatCatch.querySelector("#close").addEventListener('click', function (event) {
recorder?.state && recorder.stop();
CatCatch.style.display = "none";
window.postMessage({ action: "catCatchToBackground", Message: "script", script: "recorder.js", refresh: false });
});
function init() {
getVideo();
$start.style.display = 'inline';
$stop.style.display = 'none';
}
setTimeout(init, 500);
// #region 视频编码选择
function setMimeType() {
function getSupportedMimeTypes(media, types, codecs) {
const supported = [];
types.forEach((type) => {
const mimeType = `${media}/${type}`;
codecs.forEach((codec) => [`${mimeType};codecs=${codec}`].forEach(variation => {
if (MediaRecorder.isTypeSupported(variation)) {
supported.push(variation);
}
}));
if (MediaRecorder.isTypeSupported(mimeType)) {
supported.push(mimeType);
}
});
return supported;
};
const videoTypes = ["webm", "ogg", "mp4", "x-matroska"];
const codecs = ["should-not-be-supported", "vp9", "vp8", "avc1", "av1", "h265", "h.265", "h264", "h.264", "opus", "pcm", "aac", "mpeg", "mp4a"];
const supportedVideos = getSupportedMimeTypes("video", videoTypes, codecs);
supportedVideos.forEach(function (type) {
$mimeTypeList.options.add(new Option(type, type));
});
option.mimeType = supportedVideos[0];
$mimeTypeList.addEventListener('change', function (event) {
if (recorder && recorder.state && recorder.state === 'recording') {
$tips.innerHTML = i18n("recordingChangeEncoding", "录制中不能更改编码");
return;
}
if (MediaRecorder.isTypeSupported(event.target.value)) {
option.mimeType = event.target.value;
$tips.innerHTML = event.target.value;
} else {
$tips.innerHTML = i18n("formatNotSupported", "不支持此格式");
}
});
}
setMimeType();
// #endregion 视频编码选择
// #region 获取视频列表
function getVideo() {
videoList = [];
$videoList.options.length = 0;
document.querySelectorAll("video, audio").forEach(function (video, index) {
if (video.currentSrc) {
const src = video.currentSrc.split("/").pop();
videoList.push(video);
$videoList.options.add(new Option(src, index));
}
});
$tips.innerHTML = videoList.length ? i18n("clickToStartRecording", "请点击开始录制") : i18n("noVideoDetected", "没有检测到视频, 请重新读取");
}
CatCatch.querySelector("#getVideo").addEventListener('click', getVideo);
CatCatch.querySelector("#stop").addEventListener('click', function () {
recorder.stop();
});
// #endregion 获取视频列表
CatCatch.querySelector("#start").addEventListener('click', function (event) {
if (!MediaRecorder.isTypeSupported(option.mimeType)) {
$tips.innerHTML = i18n("formatNotSupported", "不支持此格式");
return;
}
init();
const index = $videoList.value;
if (index && videoList[index]) {
let stream = null;
try {
const frameRate = +CatCatch.querySelector("#frameRate").value;
if (frameRate) {
stream = videoList[index].captureStream(frameRate);
} else {
stream = videoList[index].captureStream();
}
} catch (e) {
$tips.innerHTML = i18n("recordingNotSupported", "不支持录制");
return;
}
// 码率
option.audioBitsPerSecond = +CatCatch.querySelector("#audioBits").value;
option.videoBitsPerSecond = +CatCatch.querySelector("#videoBits").value;
recorder = new MediaRecorder(stream, option);
recorder.ondataavailable = function (event) {
if (CatCatch.querySelector("#ffmpeg").checked) {
window.postMessage({
action: "catCatchFFmpeg",
use: "transcode",
files: [{ data: URL.createObjectURL(event.data), type: option.mimeType }],
title: document.title.trim()
});
$tips.innerHTML = i18n("clickToStartRecording", "请点击开始录制");
return;
}
const a = document.createElement('a');
a.href = URL.createObjectURL(event.data);
a.download = `${document.title}`;
a.click();
a.remove();
$tips.innerHTML = i18n("downloadCompleted", "下载完成");;
}
recorder.onstart = function (event) {
$stop.style.display = 'inline';
$start.style.display = 'none';
$tips.innerHTML = i18n("recording", "视频录制中");
}
recorder.onstop = function (event) {
$tips.innerHTML = i18n("stopRecording", "停止录制");
init();
}
recorder.onerror = function (event) {
init();
$tips.innerHTML = i18n("recordingFailed", "录制失败");;
console.log(event);
};
recorder.start();
videoList[index].play();
setTimeout(() => {
if (recorder.state === 'recording') {
$stop.style.display = 'inline';
$start.style.display = 'none';
$tips.innerHTML = i18n("recording", "视频录制中");
}
}, 500);
} else {
$tips.innerHTML = i18n("noVideoDetected", "请确认视频是否存在");
}
});
// #region 移动逻辑
let x, y;
function move(event) {
CatCatch.style.left = event.pageX - x + 'px';
CatCatch.style.top = event.pageY - y + 'px';
}
CatCatch.addEventListener('mousedown', function (event) {
x = event.pageX - CatCatch.offsetLeft;
y = event.pageY - CatCatch.offsetTop;
document.addEventListener('mousemove', move);
document.addEventListener('mouseup', function () {
document.removeEventListener('mousemove', move);
});
});
// #endregion 移动逻辑
// i18n
if (window.CatCatchI18n) {
CatCatch.querySelectorAll('[data-i18n]').forEach(function (element) {
element.innerHTML = window.CatCatchI18n[element.dataset.i18n][language];
});
CatCatch.querySelectorAll('[data-i18n-outer]').forEach(function (element) {
element.outerHTML = window.CatCatchI18n[element.dataset.i18nOuter][language];
});
}
function i18n(key, original = "") {
if (!window.CatCatchI18n) { return original };
return window.CatCatchI18n[key][language];
}
})();

257
catch-script/recorder2.js Normal file
View File

@@ -0,0 +1,257 @@
(function () {
console.log("recorder2.js Start");
if (document.getElementById("catCatchRecorder2")) {
return;
}
if (!navigator.mediaDevices) {
alert("当前网页不支持屏幕分享");
return;
}
let language = navigator.language.replace("-", "_");
if (window.CatCatchI18n) {
if (!window.CatCatchI18n.languages.includes(language)) {
language = language.split("_")[0];
if (!window.CatCatchI18n.languages.includes(language)) {
language = "en";
}
}
}
// 添加style
const style = document.createElement("style");
style.innerHTML = `
@keyframes color-change{
0% { outline: 4px solid rgb(26, 115, 232); }
50% { outline: 4px solid red; }
100% { outline: 4px solid rgb(26, 115, 232); }
}
#catCatchRecorder2 {
font-weight: bold;
position: absolute;
cursor: move;
z-index: 999999999;
outline: 4px solid rgb(26, 115, 232);
resize: both;
overflow: hidden;
height: 720px;
width: 1024px;
top: 30%;
left: 30%;
pointer-events: none;
font-size: 10px;
}
#catCatchRecorderHeader {
background: rgb(26, 115, 232);
color: white;
text-align: center;
height: 20px;
cursor: pointer;
display: flex;
justify-content: space-evenly;
align-items: center;
pointer-events: auto;
}
#catCatchRecorderTitle {
cursor: move;
user-select: none;
width: 45%;
}
#catCatchRecorderinnerCropArea {
height: calc(100% - 20px);
width: 100%;
}
.animation {
animation: color-change 5s infinite;
}
.input-group {
display: flex;
align-items: center;
}
.input-group label {
margin-right: 5px;
}
#videoBitrate, #audioBitrate {
width: 4rem;
}
.input-group label{
width: 5rem;
}`;
// 添加div
let cat = document.createElement("div");
cat.setAttribute("id", "catCatchRecorder2");
cat.innerHTML = `<div id="catCatchRecorderinnerCropArea"></div>
<div id="catCatchRecorderHeader">
<div class="input-group">
<select id="videoBits">
<option value="2500000" data-i18n="videoBits">视频码率</option>
<option value="2500000">2.5 Mbps</option>
<option value="5000000">5 Mbps</option>
<option value="8000000">8 Mbps</option>
<option value="16000000">16 Mbps</option>
</select>
</div>
<div class="input-group">
<select id="audioBits">
<option value="128000" data-i18n="audioBits">视频码率</option>
<option value="128000">128 kbps</option>
<option value="256000">256 kbps</option>
</select>
</div>
<div id="catCatchRecorderStart" data-i18n="startRecording">开始录制</div>
<div id="catCatchRecorderTitle" data-i18n="dragWindow">拖动窗口</div>
<div id="catCatchRecorderClose" data-i18n="close">关闭</div>
</div>`;
// 创建 Shadow DOM 放入CatCatch
const divShadow = document.createElement('div');
const shadowRoot = divShadow.attachShadow({ mode: 'closed' });
shadowRoot.appendChild(cat);
shadowRoot.appendChild(style);
document.getElementsByTagName('html')[0].appendChild(divShadow);
// 事件绑定
const catCatchRecorderStart = cat.querySelector("#catCatchRecorderStart");
catCatchRecorderStart.onclick = function () {
if (recorder) {
recorder.stop();
return;
}
try { startRecording(); } catch (e) { console.log(e); return; }
}
cat.querySelector("#catCatchRecorderClose").onclick = function () {
recorder && recorder.stop();
cat.remove();
}
// 拖动div
const catCatchRecorderinnerCropArea = cat.querySelector("#catCatchRecorderinnerCropArea");
cat.querySelector("#catCatchRecorderTitle").onpointerdown = (e) => {
let pos1, pos2, pos3, pos4;
pos3 = e.clientX;
pos4 = e.clientY;
if (pos3 - cat.offsetWidth - cat.offsetLeft > - 20 &&
pos4 - cat.offsetHeight - cat.offsetTop > - 20) {
return;
}
document.onpointermove = (e) => {
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
cat.style.top = cat.offsetTop - pos2 + "px";
cat.style.left = cat.offsetLeft - pos1 + "px";
}
document.onpointerup = () => {
document.onpointerup = null;
document.onpointermove = null;
}
}
// document.getElementsByTagName('html')[0].appendChild(cat);
// 初始化位置
const video = document.querySelector("video");
if (video) {
// 调整和video一样大小
if (video.clientHeight >= 0 && video.clientWidth >= 0) {
cat.style.height = video.clientHeight + 20 + "px";
cat.style.width = video.clientWidth + "px";
}
// 调整到video的位置
const videoOffset = getElementOffset(video);
if (videoOffset.top >= 0 && videoOffset.left >= 0) {
cat.style.top = videoOffset.top + "px";
cat.style.left = videoOffset.left + "px";
}
// 防止遮挡菜单
let catAttr = cat.getBoundingClientRect();
if (document.documentElement.scrollTop + catAttr.bottom > document.documentElement.scrollTop + window.innerHeight) {
cat.style.top = document.documentElement.scrollTop + window.innerHeight - catAttr.height + "px";
}
}
// 录制
var recorder;
async function startRecording() {
const buffer = [];
let option = {
mimeType: 'video/webm;codecs=vp8,opus',
videoBitsPerSecond: +cat.querySelector("#videoBits").value,
audioBitsPerSecond: +cat.querySelector("#audioBits").value
};
if (MediaRecorder.isTypeSupported('video/webm;codecs=vp9,opus')) {
option.mimeType = 'video/webm;codecs=vp9,opus';
} else if (MediaRecorder.isTypeSupported('video/webm;codecs=h264')) {
option.mimeType = 'video/webm;codecs=h264';
}
const cropTarget = await CropTarget.fromElement(catCatchRecorderinnerCropArea);
const stream = await navigator.mediaDevices
.getDisplayMedia({
preferCurrentTab: true,
video: {
cursor: "never"
},
audio: {
sampleRate: 48000,
sampleSize: 16,
channelCount: 2
}
});
const [track] = stream.getVideoTracks();
await track.cropTo(cropTarget);
recorder = new MediaRecorder(stream, option);
recorder.start();
recorder.onstart = function (e) {
buffer.slice(0);
catCatchRecorderStart.innerHTML = i18n("stopRecording", "停止录制");
cat.classList.add("animation");
}
recorder.ondataavailable = function (e) {
buffer.push(e.data);
}
recorder.onstop = function () {
const fileBlob = new Blob(buffer, { type: option });
const a = document.createElement('a');
a.href = URL.createObjectURL(fileBlob);
a.download = `${document.title}.webm`;
a.click();
a.remove();
buffer.slice(0);
stream.getTracks().forEach(track => track.stop());
recorder = undefined;
catCatchRecorderStart.innerHTML = i18n("startRecording", "开始录制");
cat.classList.remove("animation");
}
}
function getElementOffset(el) {
const rect = el.getBoundingClientRect();
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
return {
top: rect.top + scrollTop,
left: rect.left + scrollLeft
};
}
// i18n
if (window.CatCatchI18n) {
CatCatch.querySelectorAll('[data-i18n]').forEach(function (element) {
const translation = window.CatCatchI18n[element.dataset.i18n]?.[language];
if (translation) {
element.innerHTML = translation;
}
});
CatCatch.querySelectorAll('[data-i18n-outer]').forEach(function (element) {
const outerTranslation = window.CatCatchI18n[element.dataset.i18nOuter]?.[language];
if (outerTranslation) {
element.outerHTML = outerTranslation;
}
});
}
function i18n(key, original = "") {
if (!window.CatCatchI18n) { return original };
return window.CatCatchI18n[key][language];
}
})();

751
catch-script/search.js Normal file
View File

@@ -0,0 +1,751 @@
// const CATCH_SEARCH_ONLY = true;
(function __CAT_CATCH_CATCH_SCRIPT__() {
const isRunningInWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope;
const CATCH_SEARCH_DEBUG = false; // 开发调试日志
// 防止 console.log 被劫持
if (!isRunningInWorker && CATCH_SEARCH_DEBUG && console.log.toString() != 'function log() { [native code] }') {
const newIframe = top.document.createElement("iframe");
newIframe.style.display = "none";
top.document.body.appendChild(newIframe);
window.console.log = newIframe.contentWindow.console.log;
}
// 防止 window.postMessage 被劫持
const _postMessage = self.postMessage;
// console.log("start search.js");
const filter = new Set();
const reKeyURL = /URI="(.*)"/;
const dataRE = /^data:(application|video|audio)\//i;
const joinBaseUrlTask = [];
const baseUrl = new Set();
const regexVimeo = /^https:\/\/[^\.]*\.vimeocdn\.com\/exp=.*\/playlist\.json\?/i;
const videoSet = new Set();
const base64Regex = /^[A-Za-z0-9+/]+={0,2}$/;
extractBaseUrl(location.href);
// Worker
const _Worker = Worker;
self.Worker = function (scriptURL, options) {
try {
const xhr = new XMLHttpRequest();
xhr.open('GET', scriptURL, false);
xhr.send();
if (xhr.status === 200) {
const blob = new Blob([`(${__CAT_CATCH_CATCH_SCRIPT__.toString()})();`, xhr.response], { type: 'text/javascript' });
const newWorker = new _Worker(URL.createObjectURL(blob), options);
newWorker.addEventListener("message", function (event) {
if (event.data?.action == "catCatchAddKey" || event.data?.action == "catCatchAddMedia") {
postData(event.data);
}
});
return newWorker;
}
} catch (error) {
return new _Worker(scriptURL, options);
}
return new _Worker(scriptURL, options);
}
self.Worker.toString = function () {
return _Worker.toString();
}
// JSON.parse
const _JSONparse = JSON.parse;
JSON.parse = function () {
let data = _JSONparse.apply(this, arguments);
findMedia(data);
return data;
}
JSON.parse.toString = function () {
return _JSONparse.toString();
}
async function findMedia(data, depth = 0) {
CATCH_SEARCH_DEBUG && console.log(data);
let index = 0;
if (!data) { return; }
if (data instanceof Array && data.length == 16) {
const isKey = data.every(function (value) {
return typeof value == 'number' && value <= 256
});
if (isKey) {
postData({ action: "catCatchAddKey", key: data, href: location.href, ext: "key" });
return;
}
}
if (data instanceof ArrayBuffer && data.byteLength == 16) {
postData({ action: "catCatchAddKey", key: data, href: location.href, ext: "key" });
return;
}
for (let key in data) {
if (index != 0) { depth = 0; } index++;
if (typeof data[key] == "object") {
// 查找疑似key
if (data[key] instanceof Array && data[key].length == 16) {
const isKey = data[key].every(function (value) {
return typeof value == 'number' && value <= 256
});
isKey && postData({ action: "catCatchAddKey", key: data[key], href: location.href, ext: "key" });
continue;
}
if (depth > 10) { continue; } // 防止死循环 最大深度
findMedia(data[key], ++depth);
continue;
}
if (typeof data[key] == "string") {
if (isUrl(data[key])) {
const ext = getExtension(data[key]);
if (ext) {
const url = data[key].startsWith("//") ? (location.protocol + data[key]) : data[key];
extractBaseUrl(url);
postData({ action: "catCatchAddMedia", url: url, href: location.href, ext: ext });
}
continue;
}
if (data[key].substring(0, 7).toUpperCase() == "#EXTM3U") {
toUrl(data[key]);
continue;
}
if (dataRE.test(data[key].substring(0, 17))) {
const text = getDataM3U8(data[key]);
text && toUrl(text);
continue;
}
if (data[key].toLowerCase().includes("urn:mpeg:dash:schema:mpd")) {
toUrl(data[key], "mpd");
continue;
}
if (CATCH_SEARCH_DEBUG && data[key].includes("manifest")) {
console.log(data);
}
}
}
}
// XHR
const _xhrOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method) {
method = method.toUpperCase();
CATCH_SEARCH_DEBUG && console.log(this);
this.addEventListener("readystatechange", function (event) {
CATCH_SEARCH_DEBUG && console.log(this);
if (this.status != 200) { return; }
// 处理viemo
this.responseURL.includes("vimeocdn.com") && vimeo(this.responseURL, this.response);
// 查找疑似key
if (this.responseType === "arraybuffer" && this.response?.byteLength) {
if (this.response.byteLength === 16 || this.response.byteLength === 32) {
postData({ action: "catCatchAddKey", key: this.response, href: location.href, ext: "key" });
}
if (this.responseURL.includes(".ts")) {
extractBaseUrl(this.responseURL);
}
}
if (typeof this.response == "object") {
findMedia(this.response);
return;
}
if (this.response == "" || typeof this.response != "string") { return; }
if (dataRE.test(this.response)) {
const text = getDataM3U8(this.response);
text && toUrl(text);
return;
}
if (dataRE.test(this.responseURL)) {
const text = getDataM3U8(this.responseURL);
text && toUrl(text);
return;
}
if (isUrl(this.response)) {
const ext = getExtension(this.response);
ext && postData({ action: "catCatchAddMedia", url: this.response, href: location.href, ext: ext });
return;
}
const responseUpper = this.response.toUpperCase();
if (responseUpper.includes("#EXTM3U")) {
if (responseUpper.substring(0, 7) == "#EXTM3U") {
if (method == "GET") {
toUrl(addBaseUrl(getBaseUrl(this.responseURL), this.response));
postData({ action: "catCatchAddMedia", url: this.responseURL, href: location.href, ext: "m3u8" });
return;
}
toUrl(this.response);
return;
}
if (isJSON(this.response)) {
if (method == "GET") {
postData({ action: "catCatchAddMedia", url: this.responseURL, href: location.href, ext: "json" });
return;
}
toUrl(this.response, "json");
return;
}
}
// dash DRM
if (responseUpper.includes("<MPD") && responseUpper.includes("</MPD>")) {
_postMessage({
action: "catCatchDashDRMMedia",
url: this.responseURL,
data: this.response,
href: location.href
});
return;
}
const isJson = isJSON(this.response);
if (isJson) {
findMedia(isJson);
return;
}
});
_xhrOpen.apply(this, arguments);
}
XMLHttpRequest.prototype.open.toString = function () {
return _xhrOpen.toString();
}
// fetch
const _fetch = fetch;
fetch = async function (input, init) {
let response;
try {
response = await _fetch.apply(this, arguments);
} catch (error) {
console.error("Fetch error:", error);
throw error; // Re-throw the error if necessary
}
const clone = response.clone();
CATCH_SEARCH_DEBUG && console.log(response);
response.arrayBuffer()
.then(arrayBuffer => {
CATCH_SEARCH_DEBUG && console.log({ arrayBuffer, input });
if (arrayBuffer.byteLength == 16) {
postData({ action: "catCatchAddKey", key: arrayBuffer, href: location.href, ext: "key" });
return;
}
let text = new TextDecoder().decode(arrayBuffer);
if (text == "") { return; }
if (typeof input == "object") { input = input.url; }
let isJson = isJSON(text);
if (isJson) {
findMedia(isJson);
return;
}
if (text.substring(0, 7).toUpperCase() == "#EXTM3U") {
if (init?.method == undefined || (init.method && init.method.toUpperCase() == "GET")) {
toUrl(addBaseUrl(getBaseUrl(input), text));
postData({ action: "catCatchAddMedia", url: input, href: location.href, ext: "m3u8" });
return;
}
toUrl(text);
return;
}
if (dataRE.test(text.substring(0, 17))) {
const data = getDataM3U8(text);
data && toUrl(data);
return;
}
});
return clone;
}
fetch.toString = function () {
return _fetch.toString();
}
// Array.prototype.slice
const _slice = Array.prototype.slice;
Array.prototype.slice = function (start, end) {
const data = _slice.apply(this, arguments);
if (end == 16 && this.length == 32) {
CATCH_SEARCH_DEBUG && console.log(this, start, end, data);
for (let item of data) {
if (typeof item != "number" || item > 255) { return data; }
}
postData({ action: "catCatchAddKey", key: data, href: location.href, ext: "key" });
}
return data;
}
Array.prototype.slice.toString = function () {
return _slice.toString();
}
//#region TypedArray.prototype.subarray
const createSubarrayWrapper = (originalSubarray) => {
return function (start, end) {
const data = originalSubarray.apply(this, arguments);
CATCH_SEARCH_DEBUG && console.log(this, start, end, data);
if (data.byteLength == 16) {
const uint8 = new _Uint8Array(data);
const isValid = Array.from(uint8).every(item => typeof item == "number" && item <= 255);
isValid && postData({ action: "catCatchAddKey", key: uint8.buffer, href: location.href, ext: "key" });
}
return data;
}
}
// Int8Array.prototype.subarray
const _Int8ArraySubarray = Int8Array.prototype.subarray;
Int8Array.prototype.subarray = createSubarrayWrapper(_Int8ArraySubarray);
Int8Array.prototype.subarray.toString = function () {
return _Int8ArraySubarray.toString();
}
// Uint8Array.prototype.subarray
const _Uint8ArraySubarray = Uint8Array.prototype.subarray;
Uint8Array.prototype.subarray = createSubarrayWrapper(_Uint8ArraySubarray);
Uint8Array.prototype.subarray.toString = function () {
return _Uint8ArraySubarray.toString();
}
//#endregion
// window.btoa / window.atob
const _btoa = btoa;
btoa = function (data) {
const base64 = _btoa.apply(this, arguments);
CATCH_SEARCH_DEBUG && console.log(base64, data, base64.length);
if (base64.length == 24 && base64.substring(22, 24) == "==") {
postData({ action: "catCatchAddKey", key: base64, href: location.href, ext: "base64Key" });
}
if (data.substring(0, 7).toUpperCase() == "#EXTM3U") {
toUrl(data);
}
return base64;
}
btoa.toString = function () {
return _btoa.toString();
}
const _atob = atob;
atob = function (base64) {
const data = _atob.apply(this, arguments);
CATCH_SEARCH_DEBUG && console.log(base64, data, base64.length);
if (base64.length == 24 && base64.substring(22, 24) == "==") {
postData({ action: "catCatchAddKey", key: base64, href: location.href, ext: "base64Key" });
}
if (data.substring(0, 7).toUpperCase() == "#EXTM3U") {
toUrl(data);
}
if (data.endsWith("</MPD>")) {
toUrl(data, "mpd");
}
return data;
}
atob.toString = function () {
return _atob.toString();
}
// fromCharCode
const _fromCharCode = String.fromCharCode;
let m3u8Text = '';
String.fromCharCode = function () {
const data = _fromCharCode.apply(this, arguments);
if (data.length < 7) { return data; }
CATCH_SEARCH_DEBUG && console.log(data, this, arguments);
if (data.substring(0, 7) == "#EXTM3U" || data.includes("#EXTINF:")) {
m3u8Text += data;
if (m3u8Text.includes("#EXT-X-ENDLIST")) {
toUrl(m3u8Text.split("#EXT-X-ENDLIST")[0] + "#EXT-X-ENDLIST");
m3u8Text = '';
}
return data;
}
const key = data.replaceAll("\u0010", "");
if (key.length == 32) {
postData({ action: "catCatchAddKey", key: key, href: location.href, ext: "key" });
}
return data;
}
String.fromCharCode.toString = function () {
return _fromCharCode.toString();
}
// DataView
const _DataView = DataView;
DataView = new Proxy(_DataView, {
construct(target, args) {
let instance = new target(...args);
// 劫持常用的set方法
for (const methodName of ['setInt8', 'setUint8', 'setInt16', 'setUint16', 'setInt32', 'setUint32']) {
if (typeof instance[methodName] !== 'function') {
continue;
}
instance[methodName] = new Proxy(instance[methodName], {
apply(target, thisArg, argArray) {
const result = Reflect.apply(target, thisArg, argArray);
if (thisArg.byteLength == 16) {
postData({ action: "catCatchAddKey", key: thisArg.buffer, href: location.href, ext: "key" });
}
return result;
}
});
}
CATCH_SEARCH_DEBUG && console.log(target.name, args, instance);
if (instance.byteLength == 16 && instance.buffer.byteLength == 16) {
postData({ action: "catCatchAddKey", key: instance.buffer, href: location.href, ext: "key" });
}
if (instance.byteLength == 256 || instance.byteLength == 128 || instance.byteLength == 32) {
const _buffer = isRepeatedExpansion(instance.buffer, 16);
if (_buffer) {
postData({ action: "catCatchAddKey", key: _buffer, href: location.href, ext: "key" });
}
}
if (instance.byteLength == 32) {
const key = instance.buffer.slice(0, 16);
postData({ action: "catCatchAddKey", key: key, href: location.href, ext: "key" });
}
return instance;
}
});
// escape
const _escape = escape;
escape = function (str) {
CATCH_SEARCH_DEBUG && console.log(str);
if (str?.length && str.length == 24 && str.substring(22, 24) == "==") {
postData({ action: "catCatchAddKey", key: str, href: location.href, ext: "base64Key" });
}
return _escape(str);
}
escape.toString = function () {
return _escape.toString();
}
// indexOf
const _indexOf = String.prototype.indexOf;
String.prototype.indexOf = function (searchValue, fromIndex) {
const out = _indexOf.apply(this, arguments);
// CATCH_SEARCH_DEBUG && console.log(this, searchValue, fromIndex, out);
if (searchValue === '#EXTM3U' && out !== -1) {
const data = this.substring(fromIndex);
toUrl(data);
}
return out;
}
String.prototype.indexOf.toString = function () {
return _indexOf.toString();
}
const uint32ArrayToUint8Array_ = (array) => {
const newArray = new Uint8Array(16);
for (let i = 0; i < 4; i++) {
newArray[i * 4] = (array[i] >> 24) & 0xff;
newArray[i * 4 + 1] = (array[i] >> 16) & 0xff;
newArray[i * 4 + 2] = (array[i] >> 8) & 0xff;
newArray[i * 4 + 3] = array[i] & 0xff;
}
return newArray;
}
const uint16ArrayToUint8Array_ = (array) => {
const newArray = new Uint8Array(16);
for (let i = 0; i < 8; i++) {
newArray[i * 2] = (array[i] >> 8) & 0xff;
newArray[i * 2 + 1] = array[i] & 0xff;
}
return newArray;
}
// findTypedArray
const findTypedArray = (target, args) => {
const isArray = Array.isArray(args[0]) && args[0].length === 16;
const isArrayBuffer = args[0] instanceof ArrayBuffer && args[0].byteLength === 16;
const instance = new target(...args);
CATCH_SEARCH_DEBUG && console.log(target.name, args, instance);
if (isArray || isArrayBuffer) {
postData({ action: "catCatchAddKey", key: args[0], href: location.href, ext: "key" });
} else if (instance.buffer.byteLength === 16) {
if (target.name === 'Uint32Array') {
postData({ action: "catCatchAddKey", key: uint32ArrayToUint8Array_(instance).buffer, href: location.href, ext: "key" });
} else if (target.name === 'Uint16Array') {
postData({ action: "catCatchAddKey", key: uint16ArrayToUint8Array_(instance).buffer, href: location.href, ext: "key" });
} else {
postData({ action: "catCatchAddKey", key: instance.buffer, href: location.href, ext: "key" });
}
}
return instance;
}
// Uint8Array
const _Uint8Array = Uint8Array;
Uint8Array = new Proxy(_Uint8Array, {
construct(target, args) {
return findTypedArray(target, args);
}
});
// Uint16Array
const _Uint16Array = Uint16Array;
Uint16Array = new Proxy(_Uint16Array, {
construct(target, args) {
return findTypedArray(target, args);
}
});
// Uint32Array
const _Uint32Array = Uint32Array;
Uint32Array = new Proxy(_Uint32Array, {
construct(target, args) {
return findTypedArray(target, args);
}
});
// join
const _arrayJoin = Array.prototype.join;
Array.prototype.join = function () {
const data = _arrayJoin.apply(this, arguments);
// CATCH_SEARCH_DEBUG && console.log(data, this, arguments);
if (data.substring(0, 7).toUpperCase() == "#EXTM3U") {
toUrl(data);
}
if (data.length == 24) {
// 判断是否是base64
CATCH_SEARCH_DEBUG && console.log(data, this, arguments);
base64Regex.test(data) && postData({ action: "catCatchAddKey", key: data, href: location.href, ext: "base64Key" });
}
return data;
}
Array.prototype.join.toString = function () {
return _arrayJoin.toString();
}
function isUrl(str) {
return (str.startsWith("http://") || str.startsWith("https://") || str.startsWith("//"));
}
function isFullM3u8(text) {
let tsLists = text.split("\n");
for (let ts of tsLists) {
if (ts[0] == "#") { continue; }
if (isUrl(ts)) { return true; }
return false;
}
return false;
}
function TsProtocol(text) {
let tsLists = text.split("\n");
for (let i in tsLists) {
if (tsLists[i][0] == "#") { continue; }
if (tsLists[i].startsWith("//")) {
tsLists[i] = location.protocol + tsLists[i];
}
}
// return tsLists.join("\n");
return _arrayJoin.call(tsLists, "\n");
}
function getBaseUrl(url) {
let bashUrl = url.split("/");
bashUrl.pop();
// return baseUrl.join("/") + "/";
return _arrayJoin.call(bashUrl, "/") + "/";
}
function addBaseUrl(baseUrl, m3u8Text) {
let m3u8_split = m3u8Text.split("\n");
m3u8Text = "";
for (let ts of m3u8_split) {
if (ts == "" || ts == " " || ts == "\n") { continue; }
if (ts.includes("URI=")) {
let KeyURL = reKeyURL.exec(ts);
if (KeyURL && KeyURL[1] && !isUrl(KeyURL[1])) {
ts = ts.replace(reKeyURL, 'URI="' + baseUrl + KeyURL[1] + '"');
}
}
if (ts[0] != "#" && !isUrl(ts)) {
if (ts.startsWith("/")) {
// url根目录
const urlSplit = baseUrl.split("/");
ts = urlSplit[0] + "//" + urlSplit[2] + ts;
} else {
ts = baseUrl + ts;
}
}
m3u8Text += ts + "\n";
}
return m3u8Text;
}
function isJSON(str) {
if (typeof str == "object") {
return str;
}
if (typeof str == "string") {
try {
return _JSONparse(str);
} catch (e) { return false; }
}
return false;
}
function getExtension(str) {
let ext;
try {
if (str.startsWith("//")) {
str = location.protocol + str;
}
ext = new URL(str);
} catch (e) { return undefined; }
ext = ext.pathname.split(".");
if (ext.length == 1) { return undefined; }
ext = ext[ext.length - 1].toLowerCase();
if (ext == "m3u8" ||
ext == "m3u" ||
ext == "mpd" ||
ext == "mp4" ||
ext == "mp3" ||
ext == "flv" ||
ext == "key"
) { return ext; }
return false;
}
function toUrl(text, ext = "m3u8") {
if (!text) { return; }
// 处理ts地址无protocol
text = TsProtocol(text);
if (isFullM3u8(text)) {
let url = URL.createObjectURL(new Blob([new TextEncoder("utf-8").encode(text)]));
postData({ action: "catCatchAddMedia", url: url, href: location.href, ext: ext });
return;
}
baseUrl.forEach((url) => {
url = URL.createObjectURL(new Blob([new TextEncoder("utf-8").encode(addBaseUrl(url, text))]));
postData({ action: "catCatchAddMedia", url: url, href: location.href, ext: ext });
});
joinBaseUrlTask.push((url) => {
url = URL.createObjectURL(new Blob([new TextEncoder("utf-8").encode(addBaseUrl(url, text))]));
postData({ action: "catCatchAddMedia", url: url, href: location.href, ext: ext });
});
}
function getDataM3U8(text) {
text = text.substring(text.indexOf('/') + 1);
const mimeTypes = ["vnd.apple.mpegurl", "x-mpegurl", "mpegurl"];
const matchedType = mimeTypes.find(type =>
text.toLowerCase().startsWith(type)
);
if (!matchedType) return false;
const remainingText = text.slice(matchedType.length + 1);
const [prefix, data] = remainingText.split(/,(.+)/);
return prefix.toLowerCase() === 'base64'
? _atob(data)
: remainingText;
}
function postData(data) {
let value = data.url ? data.url : data.key;
if (value instanceof ArrayBuffer || value instanceof Array) {
if (value.byteLength == 0) { return; }
if (data.action == "catCatchAddKey") {
// 判断是否ftyp
const uint8 = new _Uint8Array(value);
if ((uint8[4] === 0x73 || uint8[4] === 0x66) && uint8[5] == 0x74 && uint8[6] == 0x79 && uint8[7] == 0x70) {
return;
}
}
data.key = ArrayToBase64(value);
value = data.key;
}
/**
* AAAAAAAA... 空数据
*/
if (data.action == "catCatchAddKey" && (data.key.startsWith("AAAAAAAAAAAAAAAAAAAA"))) {
return;
}
if (filter.has(value)) { return false; }
filter.add(value);
data.requestId = Date.now().toString() + filter.size;
_postMessage(data);
}
function ArrayToBase64(data) {
try {
let bytes = new _Uint8Array(data);
let binary = "";
for (let i = 0; i < bytes.byteLength; i++) {
binary += _fromCharCode(bytes[i]);
}
if (typeof _btoa == "function") {
return _btoa(binary);
}
return _btoa(binary);
} catch (e) {
return false;
}
}
function isRepeatedExpansion(array, expansionLength) {
let _buffer = new _Uint8Array(expansionLength);
array = new _Uint8Array(array);
for (let i = 0; i < expansionLength; i++) {
_buffer[i] = array[i];
for (let j = i + expansionLength; j < array.byteLength; j += expansionLength) {
if (array[i] !== array[j]) {
return false;
}
}
}
return _buffer.buffer;
}
function extractBaseUrl(url) {
let urlSplit = url.split("/");
urlSplit.pop();
urlSplit = urlSplit.join("/") + "/";
if (!baseUrl.has(urlSplit)) {
joinBaseUrlTask.forEach(fn => fn(urlSplit));
baseUrl.add(urlSplit);
}
}
// vimeo json 翻译为 m3u8
async function vimeo(originalUrl, json) {
if (!json || !regexVimeo.test(originalUrl) || videoSet.has(originalUrl)) return;
const data = isJSON(json);
if (!data?.base_url || !data?.video) return;
videoSet.add(originalUrl);
try {
const url = new URL(originalUrl);
const pathBase = url.pathname.substring(0, url.pathname.lastIndexOf('/')) + "/";
const baseURL = new URL(url.origin + pathBase + data.base_url).href;
let M3U8List = ["#EXTM3U", "#EXT-X-INDEPENDENT-SEGMENTS", "#EXT-X-VERSION:3"];
const toM3U8 = (stream) => {
if (!stream.segments || stream.segments.length == 0) return null;
let M3U8 = [
"#EXTM3U",
"#EXT-X-VERSION:3",
`#EXT-X-TARGETDURATION:${stream.duration}`,
"#EXT-X-MEDIA-SEQUENCE:0",
"#EXT-X-PLAYLIST-TYPE:VOD"
];
if (stream.init_segment) {
M3U8.push(`#EXT-X-MAP:URI="data:application/octet-stream;base64,${stream.init_segment}"`);
} else if (stream.init_segment_url) {
M3U8.push(`#EXT-X-MAP:URI="${baseURL}${stream.base_url}${stream.init_segment_url}"`);
}
for (const segment of stream.segments) {
M3U8.push(`#EXTINF:${segment.end - segment.start},`);
M3U8.push(`${baseURL}${stream.base_url}${segment.url}`);
}
M3U8.push("#EXT-X-ENDLIST");
return URL.createObjectURL(
new Blob([new TextEncoder("utf-8").encode(_arrayJoin.call(M3U8, "\n"))])
);
}
if (data.video) {
for (const stream of data.video) {
const blobUrl = toM3U8(stream);
if (!blobUrl) continue;
M3U8List.push(`#EXT-X-STREAM-INF:BANDWIDTH=${stream.bitrate},RESOLUTION=${stream.width}x${stream.height},CODECS="${stream.codecs}"`);
M3U8List.push(blobUrl);
}
}
if (data.audio) {
for (const stream of data.audio) {
const blobUrl = toM3U8(stream);
if (!blobUrl) continue;
M3U8List.push(`#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="${stream.id}",NAME="${stream.bitrate}",URI="${blobUrl}"`);
}
}
const blobUrl = URL.createObjectURL(
new Blob([new TextEncoder("utf-8").encode(_arrayJoin.call(M3U8List, "\n"))])
);
postData({ action: "catCatchAddMedia", url: blobUrl, href: location.href, ext: "m3u8" });
} catch (e) {
CATCH_SEARCH_DEBUG && console.error("Error processing Vimeo stream:", e);
}
}
})();

320
catch-script/webrtc.js Normal file
View File

@@ -0,0 +1,320 @@
(function () {
console.log("webrtc.js Start");
if (document.getElementById("catCatchWebRTC")) { return; }
// 多语言
let language = navigator.language.replace("-", "_");
if (window.CatCatchI18n) {
if (!window.CatCatchI18n.languages.includes(language)) {
language = language.split("_")[0];
if (!window.CatCatchI18n.languages.includes(language)) {
language = "en";
}
}
}
const buttonStyle = 'style="border:solid 1px #000;margin:2px;padding:2px;background:#fff;border-radius:4px;border:solid 1px #c7c7c780;color:#000;"';
const checkboxStyle = 'style="-webkit-appearance: auto;"';
const CatCatch = document.createElement("div");
CatCatch.innerHTML = `<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYBAMAAAASWSDLAAAAKlBMVEUAAADLlROxbBlRAD16GS5oAjWWQiOCIytgADidUx/95gHqwwTx0gDZqwT6kfLuAAAACnRSTlMA/vUejV7kuzi8za0PswAAANpJREFUGNNjwA1YSxkYTEqhnKZLLi6F1w0gnKA1shdvHYNxdq1atWobjLMKCOAyC3etlVrUAOH4HtNZmLgoAMKpXX37zO1FwcZAwMDguGq1zKpFmTNnzqx0Bpp2WvrU7ttn9py+I8JgLn1R8Pad22vurNkjwsBReHv33junzuyRnOnMwNCSeFH27K5dq1SNgcZxFMnuWrNq1W5VkNntihdv7ToteGcT0C7mIkE1qbWCYjJnM4CqEoWKdoslChXuUgXJqIcLebiphSgCZRhaPDhcDFhdmUMCGIgEAFA+Uc02aZg9AAAAAElFTkSuQmCC" style="-webkit-user-drag: none;width: 20px;">
<div id="tips" data-i18n="waiting">正在等待视频流..."</div>
<div id="time"></div>
${i18n("selectVideo", "选择视频")}:
<select id="videoTrack">
<option value="-1">${i18n("selectVideo", "选择视频")}</option>
</select>
${i18n("selectAudio", "选择音频")}:
<select id="audioTrack">
<option value="-1">${i18n("selectVideo", "选择视频")}</option>
</select>
${i18n("recordEncoding", "录制编码")}: <select id="mimeTypeList" style="max-width: 200px;"></select>
<label><input type="checkbox" id="autoSave1"} ${checkboxStyle} data-i18n="save1hour">1小时保存一次</label>
<label>
<select id="videoBits">
<option value="2500000" data-i18n="videoBits">视频码率</option>
<option value="2500000">2.5 Mbps</option>
<option value="5000000">5 Mbps</option>
<option value="8000000">8 Mbps</option>
<option value="16000000">16 Mbps</option>
</select>
<select id="audioBits">
<option value="128000" data-i18n="audioBits">音频码率</option>
<option value="128000">128 kbps</option>
<option value="256000">256 kbps</option>
</select>
</label>
<div>
<button id="start" ${buttonStyle} data-i18n="startRecording">开始录制</button>
<button id="stop" ${buttonStyle} data-i18n="stopRecording">停止录制</button>
<button id="save" ${buttonStyle} data-i18n="save">保存</button>
<button id="hide" ${buttonStyle} data-i18n="hide">隐藏</button>
<button id="close" ${buttonStyle} data-i18n="close">关闭</button>
</div>`;
CatCatch.style = `
position: fixed;
z-index: 999999;
top: 10%;
left: 80%;
background: rgb(255 255 255 / 85%);
border: solid 1px #c7c7c7;
border-radius: 4px;
color: rgb(26, 115, 232);
padding: 5px 5px 5px 5px;
font-size: 12px;
font-family: "Microsoft YaHei", "Helvetica", "Arial", sans-serif;
user-select: none;
display: flex;
align-items: flex-start;
justify-content: space-evenly;
flex-direction: column;
line-height: 20px;`;
// 创建 Shadow DOM 放入CatCatch
const divShadow = document.createElement('div');
const shadowRoot = divShadow.attachShadow({ mode: 'closed' });
shadowRoot.appendChild(CatCatch);
// 页面插入Shadow DOM
document.getElementsByTagName('html')[0].appendChild(divShadow);
// 提示
const $tips = CatCatch.querySelector("#tips");
const tips = (text) => {
$tips.innerHTML = text;
}
// 开始 结束 按钮切换
const $start = CatCatch.querySelector("#start");
const $stop = CatCatch.querySelector("#stop");
const buttonState = (state = true) => {
$start.style.display = state ? 'inline' : 'none';
$stop.style.display = state ? 'none' : 'inline';
}
$start.style.display = 'inline';
$stop.style.display = 'none';
// 关闭
CatCatch.querySelector("#close").addEventListener('click', function (event) {
recorder?.state && recorder.stop();
CatCatch.style.display = "none";
window.postMessage({ action: "catCatchToBackground", Message: "script", script: "webrtc.js", refresh: true });
});
// 隐藏
CatCatch.querySelector("#hide").addEventListener('click', function (event) {
CatCatch.style.display = "none";
});
const tracks = { video: [], audio: [] };
const $tracks = { video: CatCatch.querySelector('#videoTrack'), audio: CatCatch.querySelector('#audioTrack') };
/* 核心变量 */
let recorder = null; // 录制器
let autoSave1Timer = null; // 1小时保存一次
// #region 编码选择
let option = { mimeType: 'video/webm;codecs=vp9,opus' };
function getSupportedMimeTypes(media, types, codecs) {
const supported = [];
types.forEach((type) => {
const mimeType = `${media}/${type}`;
codecs.forEach((codec) => [`${mimeType};codecs=${codec}`].forEach(variation => {
if (MediaRecorder.isTypeSupported(variation)) {
supported.push(variation);
}
}));
if (MediaRecorder.isTypeSupported(mimeType)) {
supported.push(mimeType);
}
});
return supported;
};
const $mimeTypeList = CatCatch.querySelector("#mimeTypeList");
const videoTypes = ["webm", "ogg", "mp4", "x-matroska"];
const codecs = ["should-not-be-supported", "vp9", "vp8", "avc1", "av1", "h265", "h.265", "h264", "h.264", "opus", "pcm", "aac", "mpeg", "mp4a"];
const supportedVideos = getSupportedMimeTypes("video", videoTypes, codecs);
supportedVideos.forEach(function (type) {
$mimeTypeList.options.add(new Option(type, type));
});
option.mimeType = supportedVideos[0];
$mimeTypeList.addEventListener('change', function (event) {
if (recorder && recorder.state && recorder.state === 'recording') {
tips(i18n("recordingChangeEncoding", "录制中不能更改编码"));
return;
}
if (MediaRecorder.isTypeSupported(event.target.value)) {
option.mimeType = event.target.value;
tips(`${i18n("recordEncoding", "录制编码")}:` + event.target.value);
} else {
tips(i18n("formatNotSupported", "不支持此格式"));
}
});
// #endregion 编码选择
// 录制
$time = CatCatch.querySelector("#time");
CatCatch.querySelector("#start").addEventListener('click', function () {
if (!tracks.video.length && !tracks.audio.length) {
tips(i18n("streamEmpty", "媒体流为空"));
return;
}
let recorderTime = 0;
let recorderTimeer = undefined;
let chunks = [];
// 音频 视频 选择
const videoTrack = +CatCatch.querySelector("#videoTrack").value;
const audioTrack = +CatCatch.querySelector("#audioTrack").value;
const streamTrack = [];
if (videoTrack !== -1 && tracks.video[videoTrack]) {
streamTrack.push(tracks.video[videoTrack]);
}
if (audioTrack !== -1 && tracks.audio[audioTrack]) {
streamTrack.push(tracks.audio[audioTrack]);
}
// 码率
option.audioBitsPerSecond = +CatCatch.querySelector("#audioBits").value;
option.videoBitsPerSecond = +CatCatch.querySelector("#videoBits").value;
const mediaStream = new MediaStream(streamTrack);
recorder = new MediaRecorder(mediaStream, option);
recorder.ondataavailable = event => {
chunks.push(event.data)
};
recorder.onstop = () => {
recorderTime = 0;
clearInterval(recorderTimeer);
clearInterval(autoSave1Timer);
$time.innerHTML = "";
tips(i18n("stopRecording", "已停止录制!"));
download(chunks);
buttonState();
}
recorder.onstart = () => {
chunks = [];
tips(i18n("recording", "视频录制中"));
$time.innerHTML = "00:00";
recorderTimeer = setInterval(function () {
recorderTime++;
$time.innerHTML = secToTime(recorderTime);
}, 1000);
buttonState(false);
}
recorder.onerror = (msg) => {
console.error(msg);
}
recorder.start(60000);
});
// 停止录制
CatCatch.querySelector("#stop").addEventListener('click', function () {
if (recorder) {
recorder.stop();
recorder = undefined;
}
});
// 保存
CatCatch.querySelector("#save").addEventListener('click', function () {
if (recorder) {
recorder.stop();
recorder.start();
}
});
// 每1小时 保存一次
CatCatch.querySelector("#autoSave1").addEventListener('click', function () {
clearInterval(autoSave1Timer);
if (CatCatch.querySelector("#autoSave1").checked) {
autoSave1Timer = setInterval(function () {
if (recorder) {
recorder.stop();
recorder.start();
}
}, 3600000);
}
});
// 获取webRTC流
window.RTCPeerConnection = new Proxy(window.RTCPeerConnection, {
construct(target, args) {
const pc = new target(...args);
pc.addEventListener('track', (event) => {
const track = event.track;
if (track.kind === 'video' || track.kind === 'audio') {
tips(`${track.kind} ${i18n("streamAdded", "流已添加")}`);
$tracks[track.kind].appendChild(new Option(track.label, tracks[track.kind].length));
$tracks[track.kind].value = tracks[track.kind].length;
tracks[track.kind].push(track);
if (tracks.video.length && tracks.audio.length) {
tips(i18n("videoAndAudio", "已包含音频和视频流"));
}
}
});
pc.addEventListener('iceconnectionstatechange', (event) => {
if (pc.iceConnectionState === 'disconnected' && recorder?.state === 'recording') {
recorder.stop();
tips(i18n("stopRecording", "连接已断开,录制已停止"));
}
});
return pc;
}
});
// #region 移动逻辑
let x, y;
const move = (event) => {
CatCatch.style.left = event.pageX - x + 'px';
CatCatch.style.top = event.pageY - y + 'px';
}
CatCatch.addEventListener('mousedown', function (event) {
x = event.pageX - CatCatch.offsetLeft;
y = event.pageY - CatCatch.offsetTop;
document.addEventListener('mousemove', move);
document.addEventListener('mouseup', function () {
document.removeEventListener('mousemove', move);
});
});
// #endregion 移动逻辑
function download(chunks) {
const blob = new Blob(chunks, { type: option.mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = 'recorded-video.mp4';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
}
// 秒转换成时间
function secToTime(sec) {
let hour = (sec / 3600) | 0;
let min = ((sec % 3600) / 60) | 0;
sec = (sec % 60) | 0;
let time = hour > 0 ? hour + ":" : "";
time += min.toString().padStart(2, '0') + ":";
time += sec.toString().padStart(2, '0');
return time;
}
// 防止网页意外关闭跳转
window.addEventListener('beforeunload', function (e) {
recorder && recorder.stop();
return true;
});
// i18n
if (window.CatCatchI18n) {
CatCatch.querySelectorAll('[data-i18n]').forEach(function (element) {
element.innerHTML = window.CatCatchI18n[element.dataset.i18n][language];
});
CatCatch.querySelectorAll('[data-i18n-outer]').forEach(function (element) {
element.outerHTML = window.CatCatchI18n[element.dataset.i18nOuter][language];
});
}
function i18n(key, original = "") {
if (!window.CatCatchI18n) { return original };
return window.CatCatchI18n[key][language];
}
})();

411
css/install.css Normal file
View File

@@ -0,0 +1,411 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
}
:root {
--primary: #4361ee;
--primary-light: #4895ef;
--secondary: #06d6a0;
--danger: #ef476f;
--light: #f8f9fa;
--dark: #212529;
--gray: #6c757d;
--border-radius: 16px;
--shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
--transition: all 0.3s ease;
/* 新增深色模式变量 */
--bg-gradient-start: #f0f4ff;
--bg-gradient-end: #e6f7ff;
--card-bg: white;
--card-header-bg: linear-gradient(to right, #f8f9fa, #e9ecef);
--text-color: #212529;
--subtitle-color: #6c757d;
--content-bg: #f8f9fa;
--agreement-bg: #f8f9fa;
--agreement-border: rgba(0, 0, 0, 0.1);
--agreement-color: #6c757d;
}
body {
background: linear-gradient(
135deg,
var(--bg-gradient-start) 0%,
var(--bg-gradient-end) 100%
);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
color: var(--text-color);
line-height: 1.6;
}
.container {
width: 100%;
max-width: 800px;
position: relative;
}
/* 语言切换按钮 */
.lang-switch {
position: absolute;
top: 20px;
right: 20px;
z-index: 10;
background: var(--card-bg);
border-radius: 50px;
padding: 10px 18px;
display: flex;
align-items: center;
gap: 10px;
box-shadow: var(--shadow);
cursor: pointer;
transition: var(--transition);
font-weight: 600;
font-size: 1rem;
}
.lang-switch:hover {
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
background: var(--primary);
color: white;
}
.lang-emoji {
font-size: 1.3rem;
}
/* 头部样式 */
.header {
text-align: center;
margin-bottom: 30px;
position: relative;
padding-top: 20px;
}
.logo-container {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
.logo {
width: 120px;
height: 120px;
background: linear-gradient(135deg, var(--primary), var(--primary-light));
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: var(--shadow);
animation: float 3s ease-in-out infinite;
font-size: 3.5rem;
}
h1 {
font-size: 2.5rem;
font-weight: 800;
margin-bottom: 10px;
background: linear-gradient(to right, var(--primary), var(--primary-light));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
font-size: 1.2rem;
color: var(--subtitle-color);
font-weight: 500;
max-width: 600px;
margin: 0 auto;
}
/* 卡片样式 */
.card {
background: var(--card-bg);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
overflow: hidden;
margin-bottom: 30px;
transform: translateY(0);
transition: var(--transition);
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15);
}
.card-header {
padding: 20px;
background: var(--card-header-bg);
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.card-title {
font-size: 1.5rem;
font-weight: 600;
color: var(--primary);
display: flex;
align-items: center;
gap: 10px;
}
.card-title .emoji {
font-size: 1.5rem;
}
.card-body {
padding: 25px;
}
.policy-section {
margin-top: 30px;
}
.section-title {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 15px;
color: var(--primary);
display: flex;
align-items: center;
gap: 10px;
}
.section-title .emoji {
font-size: 1.3rem;
}
.content-box {
background-color: var(--content-bg);
border-radius: 12px;
padding: 20px;
margin-top: 10px;
border: 1px solid rgba(0, 0, 0, 0.05);
line-height: 1.7;
display: flex;
justify-content: center;
}
.content-box a {
color: var(--primary);
font-weight: 600;
text-decoration: none;
transition: var(--transition);
position: relative;
font-size: 1.1rem;
padding: 10px 20px;
border-radius: 30px;
background-color: rgba(67, 97, 238, 0.1);
display: flex;
align-items: center;
gap: 8px;
}
.content-box a:hover {
background-color: rgba(67, 97, 238, 0.2);
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(67, 97, 238, 0.2);
}
.content-box a .emoji {
font-size: 1.2rem;
}
.content-box p {
font-size: 1rem;
}
.agreement {
text-align: center;
padding: 20px;
background-color: var(--agreement-bg);
border-radius: var(--border-radius);
margin: 25px 0;
font-style: italic;
color: var(--agreement-color);
border: 1px dashed var(--agreement-border);
display: flex;
flex-direction: column;
gap: 10px;
}
.agreement .emoji {
font-size: 1.5rem;
}
.buttons {
display: flex;
gap: 20px;
margin-top: 30px;
}
.btn {
flex: 1;
padding: 16px 20px;
border: none;
border-radius: 50px;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.btn-primary {
background: linear-gradient(to right, var(--primary), var(--primary-light));
color: white;
box-shadow: 0 4px 15px rgba(67, 97, 238, 0.3);
}
.btn-primary:hover {
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(67, 97, 238, 0.4);
background: linear-gradient(to right, var(--primary-light), var(--primary));
}
.btn-outline {
background: transparent;
color: var(--danger);
border: 2px solid var(--danger);
}
.btn-outline:hover {
background-color: rgba(239, 71, 111, 0.05);
transform: translateY(-3px);
}
/* 多语言内容控制 */
.lang-zh,
.lang-en {
display: none;
}
.lang-zh.active,
.lang-en.active {
display: block;
}
/* 动画效果 */
@keyframes float {
0% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
100% {
transform: translateY(0px);
}
}
.fade-in {
animation: fadeIn 0.5s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 响应式设计 */
@media (max-width: 768px) {
h1 {
font-size: 2rem;
}
.buttons {
flex-direction: column;
gap: 12px;
}
.card-body {
padding: 20px;
}
.subtitle {
font-size: 1.1rem;
}
}
@media (max-width: 480px) {
h1 {
font-size: 1.8rem;
}
.subtitle {
font-size: 1rem;
}
.logo {
width: 90px;
height: 90px;
font-size: 2.8rem;
}
.lang-switch {
top: 10px;
right: 10px;
padding: 8px 14px;
font-size: 0.9rem;
}
.content-box a {
font-size: 0.95rem;
padding: 8px 15px;
}
}
/* 深色模式支持 */
@media (prefers-color-scheme: dark) {
:root {
--bg-gradient-start: #0f172a;
--bg-gradient-end: #1e293b;
--card-bg: #1e293b;
--card-header-bg: linear-gradient(to right, #334155, #1e293b);
--text-color: #e2e8f0;
--subtitle-color: #94a3b8;
--content-bg: #334155;
--agreement-bg: #334155;
--agreement-border: rgba(255, 255, 255, 0.1);
--agreement-color: #cbd5e1;
--shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.lang-switch {
background: #334155;
color: #e2e8f0;
}
.content-box a {
background-color: rgba(67, 97, 238, 0.15);
color: #93c5fd;
}
.content-box a:hover {
background-color: rgba(67, 97, 238, 0.25);
box-shadow: 0 4px 10px rgba(67, 97, 238, 0.2);
}
.btn-outline:hover {
background-color: rgba(239, 71, 111, 0.15);
}
}

16
css/mobile.css Normal file
View File

@@ -0,0 +1,16 @@
.popupBody {
width: 100%;
}
.wrapper.options {
margin-right: 10px;
}
.m3u8_wrapper #mergeTs {
font-size: 2rem;
}
.newDownload {
width: 100%;
padding: 0 2rem;
}

602
css/options.css Normal file
View File

@@ -0,0 +1,602 @@
body {
background: var(--background-color);
font-size: 13px;
font-family: "Microsoft YaHei", "Helvetica", "Arial", sans-serif;
margin: 0;
}
.wrapper {
margin: 0 auto;
width: 45rem;
}
.error {
color: var(--text-error-color);
}
h1 {
font-size: 1.125em;
font-weight: normal;
margin: 0;
}
h2 {
font-size: 1.125em;
font-weight: normal;
margin: 0;
}
p {
margin: auto;
}
.optionBox {
background: var(--optionBox-color);
border-radius: 4px;
box-shadow: 0 1px 2px 0 rgb(60 64 67 / 30%), 0 1px 3px 1px rgb(60 64 67 / 15%);
padding: 0.75em 1.25em;
margin-top: 5px;
}
table {
width: 100%;
text-align: center;
}
input,
textarea {
padding: 5px 5px;
}
input.ext {
width: 100px;
text-align: center;
}
input.type {
width: 200px;
text-align: center;
}
input.size {
width: 100px;
text-align: center;
}
input.regexType {
width: 20px;
text-align: center;
}
input.regexExt {
width: 35px;
text-align: center;
}
input.regex {
width: 320px;
text-align: center;
}
/* input#OtherAutoClear {
margin-left: 250px;
width: 45px;
} */
/* 滑动开关 组件 */
.switch {
height: 22px;
width: 50px;
margin: auto;
}
.switch .switchRound {
position: relative;
display: block;
width: 100%;
height: 100%;
background-color: var(--switch-off-color);
transition: all 0.2s ease-in-out;
}
.switch .switchRoundBtn {
display: block;
position: absolute;
top: 2px;
left: 3px;
bottom: 3px;
width: 18px;
height: 18px;
background-color: var(--switch-round-color);
transition: all 0.2s ease-in-out;
}
.switch .switchInput {
display: none;
}
.switch .switchInput:checked + .switchRound {
background-color: var(--switch-on-color);
}
.switch .switchInput:checked + .switchRound > .switchRoundBtn {
left: 29px;
}
.switch .switchRadius {
border-radius: 50px;
}
/* 滑动开关 组件 END */
.list {
padding-left: 10px;
padding-top: 5px;
}
.item {
align-items: center;
display: flex;
min-height: 30px;
border-bottom: solid 1px rgba(0, 0, 0, 0.06);
flex-wrap: wrap;
align-items: flex-end;
align-content: space-around;
}
.item .switch {
margin-right: 50px;
}
.item .switchSelect {
margin-right: 85px;
}
.optionsTitle {
margin-top: 20px;
}
.RemoveButton {
fill: var(--text-color);
height: 20px;
cursor: pointer;
}
button,
.button,
.button2 {
padding: calc(0.5em - 1px) 1em;
margin: 5px 5px 5px 5px;
/* font-size: 13px; */
}
.flex-end {
display: flex;
justify-content: flex-end;
}
.explain {
color: #6c6c6c;
}
#typeList,
#extList {
margin-top: 10px;
}
.loose .item {
margin-bottom: 5px;
min-height: 35px;
}
#m3u8_url,
#mpd_url,
.test_url {
overflow: hidden;
display: block;
text-overflow: ellipsis;
word-break: break-all;
color: var(--text2-color);
}
.block {
border-bottom: solid 1px rgba(0, 0, 0, 0.06);
padding-bottom: 5px;
margin-bottom: 5px;
}
.m3u8_wrapper .block {
border-bottom: 0px;
}
.wrapper1024 {
margin: 0 auto;
width: 1024px;
}
.wrapper1080 {
margin: 0 auto;
width: 1080px;
}
textarea {
font-size: 12px;
font-family: "Microsoft YaHei", "Helvetica", "Arial", sans-serif;
}
#textarea {
text-align: center;
}
.m3u8_wrapper video {
max-height: 80vh;
max-width: 100%;
}
#media_file {
word-break: break-all;
}
#media_file,
#jsonText,
#m3u8Text {
height: 55vh;
}
/* #media_file {
font-size: 12px;
font-family: "Microsoft YaHei", "Helvetica", "Arial", sans-serif;
height: 700px;
overflow-y: auto;
border: solid 1.5px rgb(0 0 0 / 50%);
word-break: break-all;
} */
#formatStr {
width: 145px;
}
#tips input {
color: var(--text2-color);
}
.keyUrl {
width: 1034px;
}
.fullInput {
/* width: 975px; */
width: 100%;
margin: 5px 0 5px 0;
}
.select {
appearance: none;
background: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIxMiIgZmlsbD0iIzVGNjM2OCI+PHBhdGggZD0iTTAgMGgyNEwxMiAxMnoiLz48L3N2Zz4=)
calc(100% - 8px) center no-repeat;
/* background-color: rgb(241, 243, 244); */
background-color: var(--background-color-two);
background-size: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
padding: 5px 10px;
}
.select {
width: 8rem;
}
.m3u8Key {
width: 300px;
}
#PlayerTemplate {
width: 200px;
}
#errorTsList p {
color: red;
word-break: break-all;
}
.progress-bar {
height: 15px;
background-color: rgb(189, 193, 198);
border-radius: 3px;
margin: 3px;
margin-bottom: 10px;
}
.progress {
width: 0px;
height: 100%;
background-color: var(--text2-color);
border-radius: 3px;
}
#fileSize,
#fileDuration {
margin-left: 20px;
}
.not-allowed {
cursor: not-allowed;
background-color: #ccc;
color: #fff;
}
.not-allowed:hover {
background: #ccc;
}
.not-allowed:active {
background: #ccc;
}
#showM3u8Help {
margin-left: 10px;
margin-top: 1px;
margin-right: 0px;
padding: 2px;
}
.m3u8checkbox {
display: flex;
cursor: pointer;
flex-direction: column;
user-select: none;
margin: 0 5px 0 5px;
}
.merge {
display: flex;
justify-content: flex-start;
margin-top: 5px;
align-items: center;
}
.customKey input {
margin-right: 5px;
}
/* .wrapper .button {
margin-top: 5px;
} */
.rangeDown {
display: flex;
flex-direction: column;
align-items: center;
margin-right: 10px;
}
.rangeDown .merge {
margin-top: 0;
}
#rangeStart,
#rangeEnd {
width: 55px;
/* text-align:center;
vertical-align:middle; */
margin-left: 2px;
margin-right: 2px;
padding-top: 3px;
padding-bottom: 3px;
}
#loading a {
word-break: break-all;
}
#next_m3u8 a {
word-break: break-all;
}
.key {
align-items: flex-end;
}
.key div {
display: flex;
flex-direction: column;
margin-right: 10px;
}
.key input {
width: 265px;
}
.method input {
width: 100px;
}
.offset {
width: 256px;
}
.videoInfo div {
margin-right: 5px;
}
.flex {
display: flex;
}
.m3u8dlArg {
margin-top: 10px;
height: 100px;
word-break: break-all;
width: 100%;
}
.m3u8DL {
margin-right: 70px !important;
}
/* .m3u8DL #m3u8dl{
width: 8rem;
} */
.break-all {
word-break: break-all;
}
/* MPD*/
.dash .select {
padding-right: 20px;
margin-bottom: 10px;
}
/* JSON格式化 */
.json-document {
margin-top: 0px;
}
ul.json-dict,
ol.json-array {
list-style-type: none;
margin: 0 0 0 1px;
border-left: 1px dotted #ccc;
padding-left: 2em;
}
.json-string {
color: #0b7500;
word-break: break-all;
white-space: break-spaces;
}
.json-literal {
color: #1a01cc;
font-weight: bold;
}
a.json-toggle {
position: relative;
color: inherit;
text-decoration: none;
}
a.json-toggle:focus {
outline: none;
}
a.json-toggle:before {
font-size: 1.1em;
color: #c0c0c0;
content: "\25BC";
position: absolute;
display: inline-block;
width: 1em;
text-align: center;
line-height: 1em;
left: -1.2em;
}
a.json-toggle:hover:before {
color: #aaa;
}
a.json-toggle.collapsed:before {
transform: rotate(-90deg);
}
a.json-placeholder {
color: #aaa;
padding: 0 1em;
text-decoration: none;
}
a.json-placeholder:hover {
text-decoration: underline;
}
#downList a {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
color: var(--text2-color);
}
#downList {
overflow: scroll;
height: 60vh;
text-align: left;
display: none;
width: 100%;
border: solid 1px var(--text-color);
}
.width3rem {
width: 3rem;
}
.popupAttr {
margin-left: 0.5rem;
}
.progress-container {
display: flex;
align-items: center;
gap: 10px;
}
.progress-wrapper {
flex: 1;
}
.newDownload .downItem {
margin-bottom: 1rem;
}
.newDownload .downItem .progress-bar {
margin-bottom: 0;
height: 20px;
}
.newDownload .downItem button {
margin: 0;
}
.newDownload .downItem .progress {
color: var(--background-color-two);
text-align: center;
transition: width 0.2s;
}
/** 导航条 **/
.sidebar {
position: fixed;
top: 0;
left: 0;
width: 10rem;
height: 100%;
padding: 10px;
background-color: var(--background-color-two);
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
overflow-y: auto;
text-align: center;
margin-right: 0;
}
.sidebar ul {
list-style-type: none;
padding: 0;
}
.sidebar li {
margin: 10px 0;
}
.sidebar a {
text-decoration: none;
color: var(--text-color);
display: block;
padding: 5px;
border-radius: 4px;
}
.sidebar a:hover {
background-color: var(--button-hover-color);
}
.item .send2localType {
margin-right: 196px;
}
.item .send2localType select {
width: 15rem;
}

267
css/popup.css Normal file
View File

@@ -0,0 +1,267 @@
a {
text-decoration: none;
word-break: break-all;
}
a:hover {
text-decoration: underline;
}
body {
font-family: arial, sans-serif;
font-size: 0.8rem;
width: 40rem;
overflow-x: hidden;
background: var(--background-color);
margin: 0;
}
.fixFirefoxRight {
margin-right: 5px;
}
.panel {
border: 1px solid #ddd0;
margin-bottom: 1px;
}
.panel-heading {
padding: 5px 5px 5px 5px;
background-color: var(--background-color-two);
cursor: pointer;
display: flex;
flex-direction: row;
align-items: center;
}
.panel-heading .name {
flex: auto;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
margin-right: 0.2rem;
}
.panel .url,
.panel .confirm {
padding: 5px;
}
.icon,
.favicon {
transition: all 0.1s;
width: 1.5rem;
height: 1.5rem;
cursor: pointer;
}
.faviconFlag {
display: none;
}
.icon:hover {
transform: scale(1.1);
}
.icon:active {
transform: scale(0.9);
}
.icon.mqtt-sending {
opacity: 0.5;
cursor: not-allowed;
}
.icon.mqtt-sending:hover {
transform: none;
}
.panel-heading .icon {
padding-left: 2px;
}
.favicon {
padding-right: 2px;
}
.panel-heading .size {
float: right;
font-weight: bold;
}
#Tips,
#TipsFixed {
left: 0;
right: 0;
text-align: center;
z-index: 9999;
pointer-events: none;
color: var(--text2-color);
font-weight: bold;
border: 1px solid #cdcdcd12;
border-radius: 2px;
background: var(--background-color-two);
padding: 0 10px;
margin-bottom: 1px;
}
#TipsFixed {
position: fixed;
display: none;
}
#preview {
max-height: 300px;
max-width: 100%;
text-align: center;
}
button,
.button2 {
padding: 3px 3px 3px 3px;
/* font-size: 0.9rem; */
}
.Tabs {
display: flex;
}
.TabButton {
text-align: center;
border: solid 1px #c7c7c700;
color: var(--text2-color);
border-radius: 5px 5px 0 0;
cursor: pointer;
width: 50%;
/* display: flex; */
padding: 3px;
margin: 1px 2px 0 2px;
flex-direction: row;
align-items: baseline;
justify-content: center;
user-select: none;
}
.flex {
display: flex;
}
.TabButton.Active {
background-color: var(--background-color-two);
border-bottom-color: transparent;
font-weight: bold;
}
.TabButton.Active div {
font-weight: bold;
}
.DownCheck {
margin: 0 2px 0 0;
width: 1.2rem;
height: 1.2rem;
flex: 0 0 auto;
}
.TabShow {
display: block !important;
}
#down,
.more {
display: flex;
flex-wrap: wrap;
position: fixed;
width: 100%;
z-index: 999;
background-color: var(--background-color-opacity);
}
#down {
bottom: 0;
justify-content: space-evenly;
}
.more {
display: none;
bottom: 26px;
justify-content: flex-start;
padding-bottom: 2px;
padding-top: 2px;
z-index: 9999;
}
.more button {
margin-left: 0.1rem;
font-size: 12px;
}
#filter {
flex-wrap: wrap;
}
#filter #regular button {
margin-left: 0px;
}
#filter #regular input {
width: 98%;
}
#filter .regular {
margin-left: 5px;
}
#filter #ext {
display: flex;
color: var(--text-color);
}
#filter div {
margin-left: 5px;
}
.flexFilter {
display: flex;
flex-wrap: wrap;
align-items: center;
}
.container {
margin-bottom: 30px;
}
#screenshots {
max-width: 100%;
max-height: 260px;
cursor: pointer;
margin: auto;
}
.flex-end {
justify-content: flex-end;
}
#otherOptions {
margin: 5px;
}
#PlayControl {
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: space-evenly;
}
#PlayControl .button2,
#PlayControl .button {
margin-left: 2px;
}
#PlayControl #playbackRate {
width: 3em;
height: 20px;
}
#otherOptions select {
margin-top: 2px;
margin-bottom: 2px;
width: 100%;
}
#PlayControl .loop {
margin: 0 5px 0 5px;
}
label {
cursor: pointer;
user-select: none;
}
#PlayControl .volume {
width: 100px;
}
.flexColumn {
display: flex;
flex-direction: column;
align-items: center;
}
.flexRow {
display: flex;
flex-direction: row;
align-items: center;
}
.nowrap {
word-break: keep-all;
}
.otherScript .button2,
.otherFeat .button2 {
width: 100%;
margin-right: 10px;
text-align: center;
}
.otherTips {
text-align: center;
color: var(--text2-color);
font-weight: bold;
}
.moreButton {
display: flex;
}
.moreButton div {
margin-right: 3px;
}
.panel .confirm {
text-align: center;
}

241
css/preview.css Normal file
View File

@@ -0,0 +1,241 @@
/* 基础样式 */
body {
margin: 0;
padding: 0;
height: 100vh;
user-select: none;
}
/* .container {
padding: 10px;
margin: 0 auto;
} */
/* 筛选区域 */
.filters {
display: grid;
gap: 5px;
/* margin-bottom: 10px; */
background: var(--background-color-two);
padding: 10px;
border-radius: 8px;
/* position: sticky; */
/* top: 0; */
/* z-index: 2; */
}
.filter-row {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.sort-options {
display: flex;
gap: 15px;
align-items: center;
}
.sort-group,
.sort-order {
display: flex;
gap: 8px;
}
#regular {
width: 512px;
}
input[type="radio"] {
vertical-align: bottom;
}
input[type="checkbox"] {
vertical-align: middle;
}
/* 文件列表 */
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 10px;
padding: 10px;
}
.file-item {
display: flex;
flex-direction: column;
min-height: 150px;
padding: 8px;
border: 3px solid transparent;
border-radius: 8px;
cursor: pointer;
box-shadow: 0 0 3px var(--button2-color);
max-height: 233px;
transition: all 0.2s;
}
.file-item:hover {
box-shadow: 0 0 10px var(--button2-color);
}
.file-item.selected {
border-color: var(--button2-color);
background-color: var(--button-hover-color);
/* box-shadow: 0 0 8px var(--button2-color); */
}
.file-name {
font-weight: bold;
color: var(--text2-color);
word-break: break-all;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* 预览容器 */
.preview-container {
margin: auto 0;
text-align: center;
}
.preview-container .icon {
/* height: 150px; */
max-height: 150px;
max-width: 233px;
}
/* .preview-image {
max-width: 100%;
max-height: 200px;
object-fit: contain;
} */
.video-preview {
width: 100%;
max-height: 150px;
}
.video-preview video {
max-width: 100%;
max-height: 100%;
}
/* 底部信息栏 */
.bottom-row {
/* margin-top: auto; */
display: flex;
justify-content: space-between;
align-items: center;
gap: 2px;
}
.file-info {
margin: 0 auto;
flex-shrink: 0;
}
/* 操作图标 */
.actions {
display: flex;
gap: 2px;
justify-content: center;
margin-bottom: -5px;
}
.actions .icon {
width: 23px;
transition: all 0.1s;
opacity: 0.5;
}
.actions .icon:hover {
/* transform: scale(1.1); */
opacity: 1;
}
.actions .icon:active {
transform: scale(0.9);
}
/* 全屏预览 */
.play-container,
.image-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 4;
}
.play-container.hide,
.image-container.hide,
.video-preview.hide {
display: none;
}
#video-player,
#image-player {
max-width: 90vw;
max-height: 90vh;
width: auto;
height: auto;
object-fit: contain;
}
/* 框选 */
#selection-box {
position: absolute;
border: 1px solid var(--button2-color);
background-color: var(--button-active-color);
pointer-events: none;
z-index: 3;
display: none;
}
/* 提示框 */
.alert-box {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 20px 40px;
border-radius: 8px;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
z-index: 1000;
}
.alert-box.active {
opacity: 1;
visibility: visible;
}
/* 分页组件样式 */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
/* margin-top: 20px; */
padding: 15px;
background: var(--background-color-two);
border-radius: 8px;
}
.pagination.hide {
display: none;
}
.page-numbers {
display: flex;
gap: 5px;
flex-wrap: wrap;
}

211
css/public.css Normal file
View File

@@ -0,0 +1,211 @@
:root {
/* 两个背景色 两个文字以及链接文字配色 */
--background-color: #fff;
--background-color-opacity: #ffffffea;
--background-color-two: #f5f5f5;
--text-color: #000;
--text-error-color: #ff0000;
--text2-color: rgb(26, 115, 232);
--link-color: #3079ed;
/* 设置页面 设置box 背景色 */
--optionBox-color: var(--background-color);
/* 两个按钮 配色 边框 */
--button-color: #fff;
--button-text-color: rgb(26, 115, 232);
--button-border: solid 1px #c7c7c780;
--button-hover-color: rgb(66 133 244 / 4%);
--button-active-color: rgb(66 133 244 / 10%);
--button2-color: rgb(26, 115, 232);
--button2-text-color: #fff;
--button2-border: solid 1px #c7c7c780;
--button2-hover-color: rgb(26 115 232 / 90%);
--button2-active-color: rgb(26 115 232 / 50%);
/* 滚动条配色 */
--scrollbar-track-color: #f5f5f500;
--scrollbar-thumb-color: #1a73e8;
/* 设置页面 滑动开关配色 */
--switch-off-color: rgb(189, 193, 198);
--switch-on-color: rgb(26, 115, 232);
--switch-round-color: #fff;
/* input textarea select 边框配色 */
--input-border: solid 1px #000;
}
html {
color: var(--text-color);
background: var(--background-color);
scrollbar-width: thin;
}
input,
textarea,
select {
color: var(--text-color);
background: var(--background-color);
scrollbar-width: thin;
border: var(--input-border);
}
a,
a:link,
a:visited {
color: var(--link-color);
}
button,
.button,
.button2 {
border-radius: 4px;
cursor: pointer;
margin: 0 0 3px 0;
user-select: none;
}
button,
.button {
background: var(--button-color);
border: var(--button-border);
color: var(--button-text-color);
}
button:hover,
.button:hover {
background: var(--button-hover-color);
}
button:active,
.button:active {
background: var(--button-active-color);
}
.button2 {
background: var(--button2-color);
border: var(--button2-border);
color: var(--button2-text-color);
}
.button2:hover {
background: var(--button2-hover-color);
}
.button2:active {
background: var(--button2-active-color);
}
button:disabled,
.button:disabled,
.button2:disabled,
.disabled {
background-color: #ccc;
color: #666;
cursor: not-allowed;
opacity: 0.6;
}
.bold {
font-weight: bold;
}
.hide {
display: none;
}
.textColor {
color: var(--text2-color);
}
.width100 {
width: 100%;
}
.height100 {
height: 100%;
}
.line {
border-top: solid 1px rgb(0 0 0 / 50%);
margin: 10px 0 10px 0;
}
.no-drop {
background-color: #ccc !important;
cursor: no-drop;
color: var(--button2-text-color);
}
.icon {
-webkit-user-drag: none;
}
/*定义整个滚动条高宽及背景:高宽分别对应横竖滚动条的尺寸*/
::-webkit-scrollbar {
width: 5px;
}
/*定义滚动条轨道:内阴影+圆角*/
::-webkit-scrollbar-track {
background-color: var(--scrollbar-track-color);
}
/*定义滑块:内阴影+圆角*/
::-webkit-scrollbar-thumb {
border-radius: 10px;
background-color: var(--scrollbar-thumb-color);
}
@media (prefers-color-scheme: dark) {
:root {
--background-color: #0f172a;
--background-color-opacity: #0f172aea;
--background-color-two: #1e293b;
--text-color: #fff;
--text-error-color: #ff0000;
--text2-color: #fff;
--link-color: #94a3b8;
--optionBox-color: var(--background-color-two);
--button-color: #161b22;
--button-border: solid 1px #c7c7c780;
--button-text-color: #fff;
--button-hover-color: rgb(66 133 244 / 4%);
--button-active-color: rgb(66 133 244 / 10%);
--button2-color: rgb(26 115 232 / 50%);
--button2-border: solid 1px #c7c7c780;
--button2-text-color: #fff;
--button2-hover-color: rgb(26 115 232 / 90%);
--button2-active-color: rgb(26 115 232 / 50%);
--scrollbar-track-color: #f5f5f500;
--scrollbar-thumb-color: #1a73e8;
--switch-off-color: rgb(189, 193, 198);
--switch-on-color: rgb(26 115 232 / 50%);
--switch-round-color: #fff;
--input-border: solid 1px #ffffffb6;
}
img.regex {
content: url(../img/regex-dark.png);
}
img.copy {
content: url(../img/copy-dark.png);
}
img.parsing {
content: url(../img/parsing-dark.png);
}
img.play {
content: url(../img/play-dark.png);
}
img.download {
content: url(../img/download-dark.svg);
}
img.qrcode {
content: url(../img/qrcode-dark.png);
}
img.cat-down {
content: url(../img/cat-down-dark.png);
}
img.aria2 {
content: url(../img/aria2-dark.png);
}
img.invoke {
content: url(../img/invoke-dark.svg);
}
img.send {
content: url(../img/send-dark.svg);
}
img.delete {
content: url(../img/delete-dark.svg);
}
img.mqtt {
content: url(../img/mqtt-dark.svg);
}
img.send2ffmpeg {
content: url(../img/send2ffmpeg-dark.svg);
}
}

52
downloader.html Normal file
View File

@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>titleDownload</title>
<link rel="stylesheet" type="text/css" href="css/public.css" media="all" />
<link rel="stylesheet" type="text/css" href="css/options.css" media="all" />
<script src="lib/jquery.min.js"></script>
<script src="lib/StreamSaver.js"></script>
<script src="js/init.js"></script>
<script src="js/firefox.js"></script>
<script src="js/function.js"></script>
</head>
<body>
<div class="wrapper1024 hide" id="getURL">
<section>
<h1 class="optionsTitle" data-i18n="titleDownload"></h1>
<div class="optionBox">
<input type="text" id="url" placeholder="URL" class="fullInput" />
<input type="text" id="referer" placeholder="referer" class="fullInput" />
<button id="getURL_btn" type="button" data-i18n="download"></button>
<label class="textColor"><input type="checkbox" id="downStream"><span
data-i18n-outer="downloadWhileSaving"></span></label>
</div>
</section>
</div>
<div class="wrapper1024 newDownload">
<section id="downfile">
<h1 class="optionsTitle" data-i18n="titleDownload"></h1>
<div class="optionBox" id="downBox"></div>
</section>
<section>
<div class="optionBox">
<button class="openDir button2" data-i18n="openDir"></button>
<button id="ffmpeg" class="button2 hide" data-i18n="sendFfmpeg"></button>
<button id="stopDownload" class="button2" data-i18n="stopDownload"></button>
<button id="test" class="button2 hide">test</button>
<label class="textColor"><input type="checkbox" id="autoClose"><span
data-i18n-outer="autoCloserDownload"></span></label>
</div>
</section>
</div>
<script src="js/m3u8.downloader.js"></script>
<script src="js/downloader.js"></script>
<script src="js/i18n.js"></script>
</body>
</html>

BIN
img/aria2-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

BIN
img/aria2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 852 B

BIN
img/cat-down-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 B

BIN
img/cat-down.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 B

BIN
img/copy-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 B

BIN
img/copy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 B

1
img/delete-dark.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#fff"><path d="M253-99q-36.19 0-64.09-26.91Q161-152.82 161-190v-552h-58v-91h228v-47h297v47h228v91h-58v552q0 37.18-27.21 64.09Q743.59-99 706-99H253Zm104-169h74v-398h-74v398Zm173 0h75v-398h-75v398Z"/></svg>

After

Width:  |  Height:  |  Size: 302 B

1
img/delete.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#1A73E8"><path d="M253-99q-36.19 0-64.09-26.91Q161-152.82 161-190v-552h-58v-91h228v-47h297v47h228v91h-58v552q0 37.18-27.21 64.09Q743.59-99 706-99H253Zm104-169h74v-398h-74v398Zm173 0h75v-398h-75v398Z"/></svg>

After

Width:  |  Height:  |  Size: 305 B

1
img/download-dark.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="#fff" version="1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M21.5 4.9l-2.1-2.7c-.1-.1-.3-.2-.5-.2H5.1c-.2 0-.4.1-.5.2L2.5 4.9c-.3.3-.5.6-.5 1V20c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V5.9c0-.4-.2-.7-.5-1zM12 18l-5-5h3V9h4v4h3l-5 5zm7-13H5l.5-1h13l.5 1z"/></svg>

After

Width:  |  Height:  |  Size: 287 B

1
img/download.svg Normal file
View File

@@ -0,0 +1 @@
<svg version="1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="#1A73E8"><path d="M21.5 4.9l-2.1-2.7c-.1-.1-.3-.2-.5-.2H5.1c-.2 0-.4.1-.5.2L2.5 4.9c-.3.3-.5.6-.5 1V20c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V5.9c0-.4-.2-.7-.5-1zM12 18l-5-5h3V9h4v4h3l-5 5zm7-13H5l.5-1h13l.5 1z"/></svg>

After

Width:  |  Height:  |  Size: 290 B

BIN
img/icon-disable.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

BIN
img/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 775 B

BIN
img/icon128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

1
img/invoke-dark.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#fff"><path d="m346-285 221-222v117h86v-263H390v86h116L286-347l60 62ZM479.68-59q-86.32 0-163.65-32.6-77.32-32.61-134.57-89.86T91.6-316.03Q59-393.35 59-479.86q0-87.41 32.66-164.28 32.67-76.86 90.21-134.41 57.54-57.55 134.41-90.5T479.34-902q87.55 0 164.88 32.86t134.56 90.25q57.23 57.4 90.22 134.52 33 77.11 33 164.91 0 86.74-32.95 163.38-32.95 76.65-90.5 134.2Q721-124.33 644.13-91.66 567.26-59 479.68-59Z"/></svg>

After

Width:  |  Height:  |  Size: 511 B

1
img/invoke.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#1A73E8"><path d="m346-285 221-222v117h86v-263H390v86h116L286-347l60 62ZM479.68-59q-86.32 0-163.65-32.6-77.32-32.61-134.57-89.86T91.6-316.03Q59-393.35 59-479.86q0-87.41 32.66-164.28 32.67-76.86 90.21-134.41 57.54-57.55 134.41-90.5T479.34-902q87.55 0 164.88 32.86t134.56 90.25q57.23 57.4 90.22 134.52 33 77.11 33 164.91 0 86.74-32.95 163.38-32.95 76.65-90.5 134.2Q721-124.33 644.13-91.66 567.26-59 479.68-59Z"/></svg>

After

Width:  |  Height:  |  Size: 514 B

1
img/mqtt-dark.svg Normal file
View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1754335005711" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5232" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M76.8 307.2c353.4592 0 640 286.5408 640 640h-102.4c0-296.9088-240.6912-537.6-537.6-537.6v-102.4z" fill="#e6e6e6" p-id="5233"></path><path d="M486.4 947.2c0-226.2272-183.3728-409.6-409.6-409.6v409.6h409.6zM844.8 947.2h102.4C947.2 466.4832 557.5168 76.8 76.8 76.8v102.4c424.1664 0 768 343.8336 768 768z" fill="#e6e6e6" p-id="5234"></path></svg>

After

Width:  |  Height:  |  Size: 673 B

1
img/mqtt.svg Normal file
View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1754335005711" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5232" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M76.8 307.2c353.4592 0 640 286.5408 640 640h-102.4c0-296.9088-240.6912-537.6-537.6-537.6v-102.4z" fill="#1a73e8" p-id="5233"></path><path d="M486.4 947.2c0-226.2272-183.3728-409.6-409.6-409.6v409.6h409.6zM844.8 947.2h102.4C947.2 466.4832 557.5168 76.8 76.8 76.8v102.4c424.1664 0 768 343.8336 768 768z" fill="#1a73e8" p-id="5234"></path></svg>

After

Width:  |  Height:  |  Size: 673 B

1
img/music.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#1a73e8"><path d="M397.88-237q49.62 0 85.37-36.33Q519-309.67 519-359v-310h122v-57H475v269q-17-10-37.2-16.5-20.19-6.5-39.8-6.5-49.67 0-85.33 36.87Q277-406.26 277-358.38q0 48.88 35.63 85.13Q348.26-237 397.88-237Zm82.07 178q-87.06 0-164.15-32.6-77.09-32.61-134.34-89.86T91.6-315.87Q59-393.03 59-480.36q0-87.23 32.66-163.94 32.67-76.7 90.21-134.25 57.54-57.55 134.25-90.5Q392.83-902 479.84-902q87.37 0 164.54 32.86 77.17 32.86 134.4 90.25 57.23 57.4 90.22 134.52 33 77.11 33 164.65 0 87.46-32.95 163.88-32.95 76.41-90.5 133.78Q721-124.69 644.13-91.85 567.26-59 479.95-59Z"/></svg>

After

Width:  |  Height:  |  Size: 674 B

BIN
img/parsing-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

BIN
img/parsing.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

BIN
img/play-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 B

BIN
img/play.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 B

BIN
img/qrcode-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 B

BIN
img/qrcode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 B

BIN
img/regex-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 B

BIN
img/regex.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 B

1
img/send-dark.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#fff"><path d="M81-121v-250l375-111L81-592v-250l848 361L81-121Z"/></svg>

After

Width:  |  Height:  |  Size: 170 B

1
img/send.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#1A73E8"><path d="M81-121v-250l375-111L81-592v-250l848 361L81-121Z"/></svg>

After

Width:  |  Height:  |  Size: 173 B

1
img/send2ffmpeg-dark.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="48px" fill="#fff"><path d="M480-108q-77 0-145-29t-118.5-79.5Q166-267 137-335t-29-145q0-77 29-145t79.5-118.5Q267-794 335-823t145-29v136q-99 0-167.5 68.5T244-480q0 99 68.5 167.5T480-244v136Zm160-132-96-97 75-75H332v-136h287l-75-76 96-96 240 240-240 240Z"/></svg>

After

Width:  |  Height:  |  Size: 346 B

1
img/send2ffmpeg.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="48px" fill="#1A73E8"><path d="M480-108q-77 0-145-29t-118.5-79.5Q166-267 137-335t-29-145q0-77 29-145t79.5-118.5Q267-794 335-823t145-29v136q-99 0-167.5 68.5T244-480q0 99 68.5 167.5T480-244v136Zm160-132-96-97 75-75H332v-136h287l-75-76 96-96 240 240-240 240Z"/></svg>

After

Width:  |  Height:  |  Size: 349 B

120
install.html Normal file
View File

@@ -0,0 +1,120 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>猫抓扩展安装成功</title>
<link rel="stylesheet" type="text/css" href="css/install.css" media="all" />
</head>
<body>
<div class="container">
<!-- 语言切换按钮 -->
<div class="lang-switch" id="langToggle">
<span class="lang-emoji" id="langEmoji">🌐</span>
<span class="lang-text" id="langText">English</span>
</div>
<div class="header">
<div class="logo-container">
<div class="logo"><img src="img/icon.png"></div>
</div>
<h1 id="main-title">恭喜 猫抓 扩展已成功安装 !</h1>
<div class="subtitle" id="subtitle">Installation successful !</div>
</div>
<div class="card fade-in">
<div class="card-header">
<div class="card-title">
<span class="emoji">🙌</span>
<span id="welcome-title">希望本扩展能帮助到你</span>
</div>
</div>
<div class="card-body">
<div class="policy-section">
<div class="section-title">
<span class="emoji">🔒</span>
<span id="privacy-title">隐私政策 / Privacy Policy</span>
</div>
<div class="content-box">
<div class="lang-zh active">
<p>本扩展收集所有信息都在本地储存处理,不会发送到远程服务器,不包含任何跟踪器。</p>
</div>
<div class="lang-en">
<p>The extension collects and processes all information locally without sending it to remote
servers and does not include any trackers.</p>
</div>
</div>
</div>
<div class="policy-section">
<div class="section-title">
<span class="emoji">⚠️</span>
<span id="disclaimer-title">免责声明 / Disclaimer</span>
</div>
<div class="content-box">
<div class="lang-zh active">
<p>本扩展仅供下载用户拥有版权或已获授权的视频,禁止用于下载受版权保护且未经授权的内容。用户需自行承担使用本工具的全部法律责任,开发者不对用户的任何行为负责。本工具按"原样"提供,开发者不承担任何直接或间接责任。
</p>
</div>
<div class="lang-en">
<p>This 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.</p>
</div>
</div>
</div>
<div class="policy-section">
<div class="section-title">
<span class="emoji">🚨</span>
<span id="issue-title">问题提交 / Issue Submission</span>
</div>
<div class="content-box">
<div class="lang-zh active">
<a href="https://cat-catch.bmmmd.com/issues" target="_blank">
<span class="emoji">🔗</span>
https://cat-catch.bmmmd.com/issues
</a>
</div>
<div class="lang-en">
<a href="https://cat-catch.bmmmd.com/issues" target="_blank">
<span class="emoji">🔗</span>
https://cat-catch.bmmmd.com/issues
</a>
</div>
</div>
</div>
<div class="agreement">
<div class="lang-zh active">
<p>点击"同意"或"关闭本页面"即表示您已阅读并同意以上内容。</p>
</div>
<div class="lang-en">
<p>By clicking "Agree" or "Close this page," you confirm that you have read and agree to the
above terms.</p>
</div>
</div>
<div class="buttons">
<button id="agreeBtn" class="btn btn-primary">
<span class="emoji"></span>
<span id="agreeText">同意</span>
</button>
<button id="uninstallBtn" class="btn btn-outline">
<span class="emoji">🗑️</span>
<span id="uninstallText">卸载扩展</span>
</button>
</div>
</div>
</div>
</div>
<script src="js/function.js"></script>
<script src="js/install.js"></script>
</body>
</html>

869
js/background.js Normal file
View File

@@ -0,0 +1,869 @@
importScripts("/js/function.js", "/js/init.js");
// Service Worker 5分钟后会强制终止扩展
// https://bugs.chromium.org/p/chromium/issues/detail?id=1271154
// https://stackoverflow.com/questions/66618136/persistent-service-worker-in-chrome-extension/70003493#70003493
chrome.webNavigation.onBeforeNavigate.addListener(function () { return; });
chrome.webNavigation.onHistoryStateUpdated.addListener(function () { return; });
chrome.runtime.onConnect.addListener(function (Port) {
if (chrome.runtime.lastError || Port.name !== "HeartBeat") return;
Port.postMessage("HeartBeat");
Port.onMessage.addListener(function (message, Port) { return; });
const interval = setInterval(function () {
clearInterval(interval);
Port.disconnect();
}, 250000);
Port.onDisconnect.addListener(function () {
interval && clearInterval(interval);
if (chrome.runtime.lastError) { return; }
});
});
/**
* 定时任务
* nowClear clear 清理冗余数据
* save 保存数据
*/
chrome.alarms.onAlarm.addListener(function (alarm) {
if (alarm.name === "nowClear" || alarm.name === "clear") {
clearRedundant();
return;
}
if (alarm.name === "save") {
(chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData });
return;
}
});
// onBeforeRequest 浏览器发送请求之前使用正则匹配发送请求的URL
// chrome.webRequest.onBeforeRequest.addListener(
// function (data) {
// try { findMedia(data, true); } catch (e) { console.log(e); }
// }, { urls: ["<all_urls>"] }, ["requestBody"]
// );
// 保存requestHeaders
chrome.webRequest.onSendHeaders.addListener(
function (data) {
if (G && G.initSyncComplete && !G.enable) { return; }
if (data.requestHeaders) {
G.requestHeaders.set(data.requestId, data.requestHeaders);
data.allRequestHeaders = data.requestHeaders;
}
try { findMedia(data, true); } catch (e) { console.log(e); }
}, { urls: ["<all_urls>"] }, ['requestHeaders',
chrome.webRequest.OnBeforeSendHeadersOptions.EXTRA_HEADERS].filter(Boolean)
);
// onResponseStarted 浏览器接收到第一个字节触发,保证有更多信息判断资源类型
chrome.webRequest.onResponseStarted.addListener(
function (data) {
try {
data.allRequestHeaders = G.requestHeaders.get(data.requestId);
if (data.allRequestHeaders) {
G.requestHeaders.delete(data.requestId);
}
findMedia(data);
} catch (e) { console.log(e, data); }
}, { urls: ["<all_urls>"] }, ["responseHeaders"]
);
// 删除失败的requestHeadersData
chrome.webRequest.onErrorOccurred.addListener(
function (data) {
G.requestHeaders.delete(data.requestId);
G.blackList.delete(data.requestId);
}, { urls: ["<all_urls>"] }
);
function findMedia(data, isRegex = false, filter = false, timer = false) {
// Service Worker被强行杀死之后重新自我唤醒等待全局变量初始化完成。
if (!G || !G.initSyncComplete || !G.initLocalComplete || G.tabId == undefined || cacheData.init) {
if (timer) { return; }
setTimeout(() => {
findMedia(data, isRegex, filter, true);
}, 500);
return;
}
// 检查 是否启用 是否在当前标签是否在屏蔽列表中
const blockUrlFlag = data.tabId && data.tabId > 0 && G.blockUrlSet.has(data.tabId);
if (!G.enable || (G.blockUrlWhite ? !blockUrlFlag : blockUrlFlag)) {
return;
}
data.getTime = Date.now();
if (!isRegex && G.blackList.has(data.requestId)) {
G.blackList.delete(data.requestId);
return;
}
// 屏蔽特殊页面发起的资源
if (data.initiator != "null" &&
data.initiator != undefined &&
isSpecialPage(data.initiator)) { return; }
if (G.isFirefox &&
data.originUrl &&
isSpecialPage(data.originUrl)) { return; }
// 屏蔽特殊页面的资源
if (isSpecialPage(data.url)) { return; }
const urlParsing = new URL(data.url);
let [name, ext] = fileNameParse(urlParsing.pathname);
//正则匹配
if (isRegex && !filter) {
for (let key in G.Regex) {
if (!G.Regex[key].state) { continue; }
G.Regex[key].regex.lastIndex = 0;
let result = G.Regex[key].regex.exec(data.url);
if (result == null) { continue; }
if (G.Regex[key].blackList) {
G.blackList.add(data.requestId);
return;
}
data.extraExt = G.Regex[key].ext ? G.Regex[key].ext : undefined;
if (result.length == 1) {
findMedia(data, true, true);
return;
}
result.shift();
result = result.map(str => decodeURIComponent(str));
if (!result[0].startsWith('https://') && !result[0].startsWith('http://')) {
result[0] = urlParsing.protocol + "//" + data.url;
}
data.url = result.join("");
findMedia(data, true, true);
return;
}
return;
}
// 非正则匹配
if (!isRegex) {
// 获取头部信息
data.header = getResponseHeadersValue(data);
//检查后缀
if (!filter && ext != undefined) {
filter = CheckExtension(ext, data.header?.size);
if (filter == "break") { return; }
}
//检查类型
if (!filter && data.header?.type != undefined) {
filter = CheckType(data.header.type, data.header?.size);
if (filter == "break") { return; }
}
//查找附件
if (!filter && data.header?.attachment != undefined) {
const res = data.header.attachment.match(reFilename);
if (res && res[1]) {
[name, ext] = fileNameParse(decodeURIComponent(res[1]));
filter = CheckExtension(ext, 0);
if (filter == "break") { return; }
}
}
//放过类型为media的资源
if (data.type == "media") {
filter = true;
}
}
if (!filter) { return; }
// 谜之原因 获取得资源 tabId可能为 -1 firefox中则正常
// 检查是 -1 使用当前激活标签得tabID
data.tabId = data.tabId == -1 ? G.tabId : data.tabId;
cacheData[data.tabId] ??= [];
cacheData[G.tabId] ??= [];
// 缓存数据大于9999条 清空缓存 避免内存占用过多
if (cacheData[data.tabId].length > G.maxLength) {
cacheData[data.tabId] = [];
(chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData });
return;
}
// 查重 避免CPU占用 大于500 强制关闭查重
// if (G.checkDuplicates && cacheData[data.tabId].length <= 500) {
// for (let item of cacheData[data.tabId]) {
// if (item.url.length == data.url.length &&
// item.cacheURL.pathname == urlParsing.pathname &&
// item.cacheURL.host == urlParsing.host &&
// item.cacheURL.search == urlParsing.search) { return; }
// }
// }
if (G.checkDuplicates && cacheData[data.tabId].length <= 500) {
const tabFingerprints = G.urlMap.get(data.tabId) || new Set();
if (tabFingerprints.has(data.url)) {
return; // 找到重复,直接返回
}
tabFingerprints.add(data.url);
G.urlMap.set(data.tabId, tabFingerprints);
if (tabFingerprints.size >= 500) {
tabFingerprints.clear();
}
}
chrome.tabs.get(data.tabId, async function (webInfo) {
if (chrome.runtime.lastError) { return; }
data.requestHeaders = getRequestHeaders(data);
// requestHeaders 中cookie 单独列出来
if (data.requestHeaders?.cookie) {
data.cookie = data.requestHeaders.cookie;
data.requestHeaders.cookie = undefined;
}
const info = {
name: name,
url: data.url,
size: data.header?.size,
ext: ext,
type: data.mime ?? data.header?.type,
tabId: data.tabId,
isRegex: isRegex,
requestId: data.requestId ?? Date.now().toString(),
initiator: data.initiator,
requestHeaders: data.requestHeaders,
cookie: data.cookie,
// cacheURL: { host: urlParsing.host, search: urlParsing.search, pathname: urlParsing.pathname },
getTime: data.getTime
};
// 不存在扩展使用类型
if (info.ext === undefined && info.type !== undefined) {
info.ext = info.type.split("/")[1];
}
// 正则匹配的备注扩展
if (data.extraExt) {
info.ext = data.extraExt;
}
// 不存在 initiator 和 referer 使用web url代替initiator
if (info.initiator == undefined || info.initiator == "null") {
info.initiator = info.requestHeaders?.referer ?? webInfo?.url;
}
// 装载页面信息
info.title = webInfo?.title ?? "NULL";
info.favIconUrl = webInfo?.favIconUrl;
info.webUrl = webInfo?.url;
// 屏蔽资源
if (!isRegex && G.blackList.has(data.requestId)) {
G.blackList.delete(data.requestId);
return;
}
// 发送到popup 并检查自动下载
chrome.runtime.sendMessage({ Message: "popupAddData", data: info }, function () {
if (G.featAutoDownTabId.size > 0 && G.featAutoDownTabId.has(info.tabId) && chrome.downloads?.State) {
try {
const downDir = info.title == "NULL" ? "CatCatch/" : stringModify(info.title) + "/";
let fileName = isEmpty(info.name) ? stringModify(info.title) + '.' + info.ext : decodeURIComponent(stringModify(info.name));
if (G.TitleName) {
fileName = filterFileName(templates(G.downFileName, info));
} else {
fileName = downDir + fileName;
}
chrome.downloads.download({
url: info.url,
filename: fileName
});
} catch (e) { return; }
}
if (chrome.runtime.lastError) { return; }
});
// 数据发送
if (G.send2local) {
try { send2local("catch", { ...info, requestHeaders: data.allRequestHeaders }, info.tabId); } catch (e) { console.log(e); }
}
// 储存数据
cacheData[info.tabId] ??= [];
cacheData[info.tabId].push(info);
// 当前标签媒体数量大于100 开启防抖 等待5秒储存 或 积累10个资源储存一次。
if (cacheData[info.tabId].length >= 100 && debounceCount <= 10) {
debounceCount++;
clearTimeout(debounce);
debounce = setTimeout(function () { save(info.tabId); }, 5000);
return;
}
// 时间间隔小于500毫秒 等待2秒储存
if (Date.now() - debounceTime <= 500) {
clearTimeout(debounce);
debounceTime = Date.now();
debounce = setTimeout(function () { save(info.tabId); }, 2000);
return;
}
save(info.tabId);
});
}
// cacheData数据 储存到 chrome.storage.local
function save(tabId) {
clearTimeout(debounce);
debounceTime = Date.now();
debounceCount = 0;
(chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData }, function () {
chrome.runtime.lastError && console.log(chrome.runtime.lastError);
});
cacheData[tabId] && SetIcon({ number: cacheData[tabId].length, tabId: tabId });
}
/**
* 监听 扩展 message 事件
*/
chrome.runtime.onMessage.addListener(function (Message, sender, sendResponse) {
if (chrome.runtime.lastError) { return; }
if (!G.initLocalComplete || !G.initSyncComplete) {
sendResponse("error");
return true;
}
// 以下检查是否有 tabId 不存在使用当前标签
Message.tabId = Message.tabId ?? G.tabId;
// 从缓存中保存数据到本地
if (Message.Message == "pushData") {
(chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData });
sendResponse("ok");
return true;
}
// 获取所有数据
if (Message.Message == "getAllData") {
sendResponse(cacheData);
return true;
}
/**
* 设置扩展图标数字
* 提供 type 删除标签为 tabId 的数字
* 不提供type 删除所有标签的数字
*/
if (Message.Message == "ClearIcon") {
Message.type ? SetIcon({ tabId: Message.tabId }) : SetIcon();
sendResponse("ok");
return true;
}
// 启用/禁用扩展
if (Message.Message == "enable") {
G.enable = !G.enable;
chrome.storage.sync.set({ enable: G.enable });
chrome.action.setIcon({ path: G.enable ? "/img/icon.png" : "/img/icon-disable.png" });
sendResponse(G.enable);
return true;
}
/**
* 提供requestId数组 获取指定的数据
*/
if (Message.Message == "getData" && Message.requestId) {
// 判断Message.requestId是否数组
if (!Array.isArray(Message.requestId)) {
Message.requestId = [Message.requestId];
}
const response = [];
if (Message.requestId.length) {
for (let item in cacheData) {
for (let data of cacheData[item]) {
if (Message.requestId.includes(data.requestId)) {
response.push(data);
}
}
}
}
sendResponse(response.length ? response : "error");
return true;
}
/**
* 提供 tabId 获取该标签数据
*/
if (Message.Message == "getData") {
sendResponse(cacheData[Message.tabId]);
return true;
}
/**
* 获取各按钮状态
* 模拟手机 自动下载 启用 以及各种脚本状态
*/
if (Message.Message == "getButtonState") {
let state = {
MobileUserAgent: G.featMobileTabId.has(Message.tabId),
AutoDown: G.featAutoDownTabId.has(Message.tabId),
enable: G.enable,
}
G.scriptList.forEach(function (item, key) {
state[item.key] = item.tabId.has(Message.tabId);
});
sendResponse(state);
return true;
}
// 对tabId的标签 进行模拟手机操作
if (Message.Message == "mobileUserAgent") {
mobileUserAgent(Message.tabId, !G.featMobileTabId.has(Message.tabId));
chrome.tabs.reload(Message.tabId, { bypassCache: true });
sendResponse("ok");
return true;
}
// 对tabId的标签 开启 关闭 自动下载
if (Message.Message == "autoDown") {
if (G.featAutoDownTabId.has(Message.tabId)) {
G.featAutoDownTabId.delete(Message.tabId);
} else {
G.featAutoDownTabId.add(Message.tabId);
}
(chrome.storage.session ?? chrome.storage.local).set({ featAutoDownTabId: Array.from(G.featAutoDownTabId) });
sendResponse("ok");
return true;
}
// 对tabId的标签 脚本注入或删除
if (Message.Message == "script") {
if (!G.scriptList.has(Message.script)) {
sendResponse("error no exists");
return false;
}
const script = G.scriptList.get(Message.script);
const scriptTabid = script.tabId;
const refresh = Message.refresh ?? script.refresh;
if (scriptTabid.has(Message.tabId)) {
scriptTabid.delete(Message.tabId);
if (Message.script == "search.js") {
G.deepSearchTemporarilyClose = Message.tabId;
}
refresh && chrome.tabs.reload(Message.tabId, { bypassCache: true });
sendResponse("ok");
return true;
}
scriptTabid.add(Message.tabId);
if (refresh) {
chrome.tabs.reload(Message.tabId, { bypassCache: true });
} else {
const files = [`catch-script/${Message.script}`];
script.i18n && files.unshift("catch-script/i18n.js");
chrome.scripting.executeScript({
target: { tabId: Message.tabId, allFrames: script.allFrames },
files: files,
injectImmediately: true,
world: script.world
});
}
sendResponse("ok");
return true;
}
// 脚本注入 脚本申请多语言文件
if (Message.Message == "scriptI18n") {
chrome.scripting.executeScript({
target: { tabId: Message.tabId, allFrames: true },
files: ["catch-script/i18n.js"],
injectImmediately: true,
world: "MAIN"
});
sendResponse("ok");
return true;
}
// Heart Beat
if (Message.Message == "HeartBeat") {
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
if (tabs[0] && tabs[0].id) {
G.tabId = tabs[0].id;
}
});
sendResponse("HeartBeat OK");
return true;
}
// 清理数据
if (Message.Message == "clearData") {
// 当前标签
if (Message.type) {
delete cacheData[Message.tabId];
(chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData });
clearRedundant();
sendResponse("OK");
return true;
}
// 其他标签
for (let item in cacheData) {
if (item == Message.tabId) { continue; }
delete cacheData[item];
}
(chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData });
clearRedundant();
sendResponse("OK");
return true;
}
// 清理冗余数据
if (Message.Message == "clearRedundant") {
clearRedundant();
sendResponse("OK");
return true;
}
// 从 content-script 或 catch-script 传来的媒体url
if (Message.Message == "addMedia") {
chrome.tabs.query({}, function (tabs) {
for (let item of tabs) {
if (item.url == Message.href) {
findMedia({ url: Message.url, tabId: item.id, extraExt: Message.extraExt, mime: Message.mime, requestId: Message.requestId, requestHeaders: Message.requestHeaders }, true, true);
return true;
}
}
findMedia({ url: Message.url, tabId: -1, extraExt: Message.extraExt, mime: Message.mime, requestId: Message.requestId, initiator: Message.href, requestHeaders: Message.requestHeaders }, true, true);
});
sendResponse("ok");
return true;
}
// ffmpeg网页通信
if (Message.Message == "catCatchFFmpeg") {
const data = { ...Message, Message: "ffmpeg", tabId: Message.tabId ?? sender.tab.id, version: G.ffmpegConfig.version };
chrome.tabs.query({ url: G.ffmpegConfig.url + "*" }, function (tabs) {
if (chrome.runtime.lastError || !tabs.length) {
chrome.tabs.create({ url: G.ffmpegConfig.url, active: Message.active ?? true }, function (tab) {
if (chrome.runtime.lastError) { return; }
G.ffmpegConfig.tab = tab.id;
G.ffmpegConfig.cacheData.push(data);
});
return true;
}
if (tabs[0].status == "complete") {
chrome.tabs.sendMessage(tabs[0].id, data);
} else {
G.ffmpegConfig.tab = tabs[0].id;
G.ffmpegConfig.cacheData.push(data);
}
});
sendResponse("ok");
return true;
}
// 发送数据到本地
if (Message.Message == "send2local" && G.send2local) {
try { send2local(Message.action, Message.data, Message.tabId); } catch (e) { console.log(e); }
sendResponse("ok");
return true;
}
});
// 选定标签 更新G.tabId
// chrome.tabs.onHighlighted.addListener(function (activeInfo) {
// if (activeInfo.windowId == -1 || !activeInfo.tabIds || !activeInfo.tabIds.length) { return; }
// G.tabId = activeInfo.tabIds[0];
// });
/**
* 监听 切换标签
* 更新全局变量 G.tabId 为当前标签
*/
chrome.tabs.onActivated.addListener(function (activeInfo) {
G.tabId = activeInfo.tabId;
if (cacheData[G.tabId] !== undefined) {
SetIcon({ number: cacheData[G.tabId].length, tabId: G.tabId });
return;
}
SetIcon({ tabId: G.tabId });
});
// 切换窗口更新全局变量G.tabId
chrome.windows.onFocusChanged.addListener(function (activeInfo) {
if (activeInfo == -1) { return; }
chrome.tabs.query({ active: true, windowId: activeInfo }, function (tabs) {
if (tabs[0] && tabs[0].id) {
G.tabId = tabs[0].id;
} else {
G.tabId = -1;
}
});
}, { filters: ["normal"] });
/**
* 监听 标签页面更新
* 检查 清理数据
* 检查 是否在屏蔽列表中
*/
chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
if (isSpecialPage(tab.url) || tabId <= 0 || !G.initSyncComplete) { return; }
if (changeInfo.status && changeInfo.status == "loading" && G.autoClearMode == 2) {
G.urlMap.delete(tabId);
chrome.alarms.get("save", function (alarm) {
if (!alarm) {
delete cacheData[tabId];
SetIcon({ tabId: tabId });
chrome.alarms.create("save", { when: Date.now() + 1000 });
}
});
}
// 检查当前标签是否在屏蔽列表中
if (changeInfo.url && tabId > 0 && G.blockUrl.length) {
G.blockUrlSet.delete(tabId);
if (isLockUrl(changeInfo.url)) {
G.blockUrlSet.add(tabId);
}
}
chrome.sidePanel.setOptions({
tabId,
path: "popup.html?tabId=" + tabId
});
});
/**
* 监听 frame 正在载入
* 检查 是否在屏蔽列表中 (frameId == 0 为主框架)
* 检查 自动清理 (frameId == 0 为主框架)
* 检查 注入脚本
*/
chrome.webNavigation.onCommitted.addListener(function (details) {
if (isSpecialPage(details.url) || details.tabId <= 0 || !G.initSyncComplete) { return; }
// 刷新页面 检查是否在屏蔽列表中
if (details.frameId == 0 && details.transitionType == "reload") {
G.blockUrlSet.delete(details.tabId);
if (isLockUrl(details.url)) {
G.blockUrlSet.add(details.tabId);
}
}
// 刷新清理角标数
if (details.frameId == 0 && (!['auto_subframe', 'manual_subframe', 'form_submit'].includes(details.transitionType)) && G.autoClearMode == 1) {
delete cacheData[details.tabId];
G.urlMap.delete(details.tabId);
(chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData });
SetIcon({ tabId: details.tabId });
}
// chrome内核版本 102 以下不支持 chrome.scripting.executeScript API
if (G.version < 102) { return; }
if (G.deepSearch && G.deepSearchTemporarilyClose != details.tabId) {
G.scriptList.get("search.js").tabId.add(details.tabId);
G.deepSearchTemporarilyClose = null;
}
// catch-script 脚本
G.scriptList.forEach(function (item, script) {
if (!item.tabId.has(details.tabId) || !item.allFrames) { return true; }
const files = [`catch-script/${script}`];
item.i18n && files.unshift("catch-script/i18n.js");
chrome.scripting.executeScript({
target: { tabId: details.tabId, frameIds: [details.frameId] },
files: files,
injectImmediately: true,
world: item.world
});
});
// 模拟手机
if (G.initLocalComplete && G.featMobileTabId.size > 0 && G.featMobileTabId.has(details.tabId)) {
chrome.scripting.executeScript({
args: [G.MobileUserAgent.toString()],
target: { tabId: details.tabId, frameIds: [details.frameId] },
func: function () {
Object.defineProperty(navigator, 'userAgent', { value: arguments[0], writable: false });
},
injectImmediately: true,
world: "MAIN"
});
}
});
/**
* 监听 标签关闭 清理数据
*/
chrome.tabs.onRemoved.addListener(function (tabId) {
// 清理缓存数据
chrome.alarms.get("nowClear", function (alarm) {
!alarm && chrome.alarms.create("nowClear", { when: Date.now() + 1000 });
});
if (G.initSyncComplete) {
G.blockUrlSet.has(tabId) && G.blockUrlSet.delete(tabId);
}
});
/**
* 浏览器 扩展快捷键
*/
chrome.commands.onCommand.addListener(function (command) {
if (command == "auto_down") {
if (G.featAutoDownTabId.has(G.tabId)) {
G.featAutoDownTabId.delete(G.tabId);
} else {
G.featAutoDownTabId.add(G.tabId);
}
(chrome.storage.session ?? chrome.storage.local).set({ featAutoDownTabId: Array.from(G.featAutoDownTabId) });
} else if (command == "catch") {
const scriptTabid = G.scriptList.get("catch.js").tabId;
scriptTabid.has(G.tabId) ? scriptTabid.delete(G.tabId) : scriptTabid.add(G.tabId);
chrome.tabs.reload(G.tabId, { bypassCache: true });
} else if (command == "m3u8") {
chrome.tabs.create({ url: "m3u8.html" });
} else if (command == "clear") {
delete cacheData[G.tabId];
(chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData });
clearRedundant();
SetIcon({ tabId: G.tabId });
} else if (command == "enable") {
G.enable = !G.enable;
chrome.storage.sync.set({ enable: G.enable });
chrome.action.setIcon({ path: G.enable ? "/img/icon.png" : "/img/icon-disable.png" });
} else if (command == "reboot") {
chrome.runtime.reload();
} else if (command == "deepSearch") {
const script = G.scriptList.get("search.js");
const scriptTabid = script.tabId;
if (scriptTabid.has(G.tabId)) {
scriptTabid.delete(G.tabId);
G.deepSearchTemporarilyClose = G.tabId;
chrome.tabs.reload(G.tabId, { bypassCache: true });
return;
}
scriptTabid.add(G.tabId);
chrome.tabs.reload(G.tabId, { bypassCache: true });
}
});
/**
* 监听 页面完全加载完成 判断是否在线ffmpeg页面
* 如果是在线ffmpeg 则发送数据
*/
chrome.webNavigation.onCompleted.addListener(function (details) {
if (G.ffmpegConfig.tab && details.tabId == G.ffmpegConfig.tab) {
setTimeout(() => {
G.ffmpegConfig.cacheData.forEach(data => {
chrome.tabs.sendMessage(details.tabId, data);
});
G.ffmpegConfig.cacheData = [];
G.ffmpegConfig.tab = 0;
}, 500);
}
});
/**
* 检查扩展名和大小
* @param {String} ext
* @param {Number} size
* @returns {Boolean|String}
*/
function CheckExtension(ext, size) {
const Ext = G.Ext.get(ext);
if (!Ext) { return false; }
if (!Ext.state) { return "break"; }
if (Ext.size != 0 && size != undefined && size <= Ext.size * 1024) { return "break"; }
return true;
}
/**
* 检查类型和大小
* @param {String} dataType
* @param {Number} dataSize
* @returns {Boolean|String}
*/
function CheckType(dataType, dataSize) {
const typeInfo = G.Type.get(dataType.split("/")[0] + "/*") || G.Type.get(dataType);
if (!typeInfo) { return false; }
if (!typeInfo.state) { return "break"; }
if (typeInfo.size != 0 && dataSize != undefined && dataSize <= typeInfo.size * 1024) { return "break"; }
return true;
}
/**
* 获取文件名及扩展名
* @param {String} pathname
* @returns {Array}
*/
function fileNameParse(pathname) {
let fileName = decodeURI(pathname.split("/").pop());
let ext = fileName.split(".");
ext = ext.length == 1 ? undefined : ext.pop().toLowerCase();
return [fileName, ext ? ext : undefined];
}
/**
* 获取响应头信息
* @param {Object} data
* @returns {Object}
*/
function getResponseHeadersValue(data) {
const header = {};
if (data.responseHeaders == undefined || data.responseHeaders.length == 0) { return header; }
for (let item of data.responseHeaders) {
item.name = item.name.toLowerCase();
if (item.name == "content-length") {
header.size ??= parseInt(item.value);
} else if (item.name == "content-type") {
header.type = item.value.split(";")[0].toLowerCase();
} else if (item.name == "content-disposition") {
header.attachment = item.value;
} else if (item.name == "content-range") {
let size = item.value.split('/')[1];
if (size !== '*') {
header.size = parseInt(size);
}
}
}
return header;
}
/**
* 获取请求头
* @param {Object} data
* @returns {Object|Boolean}
*/
function getRequestHeaders(data) {
if (data.allRequestHeaders == undefined || data.allRequestHeaders.length == 0) { return false; }
const header = {};
for (let item of data.allRequestHeaders) {
item.name = item.name.toLowerCase();
if (item.name == "referer") {
header.referer = item.value;
} else if (item.name == "origin") {
header.origin = item.value;
} else if (item.name == "cookie") {
header.cookie = item.value;
} else if (item.name == "authorization") {
header.authorization = item.value;
}
}
if (Object.keys(header).length) {
return header;
}
return false;
}
//设置扩展图标
function SetIcon(obj) {
if (obj?.number == 0 || obj?.number == undefined) {
chrome.action.setBadgeText({ text: "", tabId: obj?.tabId ?? G.tabId }, function () { if (chrome.runtime.lastError) { return; } });
// chrome.action.setTitle({ title: "还没闻到味儿~", tabId: obj.tabId }, function () { if (chrome.runtime.lastError) { return; } });
} else if (G.badgeNumber) {
obj.number = obj.number > 999 ? "999+" : obj.number.toString();
chrome.action.setBadgeText({ text: obj.number, tabId: obj.tabId }, function () { if (chrome.runtime.lastError) { return; } });
// chrome.action.setTitle({ title: "抓到 " + obj.number + " 条鱼", tabId: obj.tabId }, function () { if (chrome.runtime.lastError) { return; } });
}
}
// 模拟手机端
function mobileUserAgent(tabId, change = false) {
if (change) {
G.featMobileTabId.add(tabId);
(chrome.storage.session ?? chrome.storage.local).set({ featMobileTabId: Array.from(G.featMobileTabId) });
chrome.declarativeNetRequest.updateSessionRules({
removeRuleIds: [tabId],
addRules: [{
"id": tabId,
"action": {
"type": "modifyHeaders",
"requestHeaders": [{
"header": "User-Agent",
"operation": "set",
"value": G.MobileUserAgent
}]
},
"condition": {
"tabIds": [tabId],
"resourceTypes": Object.values(chrome.declarativeNetRequest.ResourceType)
}
}]
});
return true;
}
G.featMobileTabId.delete(tabId) && (chrome.storage.session ?? chrome.storage.local).set({ featMobileTabId: Array.from(G.featMobileTabId) });
chrome.declarativeNetRequest.updateSessionRules({
removeRuleIds: [tabId]
});
}
// 判断特殊页面
function isSpecialPage(url) {
if (!url || url == "null") { return true; }
return !(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("blob:"));
}
// 测试
// chrome.storage.local.get(function (data) { console.log(data.MediaData) });
// chrome.declarativeNetRequest.getSessionRules(function (rules) { console.log(rules); });
// chrome.tabs.query({}, function (tabs) { for (let item of tabs) { console.log(item.id); } });

274
js/content-script.js Normal file
View File

@@ -0,0 +1,274 @@
(function () {
var _videoObj = [];
var _videoSrc = [];
var _key = new Set();
chrome.runtime.onMessage.addListener(function (Message, sender, sendResponse) {
if (chrome.runtime.lastError) { return; }
// 获取页面视频对象
if (Message.Message == "getVideoState") {
let videoObj = [];
let videoSrc = [];
document.querySelectorAll("video, audio").forEach(function (video) {
if (video.currentSrc != "" && video.currentSrc != undefined) {
videoObj.push(video);
videoSrc.push(video.currentSrc);
}
});
const iframe = document.querySelectorAll("iframe");
if (iframe.length > 0) {
iframe.forEach(function (iframe) {
if (iframe.contentDocument == null) { return true; }
iframe.contentDocument.querySelectorAll("video, audio").forEach(function (video) {
if (video.currentSrc != "" && video.currentSrc != undefined) {
videoObj.push(video);
videoSrc.push(video.currentSrc);
}
});
});
}
if (videoObj.length > 0) {
if (videoObj.length !== _videoObj.length || videoSrc.toString() !== _videoSrc.toString()) {
_videoSrc = videoSrc;
_videoObj = videoObj;
}
Message.index = Message.index == -1 ? 0 : Message.index;
const video = videoObj[Message.index];
const timePCT = video.currentTime / video.duration * 100;
sendResponse({
time: timePCT,
currentTime: video.currentTime,
duration: video.duration,
volume: video.volume,
count: _videoObj.length,
src: _videoSrc,
paused: video.paused,
loop: video.loop,
speed: video.playbackRate,
muted: video.muted,
type: video.tagName.toLowerCase()
});
return true;
}
sendResponse({ count: 0 });
return true;
}
// 速度控制
if (Message.Message == "speed") {
_videoObj[Message.index].playbackRate = Message.speed;
return true;
}
// 画中画
if (Message.Message == "pip") {
if (document.pictureInPictureElement) {
try { document.exitPictureInPicture(); } catch (e) { return true; }
sendResponse({ state: false });
return true;
}
try { _videoObj[Message.index].requestPictureInPicture(); } catch (e) { return true; }
sendResponse({ state: true });
return true;
}
// 全屏
if (Message.Message == "fullScreen") {
if (document.fullscreenElement) {
try { document.exitFullscreen(); } catch (e) { return true; }
sendResponse({ state: false });
return true;
}
setTimeout(function () {
try { _videoObj[Message.index].requestFullscreen(); } catch (e) { return true; }
}, 500);
sendResponse({ state: true });
return true;
}
// 播放
if (Message.Message == "play") {
_videoObj[Message.index].play();
return true;
}
// 暂停
if (Message.Message == "pause") {
_videoObj[Message.index].pause();
return true;
}
// 循环播放
if (Message.Message == "loop") {
_videoObj[Message.index].loop = Message.action;
return true;
}
// 设置音量
if (Message.Message == "setVolume") {
_videoObj[Message.index].volume = Message.volume;
sendResponse("ok");
return true;
}
// 静音
if (Message.Message == "muted") {
_videoObj[Message.index].muted = Message.action;
return true;
}
// 设置视频进度
if (Message.Message == "setTime") {
const time = Message.time * _videoObj[Message.index].duration / 100;
_videoObj[Message.index].currentTime = time;
sendResponse("ok");
return true;
}
// 截图视频图片
if (Message.Message == "screenshot") {
try {
let video = _videoObj[Message.index];
let canvas = document.createElement("canvas");
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext("2d").drawImage(video, 0, 0, canvas.width, canvas.height);
let link = document.createElement("a");
link.href = canvas.toDataURL("image/jpeg");
link.download = `${location.hostname}-${secToTime(video.currentTime)}.jpg`;
link.click();
canvas = null;
link = null;
sendResponse("ok");
return true;
} catch (e) { console.log(e); return true; }
}
if (Message.Message == "getKey") {
sendResponse(Array.from(_key));
return true;
}
if (Message.Message == "ffmpeg") {
if (!Message.files) {
window.postMessage(Message);
sendResponse("ok");
return true;
}
Message.quantity ??= Message.files.length;
for (let item of Message.files) {
const data = { ...Message, ...item };
data.type = item.type ?? "video";
if (data.data instanceof Blob) {
window.postMessage(data);
} else {
fetch(data.data)
.then(response => response.blob())
.then(blob => {
data.data = blob;
window.postMessage(data);
});
}
}
sendResponse("ok");
return true;
}
if (Message.Message == "getPage") {
if (Message.find) {
const DOM = document.querySelector(Message.find);
DOM ? sendResponse(DOM.innerHTML) : sendResponse("");
return true;
}
sendResponse(document.documentElement.outerHTML);
return true;
}
});
// Heart Beat
var Port;
function connect() {
Port = chrome.runtime.connect(chrome.runtime.id, { name: "HeartBeat" });
Port.postMessage("HeartBeat");
Port.onMessage.addListener(function (message, Port) { return true; });
Port.onDisconnect.addListener(connect);
}
connect();
function secToTime(sec) {
let time = "";
let hour = Math.floor(sec / 3600);
let min = Math.floor((sec % 3600) / 60);
sec = Math.floor(sec % 60);
if (hour > 0) { time = hour + "'"; }
if (min < 10) { time += "0"; }
time += min + "'";
if (sec < 10) { time += "0"; }
time += sec;
return time;
}
window.addEventListener("message", (event) => {
if (!event.data || !event.data.action) { return; }
if (event.data.action == "catCatchAddMedia") {
if (!event.data.url) { return; }
chrome.runtime.sendMessage({
Message: "addMedia",
url: event.data.url,
href: event.data.href ?? event.source.location.href,
extraExt: event.data.ext,
mime: event.data.mime,
requestHeaders: { referer: event.data.referer },
requestId: event.data.requestId
});
}
if (event.data.action == "catCatchAddKey") {
let key = event.data.key;
if (key instanceof ArrayBuffer || key instanceof Array) {
key = ArrayToBase64(key);
}
if (_key.has(key)) { return; }
_key.add(key);
chrome.runtime.sendMessage({
Message: "send2local",
action: "addKey",
data: key,
});
chrome.runtime.sendMessage({
Message: "popupAddKey",
data: key,
url: event.data.url,
});
}
if (event.data.action == "catCatchFFmpeg") {
if (!event.data.use ||
!event.data.files ||
!event.data.files instanceof Array ||
event.data.files.length == 0
) { return; }
event.data.title = event.data.title ?? document.title ?? new Date().getTime().toString();
event.data.title = event.data.title.replaceAll('"', "").replaceAll("'", "").replaceAll(" ", "");
let data = {
Message: event.data.action,
action: event.data.use,
files: event.data.files,
url: event.data.href ?? event.source.location.href,
};
data = { ...event.data, ...data };
chrome.runtime.sendMessage(data);
}
if (event.data.action == "catCatchFFmpegResult") {
if (!event.data.state || !event.data.tabId) { return; }
chrome.runtime.sendMessage({ Message: "catCatchFFmpegResult", ...event.data });
}
if (event.data.action == "catCatchToBackground") {
delete event.data.action;
chrome.runtime.sendMessage(event.data);
}
if (event.data.action == "catCatchDashDRMMedia") {
// TODO DRM Media
console.log("DRM Media", event);
}
}, false);
function ArrayToBase64(data) {
try {
let bytes = new Uint8Array(data);
let binary = "";
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
if (typeof _btoa == "function") {
return _btoa(binary);
}
return btoa(binary);
} catch (e) {
return false;
}
}
})();

421
js/downloader.js Normal file
View File

@@ -0,0 +1,421 @@
// url 参数解析
const params = new URL(location.href).searchParams;
const _requestId = params.get("requestId") ? params.get("requestId").split(",") : []; // 要下载得资源ID
const _ffmpeg = params.get("ffmpeg"); // 启用在线FFmpeg
let _downStream = params.get("downStream"); // 启用边下边存 流式下载
const _data = []; // 通过_requestId获取得到得数据
const _taskId = Date.parse(new Date()); // 配合ffmpeg使用的任务ID 以便在线ffmpeg通过ID知道文件属于哪些任务
let _tabId = null; // 当前页面tab id
let _index = null; // 当前页面 tab index
// 是否表单提交下载 表单提交 不使用自定义文件名
const downloadData = localStorage.getItem('downloadData') ? JSON.parse(localStorage.getItem('downloadData')) : [];
awaitG(() => {
loadCSS();
// 获取当前标签信息
chrome.tabs.getCurrent(function (tabs) {
_tabId = tabs.id;
_index = tabs.index;
// 如果没有requestId 显示 提交表单
if (!_requestId.length) {
$("#downStream").prop("checked", G.downStream);
$("#getURL, .newDownload").toggle();
$("#getURL_btn").click(function () {
const data = [{
url: $("#getURL #url").val().trim(),
requestId: 1,
}];
// 处理请求头 如果是url直接放入referer 支持json格式
const referer = $("#getURL #referer").val().trim();
if (referer) {
if (referer.startsWith("http")) {
data[0].requestHeaders = { referer: referer };
} else {
data[0].requestHeaders = JSONparse(referer);
}
}
_downStream = $("#downStream").prop("checked");
_data.push(...data);
setHeaders(data, start(), _tabId);
$("#getURL, .newDownload").toggle();
});
return;
}
// 优先从downloadData 提取任务数据
for (let item of downloadData) {
if (_requestId.includes(item.requestId)) {
_data.push(item);
_requestId.splice(_requestId.indexOf(item.requestId), 1);
}
}
if (!_requestId.length) {
setHeaders(_data, start(), _tabId);
return;
}
// downloadData 不存在 从后台获取数据
chrome.runtime.sendMessage({ Message: "getData", requestId: _requestId }, function (data) {
if (data == "error" || !Array.isArray(data) || chrome.runtime.lastError || data.length == 0) {
alert(i18n.dataFetchFailed);
return;
}
_data.push(...data);
setHeaders(data, start(), _tabId);
});
});
});
function start() {
// 提前打开ffmpeg页面
if (_ffmpeg) {
chrome.runtime.sendMessage({
Message: "catCatchFFmpeg",
action: "openFFmpeg",
extra: i18n.waitingForMedia
});
}
$("#autoClose").prop("checked", G.downAutoClose);
streamSaver.mitm = G.streamSaverConfig.url;
const $downBox = $("#downBox"); // 下载列表容器
const down = new Downloader(_data); // 创建下载器
const itemDOM = new Map(); // 提前储存需要平凡操作的dom对象 提高效率
$("#test").click(() => console.log(down));
// 添加html
const addHtml = (fragment) => {
if (!fragment.downFileName) {
fragment.downFileName = getUrlFileName(fragment.url);
}
const html = $(`
<div class="downItem">
<div class="explain">${fragment.downFileName}</div>
<div id="downFilepProgress"></div>
<div class="progress-container">
<div class="progress-wrapper">
<div class="progress-bar">
<div class="progress"></div>
</div>
</div>
<button class="cancel-btn">${i18n.stopDownload}</button>
</div>
</div>`);
const $button = html.find("button");
$button.data("action", "stop");
// 操作对象放入itemDOM 提高效率
itemDOM.set(fragment.index, {
progressText: html.find("#downFilepProgress"),
progress: html.find(".progress"),
button: $button
});
$button.click(function () {
const action = $(this).data("action");
if (action == "stop") {
down.stop(fragment.index);
$(this).html(i18n.retryDownload).data("action", "start");
if (fragment.fileStream) {
fragment.fileStream.close();
}
} else if (action == "start") {
if (fragment.fileStream) {
fragment.fileStream = streamSaver.createWriteStream(fragment.downFileName).getWriter();
}
down.state = "waiting";
down.downloader(fragment);
$(this).html(i18n.stopDownload).data("action", "stop");
}
});
$downBox.append(html);
// 流式下载处理
if ((_downStream || G.downStream) && !_ffmpeg) {
fragment.fileStream = streamSaver.createWriteStream(fragment.downFileName).getWriter();
}
}
// 下载列表添加对应html
down.fragments.forEach(addHtml);
// 文件进程事件
let lastEmitted = Date.now();
down.on('itemProgress', function (fragment, state, receivedLength, contentLength, value) {
// 通过 lastEmitted 限制更新频率 避免疯狂dom操作
if (Date.now() - lastEmitted >= 100 && !state) {
const $dom = itemDOM.get(fragment.index);
if (contentLength) {
const progress = (receivedLength / contentLength * 100).toFixed(2) + "%";
$dom.progress.css("width", progress).html(progress);
$dom.progressText.html(`${byteToSize(receivedLength)} / ${byteToSize(contentLength)}`);
} else {
$dom.progressText.html(`${byteToSize(receivedLength)}`);
}
if (down.total == 1) {
const title = contentLength ?
`${byteToSize(receivedLength)} / ${byteToSize(contentLength)}` :
`${byteToSize(receivedLength)}`;
document.title = title;
}
lastEmitted = Date.now();
}
});
// 单文件下载完成事件
down.on('completed', function (buffer, fragment) {
const $dom = itemDOM.get(fragment.index);
$dom.progress.css("width", "100%").html("100%");
$dom.progressText.html(i18n.downloadComplete);
$dom.button.html(i18n.sendFfmpeg).data("action", "sendFfmpeg");
document.title = `${down.success}/${down.total}`;
$dom.button.hide();
// 是流式下载 停止写入
if (fragment.fileStream) {
fragment.fileStream.close();
fragment.fileStream = null;
return;
}
// 转为blob
const blob = ArrayBufferToBlob(buffer, { type: fragment.contentType });
// 发送到ffmpeg
if (_ffmpeg) {
sendFile(_ffmpeg, blob, fragment);
$dom.progressText.html(i18n.sendFfmpeg);
return;
}
$dom.progressText.html(i18n.saving);
// 直接下载
chrome.downloads.download({
url: URL.createObjectURL(blob),
filename: fragment.downFileName,
saveAs: G.saveAs
}, function (downloadId) {
fragment.downId = downloadId;
});
});
// 全部下载完成事件
down.on('allCompleted', function (buffer) {
$("#stopDownload").hide();
// 检查 down.fragments 是否都为边下边存 检查自动关闭
if (down.fragments.every(item => item.fileStream) && $("#autoClose").prop("checked")) {
setTimeout(() => {
closeTab();
}, Math.ceil(Math.random() * 999));
}
});
// 错误处理
down.on('downloadError', function (fragment, error) {
// 添加range请求头 重新尝试下载
if (!fragment.retry?.Range && error?.cause == "HTTPError") {
fragment.retry = { "Range": "bytes=0-" };
down.stop(fragment.index);
down.downloader(fragment);
return;
}
// 添加sec-fetch 再次尝试下载
if (!fragment.retry?.sec && error?.cause == "HTTPError") {
fragment.retry.sec = true;
if (!fragment.requestHeaders) { fragment.requestHeaders = {}; }
fragment.requestHeaders = { ...fragment.requestHeaders, "sec-fetch-mode": "no-cors", "sec-fetch-site": "same-site" };
setHeaders(fragment, () => { down.stop(fragment.index); down.downloader(fragment); }, _tabId);
return;
}
itemDOM.get(fragment.index).progressText.html(error);
chrome.tabs.highlight({ tabs: _index });
});
// 开始下载事件 如果存在range重下标记 则添加 range 请求头
down.on('start', function (fragment, options) {
if (fragment.retry) {
options.headers = fragment.retry;
options.cache = "no-cache";
}
});
// 全部停止下载按钮
$("#stopDownload").click(function () {
down.stop();
// 更新对应的按钮状态
itemDOM.forEach((item, index) => {
if (item.button.data("action") == "stop") {
item.button.html(i18n.retryDownload).data("action", "start");
if (down.fragments[index].fileStream) {
down.fragments[index].fileStream.close();
down.fragments[index].fileStream = null;
}
}
});
});
// 打开下载目录
$(".openDir").click(function () {
if (down.fragments[0].downId) {
chrome.downloads.show(down.fragments[0].downId);
return;
}
chrome.downloads.showDefaultFolder();
});
// 监听事件
chrome.runtime.onMessage.addListener(function (Message, sender, sendResponse) {
if (!Message.Message) { return; }
// 外部添加下载任务
if (Message.Message == "catDownload" && Message.data && Array.isArray(Message.data)) {
// ffmpeg任务的下载器 不允许添加新任务
if (_ffmpeg) {
sendResponse({ message: "FFmpeg", tabId: _tabId });
return;
}
setHeaders(Message.data, () => {
for (let fragment of Message.data) {
// 检查fragment是否已经存在
if (down.fragments.find(item => item.requestId == fragment.requestId)) {
continue;
}
_data.push(fragment);
down.push(fragment);
addHtml(fragment);
// 修改url requestId 参数
const url = new URL(location.href);
url.searchParams.set("requestId", down.fragments.map(item => item.requestId).join(","));
history.replaceState(null, null, url);
// 数据储存到localStorage
downloadData.push(fragment);
localStorage.setItem('downloadData', JSON.stringify(downloadData));
// 正在运行的下载任务小于线程数 则开始下载
if (down.running < down.thread) {
// down.downloader(fragment.index);
down.downloader();
}
};
}, _tabId);
sendResponse({ message: "OK", tabId: _tabId });
return;
}
// 以下为在线ffmpeg返回结果
if (Message.Message != "catCatchFFmpegResult" || Message.state != "ok" || _tabId == 0 || Message.tabId != _tabId) { return; }
// 发送状态提示
const $dom = itemDOM.get(Message.index);
$dom && $dom.progressText.html(i18n.hasSent);
down.buffer[Message.index] = null; //清空buffer
// 全部发送完成 检查自动关闭
if (down.success == down.total) {
if ($("#autoClose").prop("checked")) {
setTimeout(() => {
closeTab();
}, Math.ceil(Math.random() * 999));
}
}
});
// 监听下载事件 下载完成 关闭窗口
chrome.downloads.onChanged.addListener(function (downloadDelta) {
if (!downloadDelta.state || downloadDelta.state.current != "complete") { return; }
// 检查id是否本页面提交的下载
const fragment = down.fragments.find(item => item.downId == downloadDelta.id);
if (!fragment) { return; }
down.buffer[fragment.index] = null; //清空buffer
// 更新下载状态
itemDOM.get(fragment.index).progressText.html(i18n.downloadComplete);
// 完成下载 检查自动关闭
if (down.success == down.total) {
document.title = i18n.downloadComplete;
if ($("#autoClose").prop("checked")) {
setTimeout(() => {
closeTab();
}, Math.ceil(Math.random() * 999));
}
}
});
// 关闭页面 检查关闭所有未完成的下载流
window.addEventListener('beforeunload', function (e) {
const fileStream = down.fragments.filter(item => item.fileStream);
if (fileStream.length) {
e.preventDefault();
fileStream.forEach((fragment) => {
fragment.fileStream.close();
});
}
});
document.title = `${down.success}/${down.total}`;
down.start();
}
/**
* 发送数据到在线FFmpeg
* @param {String} action 发送类型
* @param {ArrayBuffer|Blob} data 数据内容
* @param {Object} fragment 数据对象
*/
let isCreatingTab = false;
function sendFile(action, data, fragment) {
// 转 blob
if (data instanceof ArrayBuffer) {
data = ArrayBufferToBlob(data, { type: fragment.contentType });
}
chrome.tabs.query({ url: G.ffmpegConfig.url + "*" }, function (tabs) {
// 等待ffmpeg 打开并且可用
if (tabs.length === 0) {
if (!isCreatingTab) {
isCreatingTab = true; // 设置创建标志位
chrome.tabs.create({ url: G.ffmpegConfig.url });
}
setTimeout(sendFile, 500, action, data, fragment);
return;
} else if (tabs[0].status !== "complete") {
setTimeout(sendFile, 233, action, data, fragment);
return;
}
isCreatingTab = false; // 重置创建标志位
/**
* https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Chrome_incompatibilities#data_cloning_algorithm
* chrome.runtime.sendMessage API
* chrome 的对象参数需要序列化 无法传递Blob
* firefox 可以直接传递Blob
*/
const baseData = {
Message: "catCatchFFmpeg",
action: action,
files: [{ data: G.isFirefox ? data : URL.createObjectURL(data), name: getUrlFileName(fragment.url), index: fragment.index }],
title: stringModify(fragment.title),
tabId: _tabId
};
if (action === "merge") {
baseData.taskId = _taskId;
baseData.quantity = _data.length;
}
chrome.runtime.sendMessage(baseData);
});
}

22
js/firefox.js Normal file
View File

@@ -0,0 +1,22 @@
// 兼容Firefox
if (typeof (browser) == "object") {
function importScripts() {
for (let script of arguments) {
const js = document.createElement('script');
js.src = script;
document.head.appendChild(js);
}
}
// browser.windows.onFocusChanged.addListener 少一个参数
const _onFocusChanged = chrome.windows.onFocusChanged.addListener;
chrome.windows.onFocusChanged.addListener = function (listener, option) {
_onFocusChanged(listener);
};
browser.runtime.onInstalled.addListener(({ reason }) => {
if (reason == "install") {
browser.tabs.create({ url: "install.html" });
}
});
}

736
js/function.js Normal file
View File

@@ -0,0 +1,736 @@
/**
* 小于10的数字前面加0
* @param {Number} date
* @returns {String|Number}
*/
function appendZero(date) {
return parseInt(date) < 10 ? `0${date}` : date;
}
/**
* 秒转格式化成时间
* @param {Number} sec
* @returns {String}
*/
function secToTime(sec) {
let hour = (sec / 3600) | 0;
let min = ((sec % 3600) / 60) | 0;
sec = (sec % 60) | 0;
let time = hour > 0 ? hour + ":" : "";
time += min.toString().padStart(2, '0') + ":";
time += sec.toString().padStart(2, '0');
return time;
}
/**
* 字节转换成大小
* @param {Number} byte 大小
* @returns {String} 格式化后的文件大小
*/
function byteToSize(byte) {
if (!byte || byte < 1024) { return 0; }
if (byte < 1024 * 1024) {
return (byte / 1024).toFixed(1) + "KB";
} else if (byte < 1024 * 1024 * 1024) {
return (byte / 1024 / 1024).toFixed(1) + "MB";
} else {
return (byte / 1024 / 1024 / 1024).toFixed(1) + "GB";
}
}
/**
* Firefox download API 无法下载 data URL
* @param {String} url
* @param {String} fileName 文件名
*/
function downloadDataURL(url, fileName) {
let link = document.createElement("a");
link.href = url;
link.download = fileName;
link.click();
link = null;
}
/**
* 判断变量是否为空
* @param {Object|String} obj 判断的变量
* @returns {Boolean}
*/
function isEmpty(obj) {
return (typeof obj == "undefined" ||
obj == null ||
obj == "" ||
obj == " ")
}
/**
* 修改请求头
* @param {Object} data 请求头数据
* @param {Function} callback
*/
function setRequestHeaders(data = {}, callback = undefined) {
chrome.declarativeNetRequest.updateSessionRules({ removeRuleIds: [1] });
chrome.tabs.getCurrent(function (tabs) {
const rules = { removeRuleIds: [tabs ? tabs.id : 1] };
if (Object.keys(data).length) {
rules.addRules = [{
"id": tabs ? tabs.id : 1,
"priority": tabs ? tabs.id : 1,
"action": {
"type": "modifyHeaders",
"requestHeaders": Object.keys(data).map(key => ({ header: key, operation: "set", value: data[key] }))
},
"condition": {
"resourceTypes": ["xmlhttprequest", "media", "image"],
}
}];
if (tabs) {
rules.addRules[0].condition.tabIds = [tabs.id];
} else {
// initiatorDomains 只支持 chrome 101+ firefox 113+
if (G.version < 101 || (G.isFirefox && G.version < 113)) {
callback && callback();
return;
}
const domain = G.isFirefox
? new URL(chrome.runtime.getURL("")).hostname
: chrome.runtime.id;
rules.addRules[0].condition.initiatorDomains = [domain];
}
}
chrome.declarativeNetRequest.updateSessionRules(rules, function () {
callback && callback();
});
});
}
/**
* 指定标签页修改 urlFilter请求头
* @param {Object} data 需要修改请求头的对象数组
* @param {*} callBack 回调函数
* @param {*} tabId 需要修改的tabId
*/
function setHeaders(data, callBack, tabId = -1) {
if (!tabId == -1) {
tabId = G.tabId;
}
const rules = { removeRuleIds: [], addRules: [] };
if (!Array.isArray(data)) {
data = [data];
}
for (let item of data) {
if (!item.requestHeaders) { continue; }
const rule = {
"id": parseInt(item.requestId),
"action": {
"type": "modifyHeaders",
"requestHeaders": Object.keys(item.requestHeaders).map(key => ({ header: key, operation: "set", value: item.requestHeaders[key] }))
},
"condition": {
"resourceTypes": ["xmlhttprequest", "media", "image"],
"tabIds": [tabId],
"urlFilter": item.url
}
}
if (item.cookie) {
rule.action.requestHeaders.push({ header: "Cookie", operation: "set", value: item.cookie });
}
rules.removeRuleIds.push(parseInt(item.requestId));
rules.addRules.push(rule);
}
chrome.declarativeNetRequest.updateSessionRules(rules, () => {
callBack && callBack();
});
}
/**
* 等待全局变量G初始化完成
* @param {Function} callback
* @param {Number} sec
*/
function awaitG(callback, sec = 0) {
const timer = setInterval(() => {
if (G.initSyncComplete && G.initLocalComplete) {
clearInterval(timer);
callback();
}
}, sec);
}
/**
* 分割字符串 不分割引号内的内容
* @param {String} text 需要处理的文本
* @param {String} separator 分隔符
* @returns {String} 返回分割后的字符串
*/
function splitString(text, separator) {
text = text.trim();
if (text.length == 0) { return []; }
const parts = [];
let inQuotes = false;
let inSingleQuotes = false;
let start = 0;
for (let i = 0; i < text.length; i++) {
if (text[i] === separator && !inQuotes && !inSingleQuotes) {
parts.push(text.slice(start, i));
start = i + 1;
} else if (text[i] === '"' && !inSingleQuotes) {
inQuotes = !inQuotes;
} else if (text[i] === "'" && !inQuotes) {
inSingleQuotes = !inSingleQuotes;
}
}
parts.push(text.slice(start));
return parts;
}
/**
* 模板的函数处理
* @param {String} text 文本
* @param {String} action 函数名
* @param {Object} data 填充的数据
* @returns {String} 返回处理后的字符串
*/
function templatesFunction(text, action, data) {
text = isEmpty(text) ? "" : text.toString();
action = splitString(action, "|");
for (let item of action) {
let action = item.trim(); // 函数
let arg = []; //参数
// 查找 ":" 区分函数与参数
const colon = item.indexOf(":");
if (colon != -1) {
action = item.slice(0, colon).trim();
arg = splitString(item.slice(colon + 1).trim(), ",").map(item => {
return item.trim().replace(/^['"]|['"]$/g, "");
});
}
// 字符串不允许为空 除非 exists find prompt函数
if (isEmpty(text) && !["exists", "find", "prompt"].includes(action)) { return "" };
// 参数不能为空 除非 filter prompt函数
if (arg.length == 0 && !["filter", "prompt"].includes(action)) { return text }
if (action == "slice") {
text = text.slice(...arg);
} else if (action == "replace") {
text = text.replace(...arg);
} else if (action == "replaceAll") {
text = text.replaceAll(...arg);
} else if (action == "regexp") {
const result = text.match(new RegExp(...arg));
text = "";
if (result && result.length >= 2) {
for (let i = 1; i < result.length; i++) {
if (result[i]) {
text += result[i].trim();
}
}
}
} else if (action == "exists") {
if (text) {
text = arg[0].replaceAll("*", text);
continue;
}
if (arg[1]) {
text = arg[1].replaceAll("*", text);
continue;
}
text = "";
} else if (action == "prepend") {
text = arg[0] + text;
} else if (action == "concat") {
text = text + arg[0];
} else if (action == "to") {
if (arg[0] == "base64") {
text = window.Base64 ? Base64.encode(text) : btoa(unescape(encodeURIComponent(text)));
} else if (arg[0] == "urlEncode") {
text = encodeURIComponent(text);
} else if (arg[0] == "urlDecode") {
text = decodeURIComponent(text);
} else if (arg[0] == "lowerCase") {
text = text.toLowerCase();
} else if (arg[0] == "upperCase") {
text = text.toUpperCase();
} else if (arg[0] == "trim") {
if (text) { text = text.trim(); }
} else if (arg[0] == "filter") {
if (text) { text = text.trim(); }
text = stringModify(text);
}
} else if (action == "find") {
text = "";
if (data.pageDOM) {
try {
text = data.pageDOM.querySelector(arg[0]).innerText?.trim();
} catch (e) { text = ""; }
}
} else if (action == "filter") {
text = stringModify(text, arg[0]);
} else if (action == "prompt") {
text = window.prompt("", text);
}
}
return text;
}
/**
* 模板替换
* @param {String} text 标签模板
* @param {Object} data 填充的数据
* @returns {String} 返回填充后的字符串
*/
function templates(text, data) {
if (isEmpty(text)) { return ""; }
// fullFileName
try {
data.fullFileName = new URL(data.url).pathname.split("/").pop();
} catch (e) {
data.fullFileName = 'NULL';
}
// fileName
data.fileName = data.fullFileName.split(".");
data.fileName.length > 1 && data.fileName.pop();
data.fileName = data.fileName.join(".");
// ext
if (isEmpty(data.ext)) {
data.ext = data.fullFileName.split(".");
data.ext = data.ext.length == 1 ? "" : data.ext[data.ext.length - 1];
}
const date = new Date();
const trimData = {
// 资源信息
url: data.url ?? "",
referer: data.requestHeaders?.referer ?? "",
origin: data.requestHeaders?.origin ?? "",
initiator: data.requestHeaders?.referer ? data.requestHeaders.referer : data.initiator,
webUrl: data.webUrl ?? "",
title: data._title ?? data.title,
pageDOM: data.pageDOM,
cookie: data.cookie ?? "",
tabId: data.tabId ?? 0,
// 时间相关
year: date.getFullYear(),
month: appendZero(date.getMonth() + 1),
date: appendZero(date.getDate()),
day: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][date.getDay()],
fullDate: `${date.getFullYear()}-${appendZero(date.getMonth() + 1)}-${appendZero(date.getDate())}`,
time: `${appendZero(date.getHours())}'${appendZero(date.getMinutes())}'${appendZero(date.getSeconds())}`,
hours: appendZero(date.getHours()),
minutes: appendZero(date.getMinutes()),
seconds: appendZero(date.getSeconds()),
now: Date.now(),
timestamp: new Date().toISOString(),
// 文件名
fullFileName: data.fullFileName ? data.fullFileName : "",
fileName: data.fileName ? data.fileName : "",
ext: data.ext ?? "",
// 全局变量
mobileUserAgent: G.MobileUserAgent,
userAgent: G.userAgent ? G.userAgent : navigator.userAgent,
}
const _data = { ...data, ...trimData };
text = text.replace(reTemplates, function (original, tag, action) {
tag = tag.trim();
// 特殊标签 data 返回所有数据
if (tag == 'data') { return JSON.stringify(trimData); }
if (action) {
return templatesFunction(_data[tag], action.trim(), _data);
}
return _data[tag] ?? original;
});
return text;
}
/**
* 从url中获取文件名
* @param {String} url
* @returns {String} 文件名
*/
function getUrlFileName(url) {
let pathname = new URL(url).pathname;
let filename = pathname.split("/").pop();
return filename ? filename : "NULL";
}
/**
* 解析json字符串 尝试修复键名没有双引号 解析错误返回默认值
* @param {string} str json字符串
* @param {object} error 解析错误返回的默认值
* @param {number} attempt 尝试修复次数
* @returns {object} 返回解析后的对象
*/
function JSONparse(str, error = {}, attempt = 0) {
if (!str) { return error; }
try {
return JSON.parse(str);
} catch (e) {
if (attempt === 0) {
// 第一次解析失败,修正字符串后递归调用
reJSONparse.lastIndex = 0;
const fixedStr = str.replace(reJSONparse, '$1"$2"$3');
return JSONparse(fixedStr, error, ++attempt);
} else {
// 第二次解析仍然失败,返回 error 对象
return error;
}
}
}
/**
* ArrayBuffer转Blob 大于2G的做切割
* @param {ArrayBuffer|Uint8Array} buffer 原始数据
* @param {Object} options Blob配置
* @returns {Blob} 返回Blob对象
*/
function ArrayBufferToBlob(buffer, options = {}) {
if (buffer instanceof Blob) {
return buffer;
}
if (buffer instanceof Uint8Array) {
buffer = buffer.buffer;
}
if (!buffer.byteLength) {
return new Blob();
}
if (!buffer instanceof ArrayBuffer) {
return new Blob();
}
if (buffer.byteLength >= 2 * 1024 * 1024 * 1024) {
const MAX_CHUNK_SIZE = 1024 * 1024 * 1024;
let offset = 0;
const blobs = [];
while (offset < buffer.byteLength) {
const chunkSize = Math.min(MAX_CHUNK_SIZE, buffer.byteLength - offset);
const chunk = buffer.slice(offset, offset + chunkSize);
blobs.push(new Blob([chunk]));
offset += chunkSize;
}
return new Blob(blobs, options);
}
return new Blob([buffer], options);
}
/**
* 清理冗余数据
*/
function clearRedundant() {
chrome.tabs.query({}, function (tabs) {
const allTabId = new Set(tabs.map(tab => tab.id));
if (!cacheData.init) {
// 清理 缓存数据
let cacheDataFlag = false;
for (let key in cacheData) {
if (!allTabId.has(Number(key))) {
cacheDataFlag = true;
delete cacheData[key];
}
}
cacheDataFlag && (chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData });
}
// 清理
G.urlMap.forEach((_, key) => {
!allTabId.has(key) && G.urlMap.delete(key);
});
// 清理脚本
G.scriptList.forEach(function (scriptList) {
scriptList.tabId.forEach(function (tabId) {
if (!allTabId.has(tabId)) {
scriptList.tabId.delete(tabId);
}
});
});
if (!G.initLocalComplete) { return; }
// 清理 declarativeNetRequest 模拟手机
chrome.declarativeNetRequest.getSessionRules(function (rules) {
let mobileFlag = false;
for (let item of rules) {
if (item.condition.tabIds) {
// 如果tabIds列表都不存在 则删除该条规则
if (!item.condition.tabIds.some(id => allTabId.has(id))) {
mobileFlag = true;
item.condition.tabIds.forEach(id => G.featMobileTabId.delete(id));
chrome.declarativeNetRequest.updateSessionRules({
removeRuleIds: [item.id]
});
}
} else if (item.id == 1) {
// 清理预览视频增加的请求头
chrome.declarativeNetRequest.updateSessionRules({ removeRuleIds: [1] });
}
}
mobileFlag && (chrome.storage.session ?? chrome.storage.local).set({ featMobileTabId: Array.from(G.featMobileTabId) });
});
// 清理自动下载
let autoDownFlag = false;
G.featAutoDownTabId.forEach(function (tabId) {
if (!allTabId.has(tabId)) {
autoDownFlag = true;
G.featAutoDownTabId.delete(tabId);
}
});
autoDownFlag && (chrome.storage.session ?? chrome.storage.local).set({ featAutoDownTabId: Array.from(G.featAutoDownTabId) });
G.blockUrlSet = new Set([...G.blockUrlSet].filter(x => allTabId.has(x)));
if (G.requestHeaders.size >= 10240) {
G.requestHeaders.clear();
}
});
// G.referer.clear();
// G.blackList.clear();
// G.temp.clear();
}
/**
* 替换掉文件名中的特殊字符 包含路径
* @param {String} str 需要处理的文本
* @param {String} text 需要替换的文本
* @returns {String} 返回替换后的字符串
*/
function stringModify(str, text) {
if (!str) { return str; }
str = filterFileName(str, text);
return str.replaceAll("\\", "&bsol;").replaceAll("/", "&sol;");
}
/**
* 替换掉文件名中的特殊字符 不包含路径
* @param {String} str 需要处理的文本
* @param {String} text 需要替换的文本
* @returns {String} 返回替换后的字符串
*/
function filterFileName(str, text) {
if (!str) { return str; }
reFilterFileName.lastIndex = 0;
str = str.replaceAll(/\u200B/g, "").replaceAll(/\u200C/g, "").replaceAll(/\u200D/g, "");
str = str.replace(reFilterFileName, function (match) {
return text || {
'<': '&lt;',
'>': '&gt;',
':': '&colon;',
'"': '&quot;',
'|': '&vert;',
'?': '&quest;',
'*': '&ast;',
'~': '_'
}[match];
});
// 前后不能是 “.”
if (str.endsWith(".")) {
str = str + "catCatch";
}
if (str.startsWith(".")) {
str = "catCatch" + str;
}
return str;
}
/**
* 展平嵌套对象的函数
* @param {Object} obj 参数对象
* @param {String} prefix 前缀
* @returns 嵌套对象扁平化
*/
function flattenObject(obj, prefix = '') {
let result = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const value = obj[key];
const newKey = prefix ? `${prefix}[${key}]` : key;
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
// 递归处理嵌套对象
Object.assign(result, flattenObject(value, newKey));
} else {
// 处理基本类型和数组
result[newKey] = value;
}
}
}
return result;
}
/**
* 发送数据到本地
* @param {String} action 发送类型
* @param {Object|Srting} data 发送的数据
* @param {Number} tabId 发送数据的标签页ID
*/
function send2local(action, data, tabId = 0) {
return new Promise((resolve, reject) => {
// 请求方式
const option = { method: G.send2localMethod };
// 处理替换模板
let body = G.send2localBody;
// 处理 addKey 请求
if (action == 'addKey' || typeof data === 'string') {
body = G.send2localBody.replaceAll('${data}', `"${data}"`);
data = { tabId: tabId };
}
data.action = action;
let postData = templates(body, data);
// 转为对象
postData = JSONparse(postData, { action, data, tabId });
try {
// 处理URL中的模板字符串并检查合法性
let send2localURL = templates(G.send2localURL, data);
send2localURL = new URL(send2localURL);
// GET请求拼接参数
if (option.method === 'GET') {
const flattenedObj = flattenObject(postData);
const urlParams = new URLSearchParams(flattenedObj);
send2localURL.search = send2localURL.search
? `${send2localURL.search}&${urlParams}`
: `?${urlParams}`;
}
// 非GET请求处理不同Content-Type
else {
const contentType = {
0: 'application/json;charset=utf-8',
1: 'multipart/form-data',
2: 'application/x-www-form-urlencoded',
3: 'text/plain'
}[G.send2localType];
option.headers = { 'Content-Type': contentType };
switch (contentType) {
case 'application/json;charset=utf-8':
option.body = JSON.stringify(postData);
break;
case 'multipart/form-data':
const formData = new FormData();
const flattened = flattenObject(postData);
Object.entries(flattened).forEach(([key, value]) => {
formData.append(key, value);
});
option.body = formData;
delete option.headers['Content-Type']; // 浏览器自动生成boundary
break;
case 'application/x-www-form-urlencoded':
const flattenedObj = flattenObject(postData);
const urlParams = new URLSearchParams(flattenedObj);
option.body = urlParams.toString();
break;
case 'text/plain':
option.body = typeof postData === 'object'
? JSON.stringify(postData)
: String(postData);
break;
default:
option.body = JSON.stringify(postData);
break;
}
}
send2localURL = send2localURL.toString();
fetch(send2localURL, option)
.then(response => resolve(response))
.catch(error => reject(error));
} catch (e) {
reject(e);
}
});
}
/**
* 判断url是否在屏蔽网址中
* @param {String} url
* @returns {Boolean}
*/
function isLockUrl(url) {
for (let key in G.blockUrl) {
if (!G.blockUrl[key].state) { continue; }
G.blockUrl[key].url.lastIndex = 0;
if (G.blockUrl[key].url.test(url)) {
return true;
}
}
return false;
}
/**
* 关闭标签页 如果tabId为0 则关闭当前标签
* 当前只有一个标签页面 创建新标签页 再关闭
* @param {Number|Array} tabId
*/
function closeTab(tabId = 0) {
chrome.tabs.query({}, async function (tabs) {
if (tabs.length === 1) {
await chrome.tabs.create({ url: 'chrome://newtab' });
tabId ? chrome.tabs.remove(tabId) : window.close();
} else {
tabId ? chrome.tabs.remove(tabId) : window.close();
}
});
}
/**
* 打开解析器
* @param {Object} data 资源对象
* @param {Object} options 选项
*/
function openParser(data, options = {}) {
chrome.tabs.get(G.tabId, function (tab) {
const url = `/${data.parsing ? data.parsing : "m3u8"}.html?${new URLSearchParams({
url: data.url,
title: data.title,
filename: data.downFileName,
tabid: data.tabId == -1 ? G.tabId : data.tabId,
initiator: data.initiator,
requestHeaders: data.requestHeaders ? JSON.stringify(data.requestHeaders) : undefined,
...Object.fromEntries(Object.entries(options).map(([key, value]) => [key, typeof value === 'boolean' ? 1 : value])),
})}`
chrome.tabs.create({
url: url,
index: tab.index + 1,
active: G.isMobile || !options.autoDown
});
});
}
/**
* 加载CSS样式
*/
function loadCSS() {
if (G.isMobile) {
const mobileCssLink = document.createElement('link');
mobileCssLink.rel = 'stylesheet';
mobileCssLink.type = 'text/css';
mobileCssLink.href = 'css/mobile.css';
document.head.appendChild(mobileCssLink);
}
const styleElement = document.createElement('style');
styleElement.textContent = G.css;
document.head.appendChild(styleElement);
}
/**
* 修建数据 不发送不必要的数据
* @param {Object} originalData 原始数据
* @returns {Object} 返回处理后的数据
*/
function trimData(originalData) {
const data = { ...originalData };
// 不发送HTML内容
data.html = undefined;
data.panelHeading = undefined;
data.urlPanel = undefined;
data.urlPanelShow = undefined;
return data;
}

15
js/i18n.js Normal file
View File

@@ -0,0 +1,15 @@
(function () {
document.querySelectorAll('[data-i18n]').forEach(function (element) {
element.innerHTML = i18n(element.dataset.i18n) ?? element.dataset.i18n;
});
document.querySelectorAll('[data-i18n-outer]').forEach(function (element) {
element.outerHTML = i18n(element.dataset.i18nOuter) ?? element.dataset.i18nOuter;
});
document.querySelectorAll('i18n').forEach(function (element) {
element.outerHTML = i18n(element.innerHTML) ?? element.innerHTML;
});
document.querySelectorAll('[data-i18n-placeholder]').forEach(function (element) {
element.setAttribute('placeholder', i18n(element.dataset.i18nPlaceholder) ?? element.dataset.i18nPlaceholder);
});
document.title = i18n(document.title) ?? document.title;
})();

395
js/init.js Normal file
View File

@@ -0,0 +1,395 @@
// 低版本chrome manifest v3协议 会有 getMessage 函数不存在的bug
if (chrome.i18n.getMessage === undefined) {
chrome.i18n.getMessage = (key) => key;
fetch(chrome.runtime.getURL("_locales/zh_CN/messages.json")).then(res => res.json()).then(data => {
chrome.i18n.getMessage = (key) => data[key].messages;
}).catch((e) => { console.error(e); });
}
/**
* 部分修改版chrome 不存在 chrome.downloads API
* 例如 夸克浏览器
* 使用传统下载方式下载 但无法监听 无法另存为 无法判断下载是否失败 唉~
*/
if (!chrome.downloads) {
chrome.downloads = {
download: function (options, callback) {
let a = document.createElement('a');
a.href = options.url;
a.download = options.filename;
a.click();
a = null;
callback && callback();
},
onChanged: { addListener: function () { } },
showDefaultFolder: function () { },
show: function () { },
}
}
// 兼容 114版本以下没有chrome.sidePanel
if (!chrome.sidePanel || !chrome.sidePanel.setPanelBehavior) {
chrome.sidePanel = {
setOptions: function (options) { },
setPanelBehavior: function (options) { },
}
}
// 简写翻译函数
const i18n = new Proxy(chrome.i18n.getMessage, {
get: function (target, key) {
return chrome.i18n.getMessage(key);
}
});
// 全局变量
var G = {};
G.initSyncComplete = false;
G.initLocalComplete = false;
// 缓存数据
var cacheData = { init: true };
G.blackList = new Set(); // 正则屏蔽资源列表
G.blockUrlSet = new Set(); // 屏蔽网址列表
G.requestHeaders = new Map(); // 临时储存请求头
G.urlMap = new Map(); // url查重map
G.deepSearchTemporarilyClose = null; // 深度搜索临时变量
// 初始化当前tabId
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
if (tabs[0] && tabs[0].id) {
G.tabId = tabs[0].id;
} else {
G.tabId = -1;
}
});
// 手机浏览器
G.isMobile = /Mobile|Android|iPhone|iPad/i.test(navigator.userAgent);
// 所有设置变量 默认值
G.OptionLists = {
Ext: [
{ "ext": "flv", "size": 0, "state": true },
{ "ext": "hlv", "size": 0, "state": true },
{ "ext": "f4v", "size": 0, "state": true },
{ "ext": "mp4", "size": 0, "state": true },
{ "ext": "mp3", "size": 0, "state": true },
{ "ext": "wma", "size": 0, "state": true },
{ "ext": "wav", "size": 0, "state": true },
{ "ext": "m4a", "size": 0, "state": true },
{ "ext": "ts", "size": 0, "state": false },
{ "ext": "webm", "size": 0, "state": true },
{ "ext": "ogg", "size": 0, "state": true },
{ "ext": "ogv", "size": 0, "state": true },
{ "ext": "acc", "size": 0, "state": true },
{ "ext": "mov", "size": 0, "state": true },
{ "ext": "mkv", "size": 0, "state": true },
{ "ext": "m4s", "size": 0, "state": true },
{ "ext": "m3u8", "size": 0, "state": true },
{ "ext": "m3u", "size": 0, "state": true },
{ "ext": "mpeg", "size": 0, "state": true },
{ "ext": "avi", "size": 0, "state": true },
{ "ext": "wmv", "size": 0, "state": true },
{ "ext": "asf", "size": 0, "state": true },
{ "ext": "movie", "size": 0, "state": true },
{ "ext": "divx", "size": 0, "state": true },
{ "ext": "mpeg4", "size": 0, "state": true },
{ "ext": "vid", "size": 0, "state": true },
{ "ext": "aac", "size": 0, "state": true },
{ "ext": "mpd", "size": 0, "state": true },
{ "ext": "weba", "size": 0, "state": true },
{ "ext": "opus", "size": 0, "state": true },
{ "ext": "srt", "size": 0, "state": false },
{ "ext": "vtt", "size": 0, "state": false },
],
Type: [
{ "type": "audio/*", "size": 0, "state": true },
{ "type": "video/*", "size": 0, "state": true },
{ "type": "application/ogg", "size": 0, "state": true },
{ "type": "application/vnd.apple.mpegurl", "size": 0, "state": true },
{ "type": "application/x-mpegurl", "size": 0, "state": true },
{ "type": "application/mpegurl", "size": 0, "state": true },
{ "type": "application/octet-stream-m3u8", "size": 0, "state": true },
{ "type": "application/dash+xml", "size": 0, "state": true },
{ "type": "application/m4s", "size": 0, "state": true },
],
Regex: [
{ "type": "ig", "regex": "https://cache\\.video\\.[a-z]*\\.com/dash\\?tvid=.*", "ext": "json", "state": false },
{ "type": "ig", "regex": ".*\\.bilivideo\\.(com|cn).*\\/live-bvc\\/.*m4s", "ext": "", "blackList": true, "state": false },
],
TitleName: false,
Player: "",
ShowWebIco: !G.isMobile,
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",
m3u8dl: 0,
m3u8dlArg: `"\${url}" --save-dir "%USERPROFILE%\\Downloads\\m3u8dl" --save-name "\${title}_\${now}" \${referer|exists:'-H "Referer:*"'} \${cookie|exists:'-H "Cookie:*"'} --no-log`,
m3u8dlConfirm: false,
playbackRate: 2,
copyM3U8: "${url}",
copyMPD: "${url}",
copyOther: "${url}",
autoClearMode: 1,
catDownload: false,
saveAs: false,
userAgent: "",
downFileName: "${title}.${ext}",
css: "",
checkDuplicates: true,
enable: true,
downActive: !G.isMobile, // 手机端默认不启用 后台下载
downAutoClose: true,
downStream: false,
aria2Rpc: "http://localhost:6800/jsonrpc",
enableAria2Rpc: false,
enableAria2RpcReferer: true,
aria2RpcToken: "",
m3u8AutoDown: true,
badgeNumber: true,
send2local: false,
send2localManual: false,
send2localURL: "http://127.0.0.1:8000/",
send2localMethod: 'POST',
send2localBody: '{"action": "${action}", "data": ${data}, "tabId": "${tabId}"}',
send2localType: 0,
popup: false,
popupMode: 0, // 0:preview.html 1:popup.html 2:window preview.html 3: window popup.html
invoke: false,
invokeText: `m3u8dlre:"\${url}" --save-dir "%USERPROFILE%\\Downloads" --del-after-done --save-name "\${title}_\${now}" --auto-select \${referer|exists:'-H "Referer: *"'}`,
invokeConfirm: false,
// m3u8解析器默认参数
M3u8Thread: 6,
M3u8Mp4: false,
M3u8OnlyAudio: false,
M3u8SkipDecrypt: false,
M3u8StreamSaver: false,
M3u8Ffmpeg: true,
M3u8AutoClose: false,
// 第三方服务地址
onlineServiceAddress: 0,
chromeLimitSize: 1.8 * 1024 * 1024 * 1024,
blockUrl: [],
blockUrlWhite: false,
maxLength: G.isMobile ? 999 : 9999,
sidePanel: false, // 侧边栏
deepSearch: false, // 常开深度搜索
// MQTT 配置
send2MQTT: false,
mqttEnable: false,
mqttBroker: "test.mosquitto.org",
mqttPort: 8081,
mqttPath: "/mqtt",
mqttProtocol: "wss",
mqttClientId: "cat-catch-client",
mqttUser: "",
mqttPassword: "",
mqttTopic: "cat-catch/media",
mqttQos: 0,
mqttTitleLength: 100,
mqttDataFormat: "",
getHtmlDOM: false,
};
// 本地储存的配置
G.LocalVar = {
featMobileTabId: [],
featAutoDownTabId: [],
mediaControl: { tabid: 0, index: -1 },
previewShowTitle: false, // 是否显示标题
previewDeleteDuplicateFilenames: false, // 是否删除重复文件名
};
// 102版本以上 非Firefox 开启更多功能
G.isFirefox = (typeof browser == "object");
G.version = navigator.userAgent.match(/(Chrome|Firefox)\/([\d]+)/);
G.version = G.version && G.version[2] ? parseInt(G.version[2]) : 93;
// 脚本列表
G.scriptList = new Map();
G.scriptList.set("search.js", { key: "search", refresh: true, allFrames: true, world: "MAIN", name: i18n.deepSearch, off: i18n.closeSearch, i18n: false, tabId: new Set() });
G.scriptList.set("catch.js", { key: "catch", refresh: true, allFrames: true, world: "MAIN", name: i18n.cacheCapture, off: i18n.closeCapture, i18n: true, tabId: new Set() });
G.scriptList.set("recorder.js", { key: "recorder", refresh: false, allFrames: true, world: "MAIN", name: i18n.videoRecording, off: i18n.closeRecording, i18n: true, tabId: new Set() });
G.scriptList.set("recorder2.js", { key: "recorder2", refresh: false, allFrames: false, world: "ISOLATED", name: i18n.screenCapture, off: i18n.closeCapture, i18n: true, tabId: new Set() });
G.scriptList.set("webrtc.js", { key: "webrtc", refresh: true, allFrames: true, world: "MAIN", name: i18n.recordWebRTC, off: i18n.closeRecording, i18n: true, tabId: new Set() });
// ffmpeg
G.ffmpegConfig = {
tab: 0,
cacheData: [],
version: 1,
get url() {
return G.onlineServiceAddress == 0 ? "https://ffmpeg.bmmmd.com/" : "https://ffmpeg2.bmmmd.com/";
}
}
// streamSaver 边下边存
G.streamSaverConfig = {
get url() {
return G.onlineServiceAddress == 0 ? "https://stream.bmmmd.com/mitm.html" : "https://stream2.bmmmd.com/mitm.html";
}
}
// 正则预编译
const reFilename = /filename="?([^"]+)"?/;
const reStringModify = /[<>:"\/\\|?*~]/g;
const reFilterFileName = /[<>:"|?*~]/g;
const reTemplates = /\${([^}|]+)(?:\|([^}]+))?}/g;
const reJSONparse = /([{,]\s*)([\w-]+)(\s*:)/g;
// 防抖
let debounce = undefined;
let debounceCount = 0;
let debounceTime = 0;
// Init
InitOptions();
// 初始变量
function InitOptions() {
// 断开重新连接后 立刻把local里MediaData数据交给cacheData
(chrome.storage.session ?? chrome.storage.local).get({ MediaData: {} }, function (items) {
if (items.MediaData.init) {
cacheData = {};
return;
}
cacheData = items.MediaData;
});
// 读取sync配置数据 交给全局变量G
chrome.storage.sync.get(G.OptionLists, function (items) {
if (chrome.runtime.lastError) {
items = G.OptionLists;
}
// 确保有默认值
for (let key in G.OptionLists) {
if (items[key] === undefined || items[key] === null) {
items[key] = G.OptionLists[key];
}
}
// Ext的Array转为Map类型
items.Ext = new Map(items.Ext.map(item => [item.ext, item]));
// Type的Array转为Map类型
items.Type = new Map(items.Type.map(item => [item.type, { size: item.size, state: item.state }]));
// 预编译正则匹配
items.Regex = items.Regex.map(item => {
let reg = undefined;
try { reg = new RegExp(item.regex, item.type) } catch (e) { item.state = false; }
return { regex: reg, ext: item.ext, blackList: item.blackList, state: item.state }
});
// 预编译屏蔽通配符
items.blockUrl = items.blockUrl.map(item => {
return { url: wildcardToRegex(item.url), state: item.state }
});
// 兼容旧配置
if (items.copyM3U8.includes('$url$')) {
items.copyM3U8 = items.copyM3U8.replaceAll('$url$', '${url}').replaceAll('$referer$', '${referer}').replaceAll('$title$', '${title}');
chrome.storage.sync.set({ copyM3U8: items.copyM3U8 });
}
if (items.copyMPD.includes('$url$')) {
items.copyMPD = items.copyMPD.replaceAll('$url$', '${url}').replaceAll('$referer$', '${referer}').replaceAll('$title$', '${title}');
chrome.storage.sync.set({ copyMPD: items.copyMPD });
}
if (items.copyOther.includes('$url$')) {
items.copyOther = items.copyOther.replaceAll('$url$', '${url}').replaceAll('$referer$', '${referer}').replaceAll('$title$', '${title}');
chrome.storage.sync.set({ copyOther: items.copyOther });
}
if (typeof items.m3u8dl == 'boolean') {
items.m3u8dl = items.m3u8dl ? 1 : 0;
chrome.storage.sync.set({ m3u8dl: items.m3u8dl });
}
// 侧边栏
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: items.sidePanel });
G = { ...items, ...G };
// 初始化 G.blockUrlSet
(typeof isLockUrl == 'function') && chrome.tabs.query({}, function (tabs) {
for (const tab of tabs) {
if (tab.url && isLockUrl(tab.url)) {
G.blockUrlSet.add(tab.id);
}
}
});
chrome.action.setIcon({ path: G.enable ? "/img/icon.png" : "/img/icon-disable.png" });
G.initSyncComplete = true;
});
// 读取local配置数据 交给全局变量G
(chrome.storage.session ?? chrome.storage.local).get(G.LocalVar, function (items) {
items.featMobileTabId = new Set(items.featMobileTabId);
items.featAutoDownTabId = new Set(items.featAutoDownTabId);
G = { ...items, ...G };
G.initLocalComplete = true;
});
}
// 监听变化,新值给全局变量
chrome.storage.onChanged.addListener(function (changes, namespace) {
if (changes.MediaData) {
if (changes.MediaData.newValue?.init) { cacheData = {}; }
return;
}
for (let [key, { oldValue, newValue }] of Object.entries(changes)) {
newValue ??= G.OptionLists[key];
if (key == "Ext") {
G.Ext = new Map(newValue.map(item => [item.ext, item]));
continue;
}
if (key == "Type") {
G.Type = new Map(newValue.map(item => [item.type, { size: item.size, state: item.state }]));
continue;
}
if (key == "Regex") {
G.Regex = newValue.map(item => {
let reg = undefined;
try { reg = new RegExp(item.regex, item.type) } catch (e) { item.state = false; }
return { regex: reg, ext: item.ext, blackList: item.blackList, state: item.state }
});
continue;
}
if (key == "blockUrl") {
G.blockUrl = newValue.map(item => {
return { url: wildcardToRegex(item.url), state: item.state }
});
continue;
}
if (key == "featMobileTabId" || key == "featAutoDownTabId") {
G[key] = new Set(newValue);
continue;
}
if (key == "sidePanel" && !G.isFirefox) {
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: newValue });
continue;
}
G[key] = newValue;
}
});
// 扩展升级,清空本地储存
chrome.runtime.onInstalled.addListener(function (details) {
if (details.reason == "update") {
chrome.storage.local.clear(function () {
if (chrome.storage.session) {
chrome.storage.session.clear(InitOptions);
} else {
InitOptions();
}
});
chrome.alarms.create("nowClear", { when: Date.now() + 3000 });
}
if (details.reason == "install") {
chrome.tabs.create({ url: "install.html" });
}
});
/**
* 将用户输入的URL可能包含通配符转换为正则表达式
* @param {string} urlPattern - 用户输入的URL可能包含通配符
* @returns {RegExp} - 转换后的正则表达式
*/
function wildcardToRegex(urlPattern) {
// 将通配符 * 转换为正则表达式的 .*
// 将通配符 ? 转换为正则表达式的 .
// 同时转义其他正则表达式特殊字符
const regexPattern = urlPattern
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // 转义正则表达式特殊字符
.replace(/\*/g, '.*') // 将 * 替换为 .*
.replace(/\?/g, '.'); // 将 ? 替换为 .
// 创建正则表达式确保匹配整个URL
return new RegExp(`^${regexPattern}$`, 'i'); // 忽略大小写
}

64
js/install.js Normal file
View File

@@ -0,0 +1,64 @@
window.addEventListener('DOMContentLoaded', function () {
// 多语言支持
let currentLang = 'zh';
function setLanguage(lang) {
currentLang = lang;
// 设置活动语言
document.querySelectorAll('.lang-zh').forEach(el => {
el.classList.toggle('active', lang === 'zh');
});
document.querySelectorAll('.lang-en').forEach(el => {
el.classList.toggle('active', lang === 'en');
});
// 更新语言切换按钮
document.getElementById('langText').textContent = lang === 'zh' ? 'English' : '中文';
document.getElementById('langEmoji').textContent = lang === 'zh' ? '🌐' : '🇨🇳';
// 更新动态文本
if (lang === 'en') {
document.getElementById('main-title').textContent = 'Installation Successful!';
document.getElementById('subtitle').textContent = 'Cat Catch Extension is now installed';
document.getElementById('welcome-title').textContent = 'Welcome to Cat Catch';
document.getElementById('privacy-title').textContent = 'Privacy Policy';
document.getElementById('disclaimer-title').textContent = 'Disclaimer';
document.getElementById('issue-title').textContent = 'Issue Submission';
document.getElementById('agreeText').textContent = 'Agree';
document.getElementById('uninstallText').textContent = 'Uninstall Extension';
} else {
document.getElementById('main-title').textContent = '恭喜 猫抓 扩展已成功安装 !';
document.getElementById('subtitle').textContent = 'Installation successful !';
document.getElementById('welcome-title').textContent = '希望本扩展能帮助到你';
document.getElementById('privacy-title').textContent = '隐私政策 / Privacy Policy';
document.getElementById('disclaimer-title').textContent = '免责声明 / Disclaimer';
document.getElementById('issue-title').textContent = '问题提交 / Issue Submission';
document.getElementById('agreeText').textContent = '同意';
document.getElementById('uninstallText').textContent = '卸载扩展';
}
}
// 语言切换功能
document.getElementById('langToggle').addEventListener('click', function () {
const newLang = currentLang === 'zh' ? 'en' : 'zh';
setLanguage(newLang);
});
// 按钮事件处理
document.getElementById('agreeBtn').addEventListener('click', function () {
closeTab();
});
document.getElementById('uninstallBtn').addEventListener('click', function () {
chrome.management.uninstallSelf({ showConfirmDialog: true });
});
const lang = navigator.language || navigator.userLanguage;
const isChinese = lang.startsWith('zh');
setLanguage(isChinese ? 'zh' : 'en');
// 添加动画效果
document.querySelector('.card').classList.add('fade-in');
document.getElementById('agreeBtn').focus();
});

85
js/json.js Normal file
View File

@@ -0,0 +1,85 @@
// url 参数解析
const params = new URL(location.href).searchParams;
var _url = params.get("url");
// const _referer = params.get("referer");
const _requestHeaders = params.get("requestHeaders");
// 修改当前标签下的所有xhr的requestHeaders
let requestHeaders = JSONparse(_requestHeaders);
setRequestHeaders(requestHeaders, () => { awaitG(init); });
function init() {
loadCSS();
var jsonContent = "";
var options = {
collapsed: true,
rootCollapsable: false,
withQuotes: false,
withLinks: true
};
if (isEmpty(_url)) {
$("#jsonCustom").show(); $("#main").hide();
$("#format").click(function () {
_url = $("#jsonUrl").val().trim();
if (isEmpty(_url)) {
let jsonText = $("#jsonText").val();
jsonContent = JSON.parse(jsonText);
renderJson();
$("#jsonCustom").hide(); $("#main").show();
return;
}
getJson(_url);
});
} else {
getJson(_url);
}
function getJson(url) {
$("#jsonCustom").hide(); $("#main").show();
$.ajax({
url: url,
dataType: "text",
}).fail(function (result) {
console.log(result);
$('#json-renderer').html(i18n.fileRetrievalFailed);
$("#collapsed").hide();
}).done(function (result) {
// console.log(result);
result = result.replace(/^try{/, "").replace(/}catch\(e\){.*}$/ig, ""); //去除try{}catch(e){}
try {
jsonContent = JSON.parse(result);
} catch (e) {
console.log(e);
let regexp = [
/^.*=({.*}).*$/,
/^.*\(({.*})\).*$/
]
for (let regex of regexp) {
let res = new RegExp(regex, "ig").exec(result);
if (res) {
// console.log(res);
result = res[1];
break;
}
}
// console.log(result);
jsonContent = JSON.parse(result);
}
renderJson();
});
}
function renderJson() {
$('#json-renderer').jsonViewer(jsonContent, options);
}
$("#collapsed").click(function () {
options.collapsed = !options.collapsed;
if (options.collapsed) {
collapsed.innerHTML = i18n.expandAllNodes;
} else {
collapsed.innerHTML = i18n.collapseAllNodes;
}
renderJson();
});
}

333
js/m3u8.downloader.js Normal file
View File

@@ -0,0 +1,333 @@
class Downloader {
constructor(fragments = [], thread = 6) {
this.fragments = fragments; // 切片列表
this.allFragments = fragments; // 储存所有原始切片列表
this.thread = thread; // 线程数
this.events = {}; // events
this.decrypt = null; // 解密函数
this.transcode = null; // 转码函数
this.init();
}
/**
* 初始化所有变量
*/
init() {
this.index = 0; // 当前任务索引
this.buffer = []; // 储存的buffer
this.state = 'waiting'; // 下载器状态 waiting running done abort
this.success = 0; // 成功下载数量
this.errorList = new Set(); // 下载错误的列表
this.buffersize = 0; // 已下载buffer大小
this.duration = 0; // 已下载时长
this.pushIndex = 0; // 推送顺序下载索引
this.controller = []; // 储存中断控制器
this.running = 0; // 正在下载数量
}
/**
* 设置监听
* @param {string} eventName 监听名
* @param {Function} callBack
*/
on(eventName, callBack) {
if (this.events[eventName]) {
this.events[eventName].push(callBack);
} else {
this.events[eventName] = [callBack];
}
}
/**
* 触发监听器
* @param {string} eventName 监听名
* @param {...any} args
*/
emit(eventName, ...args) {
if (this.events[eventName]) {
this.events[eventName].forEach(callBack => {
callBack(...args);
});
}
}
/**
* 设定解密函数
* @param {Function} callback
*/
setDecrypt(callback) {
this.decrypt = callback;
}
/**
* 设定转码函数
* @param {Function} callback
*/
setTranscode(callback) {
this.transcode = callback;
}
/**
* 停止下载 没有目标 停止所有线程
* @param {number} index 停止下载目标
*/
stop(index = undefined) {
if (index !== undefined) {
this.controller[index] && this.controller[index].abort();
return;
}
this.controller.forEach(controller => { controller.abort() });
this.state = 'abort';
}
/**
* 检查对象是否错误列表内
* @param {object} fragment 切片对象
* @returns {boolean}
*/
isErrorItem(fragment) {
return this.errorList.has(fragment);
}
/**
* 返回所有错误列表
*/
get errorItem() {
return this.errorList;
}
/**
* 按照顺序推送buffer数据
*/
sequentialPush() {
if (!this.events["sequentialPush"]) { return; }
for (; this.pushIndex < this.fragments.length; this.pushIndex++) {
if (this.buffer[this.pushIndex]) {
this.emit('sequentialPush', this.buffer[this.pushIndex]);
delete this.buffer[this.pushIndex];
continue;
}
break;
}
}
/**
* 限定下载范围
* @param {number} start 下载范围 开始索引
* @param {number} end 下载范围 结束索引
* @returns {boolean}
*/
range(start = 0, end = this.fragments.length) {
if (start > end) {
this.emit('error', 'start > end');
return false;
}
if (end > this.fragments.length) {
this.emit('error', 'end > total');
return false;
}
if (start >= this.fragments.length) {
this.emit('error', 'start >= total');
return false;
}
if (start != 0 || end != this.fragments.length) {
this.fragments = this.fragments.slice(start, end);
// 更改过下载范围 重新设定index
this.fragments.forEach((fragment, index) => {
fragment.index = index;
});
}
// 总数为空 抛出错误
if (this.fragments.length == 0) {
this.emit('error', 'List is empty');
return false;
}
return true;
}
/**
* 获取切片总数量
* @returns {number}
*/
get total() {
return this.fragments.length;
}
/**
* 获取切片总时间
* @returns {number}
*/
get totalDuration() {
return this.fragments.reduce((total, fragment) => total + fragment.duration, 0);
}
/**
* 切片对象数组的 setter getter
*/
set fragments(fragments) {
// 增加index参数 为多线程异步下载 根据index属性顺序保存
this._fragments = fragments.map((fragment, index) => ({ ...fragment, index }));
}
get fragments() {
return this._fragments;
}
/**
* 获取 #EXT-X-MAP 标签的文件url
* @returns {string}
*/
get mapTag() {
if (this.fragments[0].initSegment && this.fragments[0].initSegment.url) {
return this.fragments[0].initSegment.url;
}
return "";
}
/**
* 添加一条新资源
* @param {Object} fragment
*/
push(fragment) {
fragment.index = this.fragments.length;
this.fragments.push(fragment);
}
/**
* 下载器 使用fetch下载文件
* @param {object} fragment 重新下载的对象
*/
downloader(fragment = null) {
if (this.state === 'abort') { return; }
// 是否直接下载对象
const directDownload = !!fragment;
// 非直接下载对象 从this.fragments获取下一条资源 若不存在跳出
if (!directDownload && !this.fragments[this.index]) { return; }
// fragment是数字 直接从this.fragments获取
if (typeof fragment === 'number') {
fragment = this.fragments[fragment];
}
// 不存在下载对象 从提取fragments
fragment ??= this.fragments[this.index++];
this.state = 'running';
this.running++;
// 资源已下载 跳过
// if (this.buffer[fragment.index]) { return; }
// 停止下载控制器
const controller = new AbortController();
this.controller[fragment.index] = controller;
const options = { signal: controller.signal };
// 下载前触发事件
this.emit('start', fragment, options);
// 开始下载
fetch(fragment.url, options)
.then(response => {
if (!response.ok) {
throw new Error(response.status, { cause: 'HTTPError' });
}
const reader = response.body.getReader();
const contentLength = parseInt(response.headers.get('content-length')) || 0;
fragment.contentType = response.headers.get('content-type') ?? 'null';
let receivedLength = 0;
const chunks = [];
const pump = async () => {
while (true) {
const { value, done } = await reader.read();
if (done) { break; }
// 流式下载
fragment.fileStream ? fragment.fileStream.write(new Uint8Array(value)) : chunks.push(value);
receivedLength += value.length;
this.emit('itemProgress', fragment, false, receivedLength, contentLength, value);
}
if (fragment.fileStream) {
return new ArrayBuffer();
}
const allChunks = new Uint8Array(receivedLength);
let position = 0;
for (const chunk of chunks) {
allChunks.set(chunk, position);
position += chunk.length;
}
this.emit('itemProgress', fragment, true);
return allChunks.buffer;
}
return pump();
})
.then(buffer => {
this.emit('rawBuffer', buffer, fragment);
// 存在解密函数 调用解密函数 否则直接返回buffer
return this.decrypt ? this.decrypt(buffer, fragment) : buffer;
})
.then(buffer => {
this.emit('decryptedData', buffer, fragment);
// 存在转码函数 调用转码函数 否则直接返回buffer
return this.transcode ? this.transcode(buffer, fragment) : buffer;
})
.then(buffer => {
// 储存解密/转码后的buffer
this.buffer[fragment.index] = buffer;
// 成功数+1 累计buffer大小和视频时长
this.success++;
this.buffersize += buffer.byteLength;
this.duration += fragment.duration ?? 0;
// 下载对象来自错误列表 从错误列表内删除
this.errorList.has(fragment) && this.errorList.delete(fragment);
// 推送顺序下载
this.sequentialPush();
this.emit('completed', buffer, fragment);
// 下载完成
if (this.success == this.fragments.length) {
this.state = 'done';
this.emit('allCompleted', this.buffer, this.fragments);
}
}).catch((error) => {
console.log(error);
if (error.name == 'AbortError') {
this.emit('stop', fragment, error);
return;
}
this.emit('downloadError', fragment, error);
// 储存下载错误切片
!this.errorList.has(fragment) && this.errorList.add(fragment);
}).finally(() => {
this.running--;
// 下载下一个切片
if (!directDownload && this.index < this.fragments.length) {
this.downloader();
}
});
}
/**
* 开始下载 准备数据 调用下载器
* @param {number} start 下载范围 开始索引
* @param {number} end 下载范围 结束索引
*/
start(start = 0, end = this.fragments.length) {
// 检查下载器状态
if (this.state == 'running') {
this.emit('error', 'state running');
return;
}
// 从下载范围内 切出需要下载的部分
if (!this.range(start, end)) {
return;
}
// 初始化变量
this.init();
// 开始下载 多少线程开启多少个下载器
for (let i = 0; i < this.thread && i < this.fragments.length; i++) {
this.downloader();
}
}
/**
* 销毁 初始化所有变量
*/
destroy() {
this.stop();
this._fragments = [];
this.allFragments = [];
this.thread = 6;
this.events = {};
this.decrypt = null;
this.transcode = null;
this.init();
}
}

1941
js/m3u8.js Normal file

File diff suppressed because it is too large Load Diff

204
js/media-control.js Normal file
View File

@@ -0,0 +1,204 @@
(function () {
let _tabId = -1; // 选择的页面ID
let _index = -1; //选择的视频索引
let VideoTagTimer; // 获取所有视频标签的定时器
let VideoStateTimer; // 获取所有视频信息的定时器
let compareTab = [];
let compareVideo = [];
function setVideoTagTimer() {
clearInterval(VideoTagTimer);
VideoTagTimer = setInterval(getVideoTag, 1000);
}
function getVideoTag() {
chrome.tabs.query({ windowType: "normal" }, function (tabs) {
let videoTabList = [];
for (let tab of tabs) {
videoTabList.push(tab.id);
}
if (compareTab.toString() == videoTabList.toString()) {
return;
}
compareTab = videoTabList;
// 列出所有标签
for (let tab of tabs) {
if ($("#option" + tab.id).length == 1) { continue; }
$("#videoTabIndex").append(`<option value='${tab.id}' id="option${tab.id}">${stringModify(tab.title)}</option>`);
}
// 删除没有媒体的标签. 异步的原因使用一个for去处理无法保证标签顺序一致
for (let tab of videoTabList) {
chrome.tabs.sendMessage(tab, { Message: "getVideoState", index: 0 }, { frameId: 0 }, function (state) {
if (chrome.runtime.lastError || state.count == 0) {
$("#option" + tab).remove();
return;
}
$("#videoTabTips").remove();
if (tab == G.tabId && _tabId == -1) {
_tabId = tab;
$("#videoTabIndex").val(tab);
}
});
}
});
}
function setVideoStateTimer() {
clearInterval(VideoStateTimer);
VideoStateTimer = setInterval(getVideoState, 500);
}
function getVideoState(setSpeed = false) {
if (_tabId == -1) {
let currentTabId = $("#videoTabIndex").val();
if (currentTabId == -1) { return; }
_tabId = parseInt(currentTabId);
}
chrome.tabs.sendMessage(_tabId, { Message: "getVideoState", index: _index }, { frameId: 0 }, function (state) {
if (chrome.runtime.lastError || state.count == 0) { return; }
if (state.type == "audio") {
$("#pip").hide();
$("#screenshot").hide();
}
$("#volume").val(state.volume);
if (state.duration && state.duration != Infinity) {
$("#timeShow").html(secToTime(state.currentTime) + " / " + secToTime(state.duration));
$("#time").val(state.time);
}
state.paused ? $("#control").html(i18n.play).data("switch", "play") : $("#control").html(i18n.pause).data("switch", "pause");
state.speed == 1 ? $("#speed").html(i18n.speedPlayback).data("switch", "speed") : $("#speed").html(i18n.normalPlay).data("switch", "normal");
$("#loop").prop("checked", state.loop);
$("#muted").prop("checked", state.muted);
if (setSpeed && state.speed != 1) {
$("#playbackRate").val(state.speed);
}
if (compareVideo.toString() != state.src.toString()) {
compareVideo = state.src;
$("#videoIndex").empty();
for (let i = 0; i < state.count; i++) {
let src = state.src[i].split("/").pop();
if (src.length >= 60) {
src = src.substr(0, 35) + '...' + src.substr(-35);
}
$("#videoIndex").append(`<option value='${i}'>${src}</option>`);
}
}
_index = _index == -1 ? 0 : _index;
$("#videoIndex").val(_index);
});
}
// 点击其他设置标签页 开始读取tab信息以及视频信息
getVideoTag();
$("#otherTab").click(function () {
chrome.tabs.get(G.mediaControl.tabid, function (tab) {
if (chrome.runtime.lastError) {
_tabId = -1;
_index = -1;
setVideoTagTimer(); getVideoState(); setVideoStateTimer();
return;
}
chrome.tabs.sendMessage(G.mediaControl.tabid, { Message: "getVideoState", index: 0 }, function (state) {
_tabId = G.mediaControl.tabid;
if (state.count > G.mediaControl.index) {
_index = G.mediaControl.index;
}
$("#videoTabIndex").val(_tabId);
setVideoTagTimer(); getVideoState(true); setVideoStateTimer();
(chrome.storage.session ?? chrome.storage.local).set({ mediaControl: { tabid: _tabId, index: _index } });
});
});
// setVideoTagTimer(); getVideoState(); setVideoStateTimer();
});
// 切换标签选择 切换视频选择
$("#videoIndex, #videoTabIndex").change(function () {
if (!G.isFirefox) { $("#pip").show(); }
$("#screenshot").show();
if (this.id == "videoTabIndex") {
_tabId = parseInt($("#videoTabIndex").val());
} else {
_index = parseInt($("#videoIndex").val());
}
(chrome.storage.session ?? chrome.storage.local).set({ mediaControl: { tabid: _tabId, index: _index } });
getVideoState(true);
});
let wheelPlaybackRateTimeout;
$("#playbackRate").on("wheel", function (event) {
$(this).blur();
let speed = parseFloat($(this).val());
speed = event.originalEvent.wheelDelta < 0 ? speed - 0.1 : speed + 0.1;
speed = parseFloat(speed.toFixed(1));
if (speed < 0.1 || speed > 16) { return false; }
$(this).val(speed);
clearTimeout(wheelPlaybackRateTimeout);
wheelPlaybackRateTimeout = setTimeout(() => {
chrome.storage.sync.set({ playbackRate: speed });
chrome.tabs.sendMessage(_tabId, { Message: "speed", speed: speed, index: _index });
}, 200);
return false;
});
// 倍速播放
$("#speed").click(function () {
if (_index < 0 || _tabId < 0) { return; }
if ($(this).data("switch") == "speed") {
const speed = parseFloat($("#playbackRate").val());
chrome.tabs.sendMessage(_tabId, { Message: "speed", speed: speed, index: _index });
chrome.storage.sync.set({ playbackRate: speed });
return;
}
chrome.tabs.sendMessage(_tabId, { Message: "speed", speed: 1, index: _index });
});
// 画中画
$("#pip").click(function () {
if (_index < 0 || _tabId < 0) { return; }
chrome.tabs.sendMessage(_tabId, { Message: "pip", index: _index }, function (state) {
if (chrome.runtime.lastError) { return; }
state.state ? $("#pip").html(i18n.exit) : $("#pip").html(i18n.pictureInPicture);
});
});
// 全屏
$("#fullScreen").click(function () {
if (_index < 0 || _tabId < 0) { return; }
chrome.tabs.get(_tabId, function (tab) {
chrome.tabs.highlight({ 'tabs': tab.index }, function () {
chrome.tabs.sendMessage(_tabId, { Message: "fullScreen", index: _index }, function (state) {
close();
});
});
});
});
// 暂停 播放
$("#control").click(function () {
if (_index < 0 || _tabId < 0) { return; }
const action = $(this).data("switch");
chrome.tabs.sendMessage(_tabId, { Message: action, index: _index });
});
// 循环 静音
$("#loop, #muted").click(function () {
if (_index < 0 || _tabId < 0) { return; }
const action = $(this).prop("checked");
chrome.tabs.sendMessage(_tabId, { Message: this.id, action: action, index: _index });
});
// 调节音量和视频进度时 停止循环任务
$("#volume, #time").mousedown(function () {
if (_index < 0 || _tabId < 0) { return; }
clearInterval(VideoStateTimer);
});
// 调节音量
$("#volume").mouseup(function () {
if (_index < 0 || _tabId < 0) { return; }
chrome.tabs.sendMessage(_tabId, { Message: "setVolume", volume: $(this).val(), index: _index }, function () {
if (chrome.runtime.lastError) { return; }
setVideoStateTimer();
});
});
// 调节视频进度
$("#time").mouseup(function () {
if (_index < 0 || _tabId < 0) { return; }
chrome.tabs.sendMessage(_tabId, { Message: "setTime", time: $(this).val(), index: _index }, function () {
if (chrome.runtime.lastError) { return; }
setVideoStateTimer();
});
});
// 视频截图
$("#screenshot").click(function () {
if (_index < 0 || _tabId < 0) { return; }
chrome.tabs.sendMessage(_tabId, { Message: "screenshot", index: _index });
});
})();

187
js/mpd.js Normal file
View File

@@ -0,0 +1,187 @@
// url 参数解析
const params = new URL(location.href).searchParams;
const _url = params.get("url");
// const _referer = params.get("referer");
const _requestHeaders = params.get("requestHeaders");
const _title = params.get("title");
// 修改当前标签下的所有xhr的requestHeaders
let requestHeaders = JSONparse(_requestHeaders);
setRequestHeaders(requestHeaders, () => { awaitG(init); });
var mpdJson = {}; // 解析器json结果
var mpdXml = {}; // 解析器xml结果
// var mpdContent; // mpd文件内容
var m3u8Content = ""; //m3u8内容
var mediaInfo = "" // 媒体文件信息
chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
if (message == "getM3u8") {
sendResponse({ m3u8Content, mediaInfo });
}
});
function init() {
loadCSS();
if (_url) {
fetch(_url)
.then(response => response.text())
.then(function (text) {
// mpdContent = text;
// parseMPD(mpdContent);
parseMPD(text);
$("#mpd_url").html(_url).attr("href", _url);
});
} else {
$("#loading").hide();
$("#mpdCustom").show();
$("#parse").click(function () {
let url = $("#mpdUrl").val().trim();;
url = "mpd.html?url=" + encodeURIComponent(url);
let referer = $("#referer").val().trim();;
if (referer) { url += "&requestHeaders=" + JSON.stringify({ referer: referer }); }
chrome.tabs.update({ url: url });
});
}
$("#mpdVideoLists, #mpdAudioLists").change(function () {
let type = this.id == "mpdVideoLists" ? "video" : "audio";
showSegment(type, $(this).val());
});
$("#getVideo, #getAudio").click(function () {
let type = "video";
let index = $("#mpdVideoLists").val();
if (this.id == "getAudio") {
type = "audio";
index = $("#mpdAudioLists").val();
}
showSegment(type, index);
});
$("#videoToM3u8, #audioToM3u8").click(function () {
let index = $("#mpdVideoLists").val();
let items = mpdJson.playlists[index];
let type = "video";
if (this.id == "audioToM3u8") {
index = $("#mpdAudioLists").val();
let temp = index.split("$-bmmmd-$");
index = temp[0];
let index2 = temp[1];
items = mpdJson.mediaGroups.AUDIO.audio[index].playlists[index2];
type = "audio";
}
mediaInfo = getInfo(type);
m3u8Content = "#EXTM3U\n";
m3u8Content += "#EXT-X-VERSION:3\n";
m3u8Content += "#EXT-X-TARGETDURATION:" + items.targetDuration + "\n";
m3u8Content += "#EXT-X-MEDIA-SEQUENCE:0\n";
m3u8Content += "#EXT-X-PLAYLIST-TYPE:VOD\n";
m3u8Content += '#EXT-X-MAP:URI="' + items.segments[0].map.resolvedUri + '"\n';
for (let key in items.segments) {
m3u8Content += "#EXTINF:" + items.segments[key].duration + ",\n"
m3u8Content += items.segments[key].resolvedUri + "\n";
}
m3u8Content += "#EXT-X-ENDLIST";
// $("#media_file").html(m3u8Content); return;
chrome.tabs.getCurrent(function (tabs) {
chrome.tabs.create({ url: "m3u8.html?getId=" + tabs.id });
});
});
}
// 加密类型
function getEncryptionType(schemeIdUri) {
if (schemeIdUri.includes("edef8ba9-79d6-4ace-a3c8-27dcd51d21ed")) {
return "Widevine";
} else if (schemeIdUri.includes("9a04f079-9840-4286-ab92-e65be0885f95")) {
return "Microsoft PlayReady";
} else if (schemeIdUri.includes("94ce86fb-07ff-4f43-adb8-93d2fa968ca2")) {
return "Apple FairPlay";
} else {
return "Unknown";
}
}
// 判断DRM
function isDRM(mpdContent) {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(mpdContent, "application/xml");
let drmInfo = new Map();
const contentProtections = xmlDoc.getElementsByTagName("ContentProtection");
for (let i = 0; i < contentProtections.length; i++) {
const protection = contentProtections[i];
const schemeIdUri = protection.getAttribute("schemeIdUri");
let pssh = protection.getElementsByTagName("cenc:pssh")[0];
if (!pssh) {
pssh = protection.getElementsByTagName("mspr:pro")[0];
}
if (schemeIdUri && pssh) {
if (!drmInfo.has(schemeIdUri)) {
drmInfo.set(schemeIdUri, pssh.textContent);
}
}
}
return Array.from(drmInfo.entries()).map(([schemeIdUri, pssh]) => ({
schemeIdUri,
pssh,
encryptionType: getEncryptionType(schemeIdUri)
}));
}
function parseMPD(mpdContent) {
$("#loading").hide(); $("#main").show();
mpdJson = mpdParser.parse(mpdContent, { manifestUri: _url });
const drmInfo = isDRM(mpdContent);
if (drmInfo.length > 0) {
$("#loading").show();
$("#loading .optionBox").html(`<b>${i18n.DRMerror}</b><br><br>`);
drmInfo.map(item => {
$("#loading .optionBox").append(`<b>${item.encryptionType}</b><input value="${item.pssh}" style="width: 100%;"/>`);
});
}
for (let key in mpdJson.playlists) {
$("#mpdVideoLists").append(`<option value='${key}'>${mpdJson.playlists[key].attributes.NAME
} | ${(mpdJson.playlists[key].attributes.BANDWIDTH / 1024).toFixed(1)
} kbps | ${mpdJson.playlists[key].attributes["FRAME-RATE"].toFixed(1)
} fps | ${mpdJson.playlists[key].attributes.RESOLUTION.width
} x ${mpdJson.playlists[key].attributes.RESOLUTION.height
}</option>`);
}
for (let key in mpdJson.mediaGroups.AUDIO.audio) {
for (let index in mpdJson.mediaGroups.AUDIO.audio[key].playlists) {
let item = mpdJson.mediaGroups.AUDIO.audio[key].playlists[index];
// console.log(item);
$("#mpdAudioLists").append(`<option value='${key}$-bmmmd-$${index}'>${key} | ${item.attributes.NAME} | ${item.attributes.BANDWIDTH / 1000}Kbps</option>`);
}
}
$("#info").html(getInfo("video"));
showSegment("video", 0);
}
function showSegment(type, index) {
let textarea = "";
let items;
if (type == "video") {
items = mpdJson.playlists[index];
} else {
let temp = index.split("$-bmmmd-$");
index = temp[0];
let index2 = temp[1];
items = mpdJson.mediaGroups.AUDIO.audio[index].playlists[index2];
}
for (let key in items.segments) {
textarea += items.segments[key].resolvedUri + "\n\n";
}
$("#media_file").html(textarea);
$("#count").html(i18n("m3u8Info", [items.segments.length, secToTime(mpdJson.duration)]));
items.segments.length > 0 && $("#tips").html('initialization: <input type="text" value="' + items.segments[0].map.resolvedUri + '" spellcheck="false" readonly="readonly" class="width100">');
$("#info").html(getInfo(type));
}
function getInfo(type = "audio") {
if (type == "audio") {
return i18n.audio + ": " + $("#mpdAudioLists").find("option:selected").text();
} else {
return i18n.video + ": " + $("#mpdVideoLists").find("option:selected").text();
}
}

432
js/options.js Normal file
View File

@@ -0,0 +1,432 @@
////////////////////// 填充数据 //////////////////////
chrome.storage.sync.get(G.OptionLists, function (items) {
if (chrome.runtime.lastError) {
items = G.OptionLists;
}
// 确保有默认值
for (let key in G.OptionLists) {
if (items[key] === undefined || items[key] === null) {
items[key] = G.OptionLists[key];
}
}
if (items.Ext === undefined || items.Type === undefined || items.Regex === undefined) {
location.reload();
}
if (G.isMobile) {
$(`<link rel="stylesheet" type="text/css" href="css/mobile.css">`).appendTo("head");
}
$(`<style>${items.css}</style>`).appendTo("head");
const $extList = $("#extList");
for (let key in items.Ext) {
$extList.append(Gethtml("Ext", { ext: items.Ext[key].ext, size: items.Ext[key].size, state: items.Ext[key].state }));
}
const $typeList = $("#typeList");
for (let key in items.Type) {
$typeList.append(Gethtml("Type", { type: items.Type[key].type, size: items.Type[key].size, state: items.Type[key].state }));
}
const $regexList = $("#regexList");
for (let key in items.Regex) {
$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 }));
}
const $blockUrlList = $("#blockUrlList");
for (let key in items.blockUrl) {
$blockUrlList.append(Gethtml("blockUrl", { url: items.blockUrl[key].url, state: items.blockUrl[key].state }));
}
setTimeout(() => {
for (let key in items) {
if (key == "Ext" || key == "Type" || key == "Regex") { continue; }
if (typeof items[key] == "boolean") {
$(`#${key}`).prop("checked", items[key]);
} else {
$(`#${key}`).val(items[key]);
}
}
}, 100);
});
//新增格式
$("#AddExt").bind("click", function () {
$("#extList").append(Gethtml("Ext", { state: true }));
$("#extList [name=text]").last().focus();
});
$("#AddType").bind("click", function () {
$("#typeList").append(Gethtml("Type", { state: true }));
$("#typeList [name=text]").last().focus();
});
$("#AddRegex").bind("click", function () {
$("#regexList").append(Gethtml("Regex", { type: "ig", state: true }));
$("#regexList [name=text]").last().focus();
});
$("#blockAddUrl").bind("click", function () {
$("#blockUrlList").append(Gethtml("blockUrl", { state: true }));
$("#blockUrlList [name=url]").last().focus();
});
$("#version").html(i18n.catCatch + " v" + chrome.runtime.getManifest().version);
// 自定义播放调用模板
playerList = new Map();
playerList.set("tips", { name: i18n.invokeProtocolTemplate, template: "" });
playerList.set("default", { name: i18n.default + " / " + i18n.disable, template: "" });
playerList.set("potplayer", { name: "PotPlayer", template: "potplayer://${url} ${referer|exists:'/referer=\"*\"'}" });
playerList.set("potplayerFix", { name: "PotPlayerFix", template: "potplayer:${url} ${referer|exists:'/referer=\"*\"'}" });
playerList.set("mxPlayerAd", { name: "Android MX Player Free", template: "intent:${url}#Intent;package=com.mxtech.videoplayer.ad;end" });
playerList.set("mxPlayerPro", { name: "Android MX Player Pro", template: "intent:${url}#Intent;package=com.mxtech.videoplayer.pro;end" });
playerList.set("vlc", { name: "Android vlc", template: "intent:${url}#Intent;package=org.videolan.vlc;end" });
playerList.set("vlcCustom", { name: i18n.customVLCProtocol + " vlc://", template: "vlc://${url}" });
playerList.set("shareApi", { name: i18n.systemShare, template: "${shareApi}" });
playerList.forEach(function (item, key) {
$("#PlayerTemplate").append(`<option value="${key}">${item.name}</option>`);
});
// 增加后缀 类型 正则表达式
function Gethtml(Type, Param = new Object()) {
let html = "";
switch (Type) {
case "Ext":
html = `<td><input type="text" value="${Param.ext ? Param.ext : ""}" name="text" placeholder="${i18n.suffix}" class="ext"></td>`
html += `<td><input type="number" value="${Param.size ? Param.size : 0}" class="size" name="size">KB</td>`
break;
case "Type":
html = `<td><input type="text" value="${Param.type ? Param.type : ""}" name="text" placeholder="${i18n.type}" class="type"></td>`
html += `<td><input type="number" value="${Param.size ? Param.size : 0}" class="size" name="size">KB</td>`
break;
case "Regex":
html = `<td><input type="text" value="${Param.type ? Param.type : ""}" name="type" class="regexType"></td>`
html += `<td><input type="text" value="${Param.regex ? Param.regex : ""}" placeholder="${i18n.regexExpression}" name="regex" class="regex"></td>`
html += `<td><input type="text" value="${Param.ext ? Param.ext : ""}" name="regexExt" class="regexExt"></td>`
html += `<td>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" name="blackList" class="switchInput" ${Param.blackList ? 'checked="checked"' : ""}/>
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</td>`
break;
case "blockUrl":
html = `<td><input type="text" value="${Param.url ? Param.url : ""}" name="url" placeholder="${i18n.blockUrlTips}" class="width100"></td>`
break;
}
html = $(`<tr data-type="${Type}">
${html}
<td>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" name="state" class="switchInput" ${Param.state ? 'checked="checked"' : ""}/>
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</td>
<td>
<img src="img/delete.svg" class="RemoveButton">
</td>
</tr>`);
html.find(".RemoveButton").click(function () {
html.remove();
Save(Type);
});
html.find("input").on("input", function () {
Save(Type, 200);
});
html.find("[name=state]").on("click", function () {
Save(Type);
});
if (Type == "Type") {
html.find("input").blur(function () {
$("#typeList tr").each(function () {
let GetText = $(this).find("[name=text]").val();
if (isEmpty(GetText)) { return true; }
GetText = GetText.trim();
const test = GetText.split("/");
if (test.length != 2 || isEmpty(test[0]) || isEmpty(test[1])) {
alert(i18n.addTypeError);
return true;
}
});
});
}
return html;
}
// 预览模板
$("#PlayerTemplate").change(function () {
const Value = $(this).val();
if (this.id == "PlayerTemplate" && playerList.has(Value) && Value != "tips") {
const template = playerList.get(Value).template;
$("#Player").val(template);
chrome.storage.sync.set({ Player: template });
}
});
//失去焦点 保存自动清理数 模拟手机User Agent 自定义播放调用模板
let debounce2 = undefined;
$("[save='input']").on("input", function () {
let val = $(this).val().trim();
if (this.type == "number") {
val = parseInt(val);
}
clearTimeout(debounce2);
debounce2 = setTimeout(() => {
chrome.storage.sync.set({ [this.id]: val });
}, 300);
});
// 调试模式 使用网页标题做文件名 使用PotPlayer预览 显示网站图标 刷新自动清理
$("[save='click']").bind("click", function () {
chrome.storage.sync.set({ [this.id]: $(this).prop('checked') });
});
// [save='select'] 元素 储存
$("[save='select']").on("change", function () {
let val = $(this).val();
if (!isNaN(val)) { val = parseInt(val); }
chrome.storage.sync.set({ [this.id]: val });
});
// 一键禁用/启用
$("#allDisable, #allEnable").bind("click", function () {
const state = this.id == "allEnable";
const obj = $(this).data("switch");
let query;
if (obj == "Ext") {
query = $("#extList [name=state]");
} else if (obj == "Type") {
query = $("#typeList [name=state]");
} else if (obj == "Regex") {
query = $("#regexList [name=state]");
} else if (obj == "blockUrl") {
query = $("#blockUrlList [name=state]");
}
query.each(function () {
$(this).prop("checked", state);
});
Save(obj);
});
// m3u8dlArg 输出测试
function testTag() {
const data = {
url: $("#url").val(),
requestHeaders: { referer: $("#referer").val() },
initiator: $("#initiator").val(),
webUrl: $("#webUrl").val(),
title: $("#title").val(),
}
const result = templates($("#testTextarea").val() ?? "", data);
const m3u8dl = 'm3u8dl:' + (G.m3u8dl == 1 ? Base64.encode(result) : result);
$("#tagTestResult").html(`${result}<br><br><a href="${m3u8dl}" class="test_url">${m3u8dl}</a>`);
}
$("#showTestTag").bind("click", function () {
testTag();
$("#testTag").slideToggle();
});
$("#testTag input, #testTextarea").on("input", function () {
testTag();
});
//重置后缀 重置类型 重置正则
$("[data-reset]").bind("click", function () {
if (confirm(i18n.confirmReset)) {
const Option = $(this).data("reset");
chrome.storage.sync.set({ [Option]: G.OptionLists[Option] }, () => {
location.reload();
});
}
});
//重置设置
$(".resetOption").click(function () {
if (confirm(i18n.confirmReset)) {
const optionBox = $(this).closest('.optionBox');
const result = optionBox.find('[save]').toArray().reduce((acc, { id }) => {
acc[id] = G.OptionLists[id];
return acc;
}, {});
chrome.storage.sync.set(result, () => {
location.reload();
});
}
});
//清空数据 重置所有设置
$("#ClearData, #ResetAllOption").bind("click", function () {
if (this.id == "ResetAllOption") {
if (confirm(i18n.confirmReset)) {
chrome.storage.sync.clear();
InitOptions();
} else {
return;
}
}
chrome.storage.local.clear();
chrome.storage.session.clear();
chrome.runtime.sendMessage({ Message: "ClearIcon" });
location.reload();
});
//重启扩展
$("#extensionReload").bind("click", function () {
chrome.runtime.reload();
});
//正则表达式 测试
$("#testRegex, #testUrl").keyup(function () {
const testUrl = $("#testUrl").val();
const testRegex = $("#testRegex").val();
const testFlag = $("#testFlag").val();
if (testUrl == "" || testRegex == "") {
$("#testResult").html(i18n.noMatch);
return;
}
let regex;
try {
regex = new RegExp(testRegex, testFlag);
} catch (e) {
$("#testResult").html(e.message);
return;
}
const result = regex.exec(testUrl);
if (result == null) {
$("#testResult").html(i18n.noMatch);
return;
}
$("#testResult").html(i18n.match)
for (let i = 1; i < result.length; i++) {
if (result[i] != "") {
$("#testResult").append(
`<input type="text" style="width: 590px; color: #ff0000" value="${decodeURIComponent(result[i])}">`
);
}
}
});
//导出配置
$("#exportOptions").bind("click", function () {
chrome.storage.sync.get(null, function (items) {
let ExportData = JSON.stringify(items);
ExportData = "data:text/plain," + Base64.encode(ExportData);
let date = new Date();
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`;
if (G.isFirefox) {
downloadDataURL(ExportData, filename);
return;
}
chrome.downloads.download({
url: ExportData,
filename: filename
});
});
});
//导入配置
$("#importOptionsFile").change(function () {
const fileReader = new FileReader();
fileReader.onload = function () {
let importData = this.result;
try {
importData = JSON.parse(importData);
} catch (e) {
importData = Base64.decode(importData);
importData = JSON.parse(importData);
}
const keys = Object.keys(G.OptionLists);
for (let item in G.OptionLists) {
if (keys.includes(item) && importData[item]) {
chrome.storage.sync.set({ [item]: importData[item] });
}
}
alert("导入完成");
location.reload();
}
const file = $("#importOptionsFile").prop('files')[0];
fileReader.readAsText(file);
});
$("#importOptions").bind("click", function () {
$("#importOptionsFile").click();
});
// 保存 后缀 类型 正则 配置
function Save(option, sec = 0) {
clearTimeout(debounce);
debounce = setTimeout(() => {
if (option == "Ext") {
let Ext = new Array();
$("#extList tr").each(function () {
const _this = $(this);
let GetText = _this.find("[name=text]").val();
let GetSize = parseInt(_this.find("[name=size]").val());
let GetState = _this.find("[name=state]").prop("checked");
if (isEmpty(GetText)) { return true; }
if (isEmpty(GetSize)) { GetSize = 0; }
Ext.push({ ext: GetText.toLowerCase(), size: GetSize, state: GetState });
});
chrome.storage.sync.set({ Ext: Ext });
return;
}
if (option == "Type") {
let Type = new Array();
$("#typeList tr").each(function () {
const _this = $(this);
let GetText = _this.find("[name=text]").val();
let GetSize = parseInt(_this.find("[name=size]").val());
let GetState = _this.find("[name=state]").prop("checked");
if (isEmpty(GetText)) { return true; }
if (isEmpty(GetSize)) { GetSize = 0; }
GetText = GetText.trim();
const test = GetText.split("/");
if (test.length == 2 && !isEmpty(test[0]) && !isEmpty(test[1])) {
Type.push({ type: GetText.toLowerCase(), size: GetSize, state: GetState });
}
});
chrome.storage.sync.set({ Type: Type });
return;
}
if (option == "Regex") {
let Regex = new Array();
$("#regexList tr").each(function () {
const _this = $(this);
let GetType = _this.find("[name=type]").val();
let GetRegex = _this.find("[name=regex]").val();
let GetExt = _this.find("[name=regexExt]").val()
let GetState = _this.find("[name=state]").prop("checked");
let GetBlackList = _this.find("[name=blackList]").prop("checked");
try {
new RegExp("", GetType);
} catch (e) {
GetType = "ig";
}
if (isEmpty(GetRegex)) { return true; }
GetExt = GetExt ? GetExt.toLowerCase() : "";
Regex.push({ type: GetType, regex: GetRegex, ext: GetExt, blackList: GetBlackList, state: GetState });
});
chrome.storage.sync.set({ Regex: Regex });
return;
}
if (option == "blockUrl") {
let blockUrl = new Array();
$("#blockUrlList tr").each(function () {
const _this = $(this);
let url = _this.find("[name=url]").val();
let GetState = _this.find("[name=state]").prop("checked");
if (isEmpty(url)) { return true; }
blockUrl.push({ url: url, state: GetState });
});
chrome.storage.sync.set({ blockUrl: blockUrl });
return;
}
}, sec);
}
// 导航栏
document.querySelectorAll('nav a').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const targetId = this.getAttribute('href');
const targetElement = document.querySelector(targetId);
if (targetElement) {
targetElement.scrollIntoView({
behavior: 'smooth'
});
}
});
});
const adjustSidebarPosition = () => {
const wrapper = document.querySelector('.wrapper');
const sidebar = document.querySelector('.sidebar');
if (wrapper && sidebar) {
sidebar.style.left = `${wrapper.getBoundingClientRect().left - sidebar.offsetWidth - 20}px`;
}
}
window.addEventListener('load', adjustSidebarPosition)
window.addEventListener('resize', adjustSidebarPosition);

1065
js/popup.js Normal file

File diff suppressed because it is too large Load Diff

1035
js/preview.js Normal file

File diff suppressed because it is too large Load Diff

232
js/pupup-utils.js Normal file
View File

@@ -0,0 +1,232 @@
// 复制选项
function copyLink(data) {
let text = data.url;
if (data.parsing == "m3u8") {
text = G.copyM3U8;
} else if (data.parsing == "mpd") {
text = G.copyMPD;
} else {
text = G.copyOther;
}
return templates(text, data);
}
function isM3U8(data) {
return (
data.ext == "m3u8" ||
data.ext == "m3u" ||
data.type?.endsWith("/vnd.apple.mpegurl") ||
data.type?.endsWith("/x-mpegurl") ||
data.type?.endsWith("/mpegurl") ||
data.type?.endsWith("/octet-stream-m3u8")
)
}
function isMPD(data) {
return (data.ext == "mpd" ||
data.type == "application/dash+xml"
)
}
function isJSON(data) {
return (data.ext == "json" ||
data.type == "application/json" ||
data.type == "text/json"
)
}
function isPicture(data) {
return (data.type?.startsWith("image/") ||
data.ext == "jpg" ||
data.ext == "png" ||
data.ext == "jpeg" ||
data.ext == "bmp" ||
data.ext == "gif" ||
data.ext == "webp" ||
data.ext == "svg"
)
}
function isMediaExt(ext) {
return ['ogg', 'ogv', 'mp4', 'webm', 'mp3', 'wav', 'm4a', '3gp', 'mpeg', 'mov', 'm4s', 'aac'].includes(ext);
}
function isMedia(data) {
return isMediaExt(data.ext) || data.type?.startsWith("video/") || data.type?.startsWith("audio/");
}
/**
* ari2a RPC发送一套资源
* @param {object} data 资源对象
* @param {Function} success 成功运行函数
* @param {Function} error 失败运行函数
*/
function aria2AddUri(data, success, error) {
const json = {
"jsonrpc": "2.0",
"id": "cat-catch-" + data.requestId,
"method": "aria2.addUri",
"params": []
};
if (G.aria2RpcToken) {
json.params.push(`token:${G.aria2RpcToken}`);
}
const params = { out: data.downFileName };
if (G.enableAria2RpcReferer) {
params.header = [];
params.header.push(G.userAgent ? G.userAgent : navigator.userAgent);
if (data.requestHeaders?.referer) {
params.header.push("Referer: " + data.requestHeaders.referer);
}
if (data.cookie) {
params.header.push("Cookie: " + data.cookie);
}
if (data.requestHeaders?.authorization) {
params.header.push("Authorization: " + data.requestHeaders.authorization);
}
}
json.params.push([data.url], params);
fetch(G.aria2Rpc, {
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8"
},
body: JSON.stringify(json)
}).then(response => {
return response.json();
}).then(data => {
success && success(data);
}).catch(errMsg => {
error && error(errMsg);
});
}
// MQTT 相关功能
/**
* 发送数据到 MQTT 服务器
* @param {Object} data - 要发送的媒体数据
* @returns {Promise} - 返回发送结果的 Promise
*/
function sendToMQTT(data, config) {
return new Promise((resolve, reject) => {
if (!G.mqttEnable) {
reject("MQTT is not enabled");
return;
}
// 使用配置的标题长度如果未设置则默认为100
const titleLength = G.mqttTitleLength || 100;
data.title = data.title.slice(0, titleLength) || "";
data.action = "media_found";
data = trimData(data);
// 创建 MQTT 连接并发送数据
connectAndSendMQTT(data, config)
.then(() => {
resolve(true);
})
.catch((error) => {
console.error("MQTT send error:", error);
reject("MQTT send failed: " + error.message);
});
});
}
/**
* 连接到 MQTT 服务器并发送消息
* @param {Object} data - 要发送的数据
* @returns {Promise} - 连接和发送的 Promise
*/
function connectAndSendMQTT(data, config) {
return new Promise((resolve, reject) => {
try {
// 构建 MQTT 连接 URL
const protocol = G.mqttProtocol;
const broker = G.mqttBroker;
const port = G.mqttPort;
const path = G.mqttPath;
if (!protocol || !broker || !port || !path) {
throw new Error("MQTT connection parameters are missing");
}
const mqttUrl = `${protocol}://${broker}:${port}${path}`;
// 创建 MQTT 客户端选项
const options = {
clientId: `${G.mqttClientId || "cat-catch-client"}-${Math.random().toString(16).slice(2)}`,
clean: true,
connectTimeout: 10000,
reconnectPeriod: 0 // 不自动重连,用完即断
};
// 添加用户名和密码(如果有)
if (G.mqttUser) {
options.username = G.mqttUser;
}
if (G.mqttPassword) {
options.password = G.mqttPassword;
}
const mqttLib = window.mqtt || (typeof mqtt !== 'undefined' ? mqtt : null);
if (!mqttLib) {
throw new Error("MQTT library not found. Please check if lib/mqtt.min.js is loaded correctly.");
}
if (!mqttLib.connect) {
throw new Error("MQTT.connect method not found. Available methods: " + Object.keys(mqttLib));
}
// 如果提供了提示回调函数,则使用它
if (typeof config?.alert === 'function') {
Tips = config.alert;
}
// 2. 创建连接阶段提示:正在连接 MQTT 服务器
Tips(i18n.connectingToMQTT || "Connecting to MQTT server...", 2000);
const client = mqttLib.connect(mqttUrl, options);
// 连接成功
client.on('connect', () => {
const topic = G.mqttTopic || "cat-catch/media";
const qos = parseInt(G.mqttQos) || 2;
// 处理自定义数据格式
let message;
if (G.mqttDataFormat?.trim()) {
message = templates(G.mqttDataFormat, data);
} else {
// 使用默认JSON格式
message = JSON.stringify(data);
}
// 3. 正在发送消息到 MQTT 服务器
Tips(i18n.sendingMessageToMQTT || "Sending message to MQTT server...", 2000);
// 发送消息
client.publish(topic, message, { qos: qos }, (error) => {
if (error) {
console.error("MQTT publish error:", error);
reject(error);
} else {
resolve();
}
});
});
// 连接错误
client.on('error', (error) => {
console.error("MQTT connection error:", error);
reject(error);
});
// 连接超时
setTimeout(() => {
if (!client.connected) {
client.end();
reject(new Error("MQTT connection timeout"));
}
}, 6000);
// client.on('close', () => {
// console.log('MQTT connection closed');
// });
} catch (error) {
console.error("MQTT setup error:", error);
reject(error);
}
});
}

45
json.html Normal file
View File

@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>titleJson</title>
<link rel="stylesheet" type="text/css" href="css/public.css" media="all" />
<link rel="stylesheet" type="text/css" href="css/options.css" media="all" />
<script src="lib/jquery.min.js"></script>
<script src="js/init.js"></script>
<script src="js/firefox.js"></script>
<script src="lib/jquery.json-viewer.js"></script>
<script src="js/function.js"></script>
<script src="js/json.js"></script>
</head>
<body>
<div class="wrapper1024">
<section id="jsonCustom" class="hide">
<h1 class="optionsTitle" data-i18n="titleJson"></h1>
<div class="optionBox">
<span class="explain">json</span>
<textarea id="jsonText" spellcheck="false" data-type="link" class="width100"></textarea>
<div class="line"></div>
<span class="explain">json url</span>
<input type="text" id="jsonUrl" placeholder="jsonURL" class="fullInput" />
<button id="format" type="button" data-i18n="jsonFormatter"></button>
</div>
</section>
<section id="main">
<h1 class="optionsTitle" data-i18n="titleJson"></h1>
<div class="optionBox">
<div class="block">
<pre id="json-renderer" data-i18n="fileLoading"></pre>
</div>
<button id="collapsed" data-i18n="expandAllNodes"></button>
</div>
</section>
</div>
<script src="js/i18n.js"></script>
</body>
</html>

124
justfile Normal file
View File

@@ -0,0 +1,124 @@
# 构建脚本crx
# 设置默认shell为bash
set shell := ["bash", "-c"]
# 默认任务:显示帮助
default:
@just --list
# 安装依赖
install:
npm install
npm install -g crx3
# 清理构建目录
clean:
rm -rf build dist web-ext-artifacts
rm -f *.crx *.zip private-key.pem
# 验证manifest文件
validate:
@echo "验证 manifest.json..."
@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'); }"
# 准备构建目录
prepare: validate
@echo "准备构建目录..."
mkdir -p build
cp -r ./{catch-script,css,img,js,lib,_locales} build/
cp -r ./*.{js,html} build/
@echo "✅ 文件复制完成"
# 检查图标文件
check-icons:
@echo "检查图标文件..."
@if [ ! -d "img" ]; then echo "❌ img/ 目录不存在"; exit 1; fi
@if [ ! -f "img/icon.png" ]; then echo "❌ 缺少图标: img/icon.png"; exit 1; fi
@if [ ! -f "img/icon128.png" ]; then echo "❌ 缺少图标: img/icon128.png"; exit 1; fi
@echo "✅ 所有图标文件存在"
# 生成私钥
generate-key:
@echo "生成私钥..."
@if [ ! -f "private-key.pem" ]; then \
openssl genrsa -out private-key.pem 2048; \
echo "✅ 私钥已生成"; \
else \
echo "✅ 私钥已存在"; \
fi
# 构建ZIP文件
build-zip: prepare check-icons
@echo "构建 ZIP 文件..."
@cd build && \
VERSION=$(node -p "require('./manifest.json').version") && \
zip -r "../cat-catch${VERSION}.zip" . && \
echo "✅ ZIP 文件已生成: cat-catch${VERSION}.zip"
# 构建CRX文件
build-crx: prepare check-icons generate-key
@echo "构建 CRX 文件..."
@VERSION=$(node -p "require('./manifest.json').version") && \
crx3 -p private-key.pem -o "cat-catch${VERSION}.crx" build/ && \
echo "✅ CRX 文件已生成: cat-catch${VERSION}.crx"
# 快速构建仅ZIP
quick: build-zip
@echo "🚀 快速构建完成!"
# 完整构建CRX + ZIP
build: build-crx build-zip
@echo "🎉 构建完成!"
@ls -la *.crx *.zip 2>/dev/null || true
# 开发模式 - 自动重载
dev-watch: prepare
@echo "🔄 开发模式 - 自动构建"
@echo "================================"
@echo "监听文件变化并自动重新构建到 build/ 目录"
@echo "请在Chrome中加载 build/ 目录,然后刷新扩展"
@echo ""
@if command -v inotifywait >/dev/null 2>&1; then \
while true; do \
inotifywait -r -e modify,create,delete src/ && \
echo "🔄 检测到文件变化,重新构建..." && \
just prepare; \
done \
else \
echo "❌ 需要安装 inotify-tools: sudo apt install inotify-tools"; \
fi
# 检查扩展
lint:
@echo "检查扩展..."
@echo "验证 manifest.json 格式..."
@node -e "const manifest = require('./manifest.json'); console.log('✅ Manifest 格式正确'); console.log('扩展名:', manifest.name); console.log('版本:', manifest.version);"
@echo "检查必需文件..."
@if [ -f "popup.html" ]; then echo "✅ popup.html 存在"; else echo "❌ popup.html 缺失"; fi
@if [ -f "options.html" ]; then echo "✅ options.html 存在"; else echo "❌ options.html 缺失"; fi
@if [ -f "js/background.js" ]; then echo "✅ background.js 存在"; else echo "❌ background.js 缺失"; fi
@if [ -f "js/content-script.js" ]; then echo "✅ content.js 存在"; else echo "❌ content.js 缺失"; fi
@if [ -f "js/popup.js" ]; then echo "✅ popup.js 存在"; else echo "❌ popup.js 缺失"; fi
@if [ -f "js/options.js" ]; then echo "✅ options.js 存在"; else echo "❌ options.js 缺失"; fi
@echo "✅ Chrome扩展检查完成"
# 显示版本信息
version:
@node -p "'当前版本: ' + require('./manifest.json').version"
# 显示项目状态
status:
@echo "📊 项目状态:"
@node -p "'版本: ' + require('./manifest.json').version"
@echo "图标状态:"
@if [ -f "img/icon.png" ]; then echo " ✅ img/icon.png"; else echo " ❌ img/icon.png"; fi
@if [ -f "img/icon128.png" ]; then echo " ✅ img/icon128.png"; else echo " ❌ img/icon128.png"; fi
@echo "构建文件:"
@ls -la *.crx *.zip 2>/dev/null || echo " 无构建文件"
# 完整的发布流程
release: clean install validate build
@echo "🎉 发布包已准备完成!"
@echo "文件列表:"
@ls -la *.crx *.zip

317
lib/StreamSaver.js Normal file
View File

@@ -0,0 +1,317 @@
/*! streamsaver. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
/* global chrome location ReadableStream define MessageChannel TransformStream */
;((name, definition) => {
typeof module !== 'undefined'
? module.exports = definition()
: typeof define === 'function' && typeof define.amd === 'object'
? define(definition)
: this[name] = definition()
})('streamSaver', () => {
'use strict'
const global = typeof window === 'object' ? window : this
if (!global.HTMLElement) console.warn('streamsaver is meant to run on browsers main thread')
let mitmTransporter = null
let supportsTransferable = false
const test = fn => { try { fn() } catch (e) {} }
const ponyfill = global.WebStreamsPolyfill || {}
const isSecureContext = global.isSecureContext
// TODO: Must come up with a real detection test (#69)
let useBlobFallback = /constructor/i.test(global.HTMLElement) || !!global.safari || !!global.WebKitPoint
const downloadStrategy = isSecureContext || 'MozAppearance' in document.documentElement.style
? 'iframe'
: 'navigate'
const streamSaver = {
createWriteStream,
WritableStream: global.WritableStream || ponyfill.WritableStream,
supported: true,
version: { full: '2.0.5', major: 2, minor: 0, dot: 5 },
mitm: 'https://jimmywarting.github.io/StreamSaver.js/mitm.html?version=2.0.0'
}
/**
* create a hidden iframe and append it to the DOM (body)
*
* @param {string} src page to load
* @return {HTMLIFrameElement} page to load
*/
function makeIframe (src) {
if (!src) throw new Error('meh')
const iframe = document.createElement('iframe')
iframe.hidden = true
iframe.src = src
iframe.loaded = false
iframe.name = 'iframe'
iframe.isIframe = true
iframe.postMessage = (...args) => iframe.contentWindow.postMessage(...args)
iframe.addEventListener('load', () => {
iframe.loaded = true
}, { once: true })
document.body.appendChild(iframe)
return iframe
}
/**
* create a popup that simulates the basic things
* of what a iframe can do
*
* @param {string} src page to load
* @return {object} iframe like object
*/
function makePopup (src) {
const options = 'width=200,height=100'
const delegate = document.createDocumentFragment()
const popup = {
frame: global.open(src, 'popup', options),
loaded: false,
isIframe: false,
isPopup: true,
remove () { popup.frame.close() },
addEventListener (...args) { delegate.addEventListener(...args) },
dispatchEvent (...args) { delegate.dispatchEvent(...args) },
removeEventListener (...args) { delegate.removeEventListener(...args) },
postMessage (...args) { popup.frame.postMessage(...args) }
}
const onReady = evt => {
if (evt.source === popup.frame) {
popup.loaded = true
global.removeEventListener('message', onReady)
popup.dispatchEvent(new Event('load'))
}
}
global.addEventListener('message', onReady)
return popup
}
try {
// We can't look for service worker since it may still work on http
new Response(new ReadableStream())
if (isSecureContext && !('serviceWorker' in navigator)) {
useBlobFallback = true
}
} catch (err) {
useBlobFallback = true
}
test(() => {
// Transferable stream was first enabled in chrome v73 behind a flag
const { readable } = new TransformStream()
const mc = new MessageChannel()
mc.port1.postMessage(readable, [readable])
mc.port1.close()
mc.port2.close()
supportsTransferable = true
// Freeze TransformStream object (can only work with native)
Object.defineProperty(streamSaver, 'TransformStream', {
configurable: false,
writable: false,
value: TransformStream
})
})
function loadTransporter () {
if (!mitmTransporter) {
mitmTransporter = isSecureContext
? makeIframe(streamSaver.mitm)
: makePopup(streamSaver.mitm)
}
}
/**
* @param {string} filename filename that should be used
* @param {object} options [description]
* @param {number} size deprecated
* @return {WritableStream<Uint8Array>}
*/
function createWriteStream (filename, options, size) {
let opts = {
size: null,
pathname: null,
writableStrategy: undefined,
readableStrategy: undefined
}
let bytesWritten = 0 // by StreamSaver.js (not the service worker)
let downloadUrl = null
let channel = null
let ts = null
// normalize arguments
if (Number.isFinite(options)) {
[ size, options ] = [ options, size ]
console.warn('[StreamSaver] Deprecated pass an object as 2nd argument when creating a write stream')
opts.size = size
opts.writableStrategy = options
} else if (options && options.highWaterMark) {
console.warn('[StreamSaver] Deprecated pass an object as 2nd argument when creating a write stream')
opts.size = size
opts.writableStrategy = options
} else {
opts = options || {}
}
if (!useBlobFallback) {
loadTransporter()
channel = new MessageChannel()
// Make filename RFC5987 compatible
filename = encodeURIComponent(filename.replace(/\//g, ':'))
.replace(/['()]/g, escape)
.replace(/\*/g, '%2A')
const response = {
transferringReadable: supportsTransferable,
pathname: opts.pathname || Math.random().toString().slice(-6) + '/' + filename,
headers: {
'Content-Type': 'application/octet-stream; charset=utf-8',
'Content-Disposition': "attachment; filename*=UTF-8''" + filename
}
}
if (opts.size) {
response.headers['Content-Length'] = opts.size
}
const args = [ response, '*', [ channel.port2 ] ]
if (supportsTransferable) {
const transformer = downloadStrategy === 'iframe' ? undefined : {
// This transformer & flush method is only used by insecure context.
transform (chunk, controller) {
if (!(chunk instanceof Uint8Array)) {
throw new TypeError('Can only write Uint8Arrays')
}
bytesWritten += chunk.length
controller.enqueue(chunk)
if (downloadUrl) {
location.href = downloadUrl
downloadUrl = null
}
},
flush () {
if (downloadUrl) {
location.href = downloadUrl
}
}
}
ts = new streamSaver.TransformStream(
transformer,
opts.writableStrategy,
opts.readableStrategy
)
const readableStream = ts.readable
channel.port1.postMessage({ readableStream }, [ readableStream ])
}
channel.port1.onmessage = evt => {
// Service worker sent us a link that we should open.
if (evt.data.download) {
// Special treatment for popup...
if (downloadStrategy === 'navigate') {
mitmTransporter.remove()
mitmTransporter = null
if (bytesWritten) {
location.href = evt.data.download
} else {
downloadUrl = evt.data.download
}
} else {
if (mitmTransporter.isPopup) {
mitmTransporter.remove()
mitmTransporter = null
// Special case for firefox, they can keep sw alive with fetch
if (downloadStrategy === 'iframe') {
makeIframe(streamSaver.mitm)
}
}
// We never remove this iframes b/c it can interrupt saving
makeIframe(evt.data.download)
}
} else if (evt.data.abort) {
chunks = []
channel.port1.postMessage('abort') //send back so controller is aborted
channel.port1.onmessage = null
channel.port1.close()
channel.port2.close()
channel = null
}
}
if (mitmTransporter.loaded) {
mitmTransporter.postMessage(...args)
} else {
mitmTransporter.addEventListener('load', () => {
mitmTransporter.postMessage(...args)
}, { once: true })
}
}
let chunks = []
return (!useBlobFallback && ts && ts.writable) || new streamSaver.WritableStream({
write (chunk) {
if (!(chunk instanceof Uint8Array)) {
throw new TypeError('Can only write Uint8Arrays')
}
if (useBlobFallback) {
// Safari... The new IE6
// https://github.com/jimmywarting/StreamSaver.js/issues/69
//
// even though it has everything it fails to download anything
// that comes from the service worker..!
chunks.push(chunk)
return
}
// is called when a new chunk of data is ready to be written
// to the underlying sink. It can return a promise to signal
// success or failure of the write operation. The stream
// implementation guarantees that this method will be called
// only after previous writes have succeeded, and never after
// close or abort is called.
// TODO: Kind of important that service worker respond back when
// it has been written. Otherwise we can't handle backpressure
// EDIT: Transferable streams solves this...
channel.port1.postMessage(chunk)
bytesWritten += chunk.length
if (downloadUrl) {
location.href = downloadUrl
downloadUrl = null
}
},
close () {
if (useBlobFallback) {
const blob = new Blob(chunks, { type: 'application/octet-stream; charset=utf-8' })
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = filename
link.click()
} else {
channel.port1.postMessage('end')
}
},
abort () {
chunks = []
channel.port1.postMessage('abort')
channel.port1.onmessage = null
channel.port1.close()
channel.port2.close()
channel = null
}
}, opts.writableStrategy)
}
return streamSaver
})

40
lib/base64.js Normal file
View File

@@ -0,0 +1,40 @@
class Base64 {
/**
* 将字符串编码为Base64支持UTF-8
* @param {string} str - 需要编码的原始字符串
* @returns {string} Base64编码结果
*/
static encode(str) {
// 使用TextEncoder将字符串转换为UTF-8字节数组
const encoder = new TextEncoder();
const data = encoder.encode(str);
// 将字节数组转换为二进制字符串
let binary = '';
data.forEach(byte => binary += String.fromCharCode(byte));
// 使用浏览器内置方法进行Base64编码
return btoa(binary);
}
/**
* 解码Base64字符串为原始字符串支持UTF-8
* @param {string} base64Str - Base64编码字符串
* @returns {string} 解码后的原始字符串
*/
static decode(base64Str) {
// 解码Base64得到二进制字符串
const binaryStr = atob(base64Str);
// 将二进制字符串转换为字节数组
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) {
bytes[i] = binaryStr.charCodeAt(i);
}
// 使用TextDecoder将字节数组转换为UTF-8字符串
const decoder = new TextDecoder();
return decoder.decode(bytes);
}
}
window.Base64 = Base64;

2
lib/hls.min.js vendored Normal file

File diff suppressed because one or more lines are too long

181
lib/jquery.json-viewer.js Normal file
View File

@@ -0,0 +1,181 @@
/**
* jQuery json-viewer
* @author: Alexandre Bodelot <alexandre.bodelot@gmail.com>
* @link: https://github.com/abodelot/jquery.json-viewer
*/
(function($) {
/**
* Check if arg is either an array with at least 1 element, or a dict with at least 1 key
* @return boolean
*/
function isCollapsable(arg) {
return arg instanceof Object && Object.keys(arg).length > 0;
}
/**
* Check if a string looks like a URL, based on protocol
* This doesn't attempt to validate URLs, there's no use and syntax can be too complex
* @return boolean
*/
function isUrl(string) {
var protocols = ['http', 'https', 'ftp', 'ftps'];
for (var i = 0; i < protocols.length; ++i) {
if (string.startsWith(protocols[i] + '://')) {
return true;
}
}
return false;
}
/**
* Return the input string html escaped
* @return string
*/
function htmlEscape(s) {
return s.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/'/g, '&apos;')
.replace(/"/g, '&quot;');
}
/**
* Transform a json object into html representation
* @return string
*/
function json2html(json, options) {
var html = '';
if (typeof json === 'string') {
// Escape tags and quotes
json = htmlEscape(json);
if (options.withLinks && isUrl(json)) {
html += '<a href="' + json + '" class="json-string" target="_blank">' + json + '</a>';
} else {
// Escape double quotes in the rendered non-URL string.
json = json.replace(/&quot;/g, '\\&quot;');
html += '<span class="json-string">"' + json + '"</span>';
}
} else if (typeof json === 'number' || typeof json === 'bigint') {
html += '<span class="json-literal">' + json + '</span>';
} else if (typeof json === 'boolean') {
html += '<span class="json-literal">' + json + '</span>';
} else if (json === null) {
html += '<span class="json-literal">null</span>';
} else if (json instanceof Array) {
if (json.length > 0) {
html += '[<ol class="json-array">';
for (var i = 0; i < json.length; ++i) {
html += '<li>';
// Add toggle button if item is collapsable
if (isCollapsable(json[i])) {
html += '<a href class="json-toggle"></a>';
}
html += json2html(json[i], options);
// Add comma if item is not last
if (i < json.length - 1) {
html += ',';
}
html += '</li>';
}
html += '</ol>]';
} else {
html += '[]';
}
} else if (typeof json === 'object') {
// Optional support different libraries for big numbers
// json.isLosslessNumber: package lossless-json
// json.toExponential(): packages bignumber.js, big.js, decimal.js, decimal.js-light, others?
if (options.bigNumbers && (typeof json.toExponential === 'function' || json.isLosslessNumber)) {
html += '<span class="json-literal">' + json.toString() + '</span>';
} else {
var keyCount = Object.keys(json).length;
if (keyCount > 0) {
html += '{<ul class="json-dict">';
for (var key in json) {
if (Object.prototype.hasOwnProperty.call(json, key)) {
key = htmlEscape(key);
var keyRepr = options.withQuotes ?
'<span class="json-string">"' + key + '"</span>' : key;
html += '<li>';
// Add toggle button if item is collapsable
if (isCollapsable(json[key])) {
html += '<a href class="json-toggle">' + keyRepr + '</a>';
} else {
html += keyRepr;
}
html += ': ' + json2html(json[key], options);
// Add comma if item is not last
if (--keyCount > 0) {
html += ',';
}
html += '</li>';
}
}
html += '</ul>}';
} else {
html += '{}';
}
}
}
return html;
}
/**
* jQuery plugin method
* @param json: a javascript object
* @param options: an optional options hash
*/
$.fn.jsonViewer = function(json, options) {
// Merge user options with default options
options = Object.assign({}, {
collapsed: false,
rootCollapsable: true,
withQuotes: false,
withLinks: true,
bigNumbers: false
}, options);
// jQuery chaining
return this.each(function() {
// Transform to HTML
var html = json2html(json, options);
if (options.rootCollapsable && isCollapsable(json)) {
html = '<a href class="json-toggle"></a>' + html;
}
// Insert HTML in target DOM element
$(this).html(html);
$(this).addClass('json-document');
// Bind click on toggle buttons
$(this).off('click');
$(this).on('click', 'a.json-toggle', function() {
var target = $(this).toggleClass('collapsed').siblings('ul.json-dict, ol.json-array');
target.toggle();
if (target.is(':visible')) {
target.siblings('.json-placeholder').remove();
} else {
var count = target.children('li').length;
var placeholder = count + (count > 1 ? ' items' : ' item');
target.after('<a href class="json-placeholder">' + placeholder + '</a>');
}
return false;
});
// Simulate click on toggle button when placeholder is clicked
$(this).on('click', 'a.json-placeholder', function() {
$(this).siblings('a.json-toggle').click();
return false;
});
if (options.collapsed == true) {
// Trigger click to collapse all nodes
$(this).find('a.json-toggle').click();
}
});
};
})(jQuery);

2
lib/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

28
lib/jquery.qrcode.min.js vendored Normal file
View File

@@ -0,0 +1,28 @@
(function(r){r.fn.qrcode=function(h){var s;function u(a){this.mode=s;this.data=a}function o(a,c){this.typeNumber=a;this.errorCorrectLevel=c;this.modules=null;this.moduleCount=0;this.dataCache=null;this.dataList=[]}function q(a,c){if(void 0==a.length)throw Error(a.length+"/"+c);for(var d=0;d<a.length&&0==a[d];)d++;this.num=Array(a.length-d+c);for(var b=0;b<a.length-d;b++)this.num[b]=a[b+d]}function p(a,c){this.totalCount=a;this.dataCount=c}function t(){this.buffer=[];this.length=0}u.prototype={getLength:function(){return this.data.length},
write:function(a){for(var c=0;c<this.data.length;c++)a.put(this.data.charCodeAt(c),8)}};o.prototype={addData:function(a){this.dataList.push(new u(a));this.dataCache=null},isDark:function(a,c){if(0>a||this.moduleCount<=a||0>c||this.moduleCount<=c)throw Error(a+","+c);return this.modules[a][c]},getModuleCount:function(){return this.moduleCount},make:function(){if(1>this.typeNumber){for(var a=1,a=1;40>a;a++){for(var c=p.getRSBlocks(a,this.errorCorrectLevel),d=new t,b=0,e=0;e<c.length;e++)b+=c[e].dataCount;
for(e=0;e<this.dataList.length;e++)c=this.dataList[e],d.put(c.mode,4),d.put(c.getLength(),j.getLengthInBits(c.mode,a)),c.write(d);if(d.getLengthInBits()<=8*b)break}this.typeNumber=a}this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17;this.modules=Array(this.moduleCount);for(var d=0;d<this.moduleCount;d++){this.modules[d]=Array(this.moduleCount);for(var b=0;b<this.moduleCount;b++)this.modules[d][b]=null}this.setupPositionProbePattern(0,0);this.setupPositionProbePattern(this.moduleCount-
7,0);this.setupPositionProbePattern(0,this.moduleCount-7);this.setupPositionAdjustPattern();this.setupTimingPattern();this.setupTypeInfo(a,c);7<=this.typeNumber&&this.setupTypeNumber(a);null==this.dataCache&&(this.dataCache=o.createData(this.typeNumber,this.errorCorrectLevel,this.dataList));this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,c){for(var d=-1;7>=d;d++)if(!(-1>=a+d||this.moduleCount<=a+d))for(var b=-1;7>=b;b++)-1>=c+b||this.moduleCount<=c+b||(this.modules[a+d][c+b]=
0<=d&&6>=d&&(0==b||6==b)||0<=b&&6>=b&&(0==d||6==d)||2<=d&&4>=d&&2<=b&&4>=b?!0:!1)},getBestMaskPattern:function(){for(var a=0,c=0,d=0;8>d;d++){this.makeImpl(!0,d);var b=j.getLostPoint(this);if(0==d||a>b)a=b,c=d}return c},createMovieClip:function(a,c,d){a=a.createEmptyMovieClip(c,d);this.make();for(c=0;c<this.modules.length;c++)for(var d=1*c,b=0;b<this.modules[c].length;b++){var e=1*b;this.modules[c][b]&&(a.beginFill(0,100),a.moveTo(e,d),a.lineTo(e+1,d),a.lineTo(e+1,d+1),a.lineTo(e,d+1),a.endFill())}return a},
setupTimingPattern:function(){for(var a=8;a<this.moduleCount-8;a++)null==this.modules[a][6]&&(this.modules[a][6]=0==a%2);for(a=8;a<this.moduleCount-8;a++)null==this.modules[6][a]&&(this.modules[6][a]=0==a%2)},setupPositionAdjustPattern:function(){for(var a=j.getPatternPosition(this.typeNumber),c=0;c<a.length;c++)for(var d=0;d<a.length;d++){var b=a[c],e=a[d];if(null==this.modules[b][e])for(var f=-2;2>=f;f++)for(var i=-2;2>=i;i++)this.modules[b+f][e+i]=-2==f||2==f||-2==i||2==i||0==f&&0==i?!0:!1}},setupTypeNumber:function(a){for(var c=
j.getBCHTypeNumber(this.typeNumber),d=0;18>d;d++){var b=!a&&1==(c>>d&1);this.modules[Math.floor(d/3)][d%3+this.moduleCount-8-3]=b}for(d=0;18>d;d++)b=!a&&1==(c>>d&1),this.modules[d%3+this.moduleCount-8-3][Math.floor(d/3)]=b},setupTypeInfo:function(a,c){for(var d=j.getBCHTypeInfo(this.errorCorrectLevel<<3|c),b=0;15>b;b++){var e=!a&&1==(d>>b&1);6>b?this.modules[b][8]=e:8>b?this.modules[b+1][8]=e:this.modules[this.moduleCount-15+b][8]=e}for(b=0;15>b;b++)e=!a&&1==(d>>b&1),8>b?this.modules[8][this.moduleCount-
b-1]=e:9>b?this.modules[8][15-b-1+1]=e:this.modules[8][15-b-1]=e;this.modules[this.moduleCount-8][8]=!a},mapData:function(a,c){for(var d=-1,b=this.moduleCount-1,e=7,f=0,i=this.moduleCount-1;0<i;i-=2)for(6==i&&i--;;){for(var g=0;2>g;g++)if(null==this.modules[b][i-g]){var n=!1;f<a.length&&(n=1==(a[f]>>>e&1));j.getMask(c,b,i-g)&&(n=!n);this.modules[b][i-g]=n;e--; -1==e&&(f++,e=7)}b+=d;if(0>b||this.moduleCount<=b){b-=d;d=-d;break}}}};o.PAD0=236;o.PAD1=17;o.createData=function(a,c,d){for(var c=p.getRSBlocks(a,
c),b=new t,e=0;e<d.length;e++){var f=d[e];b.put(f.mode,4);b.put(f.getLength(),j.getLengthInBits(f.mode,a));f.write(b)}for(e=a=0;e<c.length;e++)a+=c[e].dataCount;if(b.getLengthInBits()>8*a)throw Error("code length overflow. ("+b.getLengthInBits()+">"+8*a+")");for(b.getLengthInBits()+4<=8*a&&b.put(0,4);0!=b.getLengthInBits()%8;)b.putBit(!1);for(;!(b.getLengthInBits()>=8*a);){b.put(o.PAD0,8);if(b.getLengthInBits()>=8*a)break;b.put(o.PAD1,8)}return o.createBytes(b,c)};o.createBytes=function(a,c){for(var d=
0,b=0,e=0,f=Array(c.length),i=Array(c.length),g=0;g<c.length;g++){var n=c[g].dataCount,h=c[g].totalCount-n,b=Math.max(b,n),e=Math.max(e,h);f[g]=Array(n);for(var k=0;k<f[g].length;k++)f[g][k]=255&a.buffer[k+d];d+=n;k=j.getErrorCorrectPolynomial(h);n=(new q(f[g],k.getLength()-1)).mod(k);i[g]=Array(k.getLength()-1);for(k=0;k<i[g].length;k++)h=k+n.getLength()-i[g].length,i[g][k]=0<=h?n.get(h):0}for(k=g=0;k<c.length;k++)g+=c[k].totalCount;d=Array(g);for(k=n=0;k<b;k++)for(g=0;g<c.length;g++)k<f[g].length&&
(d[n++]=f[g][k]);for(k=0;k<e;k++)for(g=0;g<c.length;g++)k<i[g].length&&(d[n++]=i[g][k]);return d};s=4;for(var j={PATTERN_POSITION_TABLE:[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,
78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],G15:1335,G18:7973,G15_MASK:21522,getBCHTypeInfo:function(a){for(var c=a<<10;0<=j.getBCHDigit(c)-j.getBCHDigit(j.G15);)c^=j.G15<<j.getBCHDigit(c)-j.getBCHDigit(j.G15);return(a<<10|c)^j.G15_MASK},getBCHTypeNumber:function(a){for(var c=a<<12;0<=j.getBCHDigit(c)-
j.getBCHDigit(j.G18);)c^=j.G18<<j.getBCHDigit(c)-j.getBCHDigit(j.G18);return a<<12|c},getBCHDigit:function(a){for(var c=0;0!=a;)c++,a>>>=1;return c},getPatternPosition:function(a){return j.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,c,d){switch(a){case 0:return 0==(c+d)%2;case 1:return 0==c%2;case 2:return 0==d%3;case 3:return 0==(c+d)%3;case 4:return 0==(Math.floor(c/2)+Math.floor(d/3))%2;case 5:return 0==c*d%2+c*d%3;case 6:return 0==(c*d%2+c*d%3)%2;case 7:return 0==(c*d%3+(c+d)%2)%2;default:throw Error("bad maskPattern:"+
a);}},getErrorCorrectPolynomial:function(a){for(var c=new q([1],0),d=0;d<a;d++)c=c.multiply(new q([1,l.gexp(d)],0));return c},getLengthInBits:function(a,c){if(1<=c&&10>c)switch(a){case 1:return 10;case 2:return 9;case s:return 8;case 8:return 8;default:throw Error("mode:"+a);}else if(27>c)switch(a){case 1:return 12;case 2:return 11;case s:return 16;case 8:return 10;default:throw Error("mode:"+a);}else if(41>c)switch(a){case 1:return 14;case 2:return 13;case s:return 16;case 8:return 12;default:throw Error("mode:"+
a);}else throw Error("type:"+c);},getLostPoint:function(a){for(var c=a.getModuleCount(),d=0,b=0;b<c;b++)for(var e=0;e<c;e++){for(var f=0,i=a.isDark(b,e),g=-1;1>=g;g++)if(!(0>b+g||c<=b+g))for(var h=-1;1>=h;h++)0>e+h||c<=e+h||0==g&&0==h||i==a.isDark(b+g,e+h)&&f++;5<f&&(d+=3+f-5)}for(b=0;b<c-1;b++)for(e=0;e<c-1;e++)if(f=0,a.isDark(b,e)&&f++,a.isDark(b+1,e)&&f++,a.isDark(b,e+1)&&f++,a.isDark(b+1,e+1)&&f++,0==f||4==f)d+=3;for(b=0;b<c;b++)for(e=0;e<c-6;e++)a.isDark(b,e)&&!a.isDark(b,e+1)&&a.isDark(b,e+
2)&&a.isDark(b,e+3)&&a.isDark(b,e+4)&&!a.isDark(b,e+5)&&a.isDark(b,e+6)&&(d+=40);for(e=0;e<c;e++)for(b=0;b<c-6;b++)a.isDark(b,e)&&!a.isDark(b+1,e)&&a.isDark(b+2,e)&&a.isDark(b+3,e)&&a.isDark(b+4,e)&&!a.isDark(b+5,e)&&a.isDark(b+6,e)&&(d+=40);for(e=f=0;e<c;e++)for(b=0;b<c;b++)a.isDark(b,e)&&f++;a=Math.abs(100*f/c/c-50)/5;return d+10*a}},l={glog:function(a){if(1>a)throw Error("glog("+a+")");return l.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;256<=a;)a-=255;return l.EXP_TABLE[a]},EXP_TABLE:Array(256),
LOG_TABLE:Array(256)},m=0;8>m;m++)l.EXP_TABLE[m]=1<<m;for(m=8;256>m;m++)l.EXP_TABLE[m]=l.EXP_TABLE[m-4]^l.EXP_TABLE[m-5]^l.EXP_TABLE[m-6]^l.EXP_TABLE[m-8];for(m=0;255>m;m++)l.LOG_TABLE[l.EXP_TABLE[m]]=m;q.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var c=Array(this.getLength()+a.getLength()-1),d=0;d<this.getLength();d++)for(var b=0;b<a.getLength();b++)c[d+b]^=l.gexp(l.glog(this.get(d))+l.glog(a.get(b)));return new q(c,0)},mod:function(a){if(0>
this.getLength()-a.getLength())return this;for(var c=l.glog(this.get(0))-l.glog(a.get(0)),d=Array(this.getLength()),b=0;b<this.getLength();b++)d[b]=this.get(b);for(b=0;b<a.getLength();b++)d[b]^=l.gexp(l.glog(a.get(b))+c);return(new q(d,0)).mod(a)}};p.RS_BLOCK_TABLE=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],
[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,
116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,
43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,
3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,
55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,
45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]];p.getRSBlocks=function(a,c){var d=p.getRsBlockTable(a,c);if(void 0==d)throw Error("bad rs block @ typeNumber:"+a+"/errorCorrectLevel:"+c);for(var b=d.length/3,e=[],f=0;f<b;f++)for(var h=d[3*f+0],g=d[3*f+1],j=d[3*f+2],l=0;l<h;l++)e.push(new p(g,j));return e};p.getRsBlockTable=function(a,c){switch(c){case 1:return p.RS_BLOCK_TABLE[4*(a-1)+0];case 0:return p.RS_BLOCK_TABLE[4*(a-1)+1];case 3:return p.RS_BLOCK_TABLE[4*
(a-1)+2];case 2:return p.RS_BLOCK_TABLE[4*(a-1)+3]}};t.prototype={get:function(a){return 1==(this.buffer[Math.floor(a/8)]>>>7-a%8&1)},put:function(a,c){for(var d=0;d<c;d++)this.putBit(1==(a>>>c-d-1&1))},getLengthInBits:function(){return this.length},putBit:function(a){var c=Math.floor(this.length/8);this.buffer.length<=c&&this.buffer.push(0);a&&(this.buffer[c]|=128>>>this.length%8);this.length++}};"string"===typeof h&&(h={text:h});h=r.extend({},{render:"canvas",width:256,height:256,typeNumber:-1,
correctLevel:2,background:"#ffffff",foreground:"#000000"},h);return this.each(function(){var a;if("canvas"==h.render){a=new o(h.typeNumber,h.correctLevel);a.addData(h.text);a.make();var c=document.createElement("canvas");c.width=h.width;c.height=h.height;for(var d=c.getContext("2d"),b=h.width/a.getModuleCount(),e=h.height/a.getModuleCount(),f=0;f<a.getModuleCount();f++)for(var i=0;i<a.getModuleCount();i++){d.fillStyle=a.isDark(f,i)?h.foreground:h.background;var g=Math.ceil((i+1)*b)-Math.floor(i*b),
j=Math.ceil((f+1)*b)-Math.floor(f*b);d.fillRect(Math.round(i*b),Math.round(f*e),g,j)}}else{a=new o(h.typeNumber,h.correctLevel);a.addData(h.text);a.make();c=r("<table></table>").css("width",h.width+"px").css("height",h.height+"px").css("border","0px").css("border-collapse","collapse").css("background-color",h.background);d=h.width/a.getModuleCount();b=h.height/a.getModuleCount();for(e=0;e<a.getModuleCount();e++){f=r("<tr></tr>").css("height",b+"px").appendTo(c);for(i=0;i<a.getModuleCount();i++)r("<td></td>").css("width",
d+"px").css("background-color",a.isDark(e,i)?h.foreground:h.background).appendTo(f)}}a=c;jQuery(a).appendTo(this)})}})(jQuery);

298
lib/m3u8-decrypt.js Normal file
View File

@@ -0,0 +1,298 @@
class AESDecryptor {
constructor() {
this.rcon = [0x0, 0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36];
this.subMix = [
new Uint32Array(256),
new Uint32Array(256),
new Uint32Array(256),
new Uint32Array(256),
];
this.invSubMix = [
new Uint32Array(256),
new Uint32Array(256),
new Uint32Array(256),
new Uint32Array(256),
];
this.sBox = new Uint32Array(256);
this.invSBox = new Uint32Array(256);
this.key = new Uint32Array(0);
this.ksRows = 0;
this.keySize = 0;
this.initTable();
}
removePadding(array) {
const outputBytes = array.byteLength;
const paddingBytes =
outputBytes && new DataView(array).getUint8(outputBytes - 1);
if (paddingBytes) {
return array.slice(0, outputBytes - paddingBytes);
}
return array;
}
// Using view.getUint32() also swaps the byte order.
uint8ArrayToUint32Array_(arrayBuffer) {
const view = new DataView(arrayBuffer);
const newArray = new Uint32Array(4);
for (let i = 0; i < 4; i++) {
newArray[i] = view.getUint32(i * 4);
}
return newArray;
}
initTable() {
const sBox = this.sBox;
const invSBox = this.invSBox;
const subMix = this.subMix;
const subMix0 = subMix[0];
const subMix1 = subMix[1];
const subMix2 = subMix[2];
const subMix3 = subMix[3];
const invSubMix = this.invSubMix;
const invSubMix0 = invSubMix[0];
const invSubMix1 = invSubMix[1];
const invSubMix2 = invSubMix[2];
const invSubMix3 = invSubMix[3];
const d = new Uint32Array(256);
let x = 0;
let xi = 0;
let i = 0;
for (i = 0; i < 256; i++) {
if (i < 128) {
d[i] = i << 1;
} else {
d[i] = (i << 1) ^ 0x11b;
}
}
for (i = 0; i < 256; i++) {
let sx = xi ^ (xi << 1) ^ (xi << 2) ^ (xi << 3) ^ (xi << 4);
sx = (sx >>> 8) ^ (sx & 0xff) ^ 0x63;
sBox[x] = sx;
invSBox[sx] = x;
// Compute multiplication
const x2 = d[x];
const x4 = d[x2];
const x8 = d[x4];
// Compute sub/invSub bytes, mix columns tables
let t = (d[sx] * 0x101) ^ (sx * 0x1010100);
subMix0[x] = (t << 24) | (t >>> 8);
subMix1[x] = (t << 16) | (t >>> 16);
subMix2[x] = (t << 8) | (t >>> 24);
subMix3[x] = t;
// Compute inv sub bytes, inv mix columns tables
t = (x8 * 0x1010101) ^ (x4 * 0x10001) ^ (x2 * 0x101) ^ (x * 0x1010100);
invSubMix0[sx] = (t << 24) | (t >>> 8);
invSubMix1[sx] = (t << 16) | (t >>> 16);
invSubMix2[sx] = (t << 8) | (t >>> 24);
invSubMix3[sx] = t;
// Compute next counter
if (!x) {
x = xi = 1;
} else {
x = x2 ^ d[d[d[x8 ^ x2]]];
xi ^= d[d[xi]];
}
}
}
expandKey(keyBuffer) {
// convert keyBuffer to Uint32Array
const key = this.uint8ArrayToUint32Array_(keyBuffer);
let sameKey = true;
let offset = 0;
while (offset < key.length && sameKey) {
sameKey = key[offset] === this.key[offset];
offset++;
}
if (sameKey) {
return;
}
this.key = key;
const keySize = (this.keySize = key.length);
if (keySize !== 4 && keySize !== 6 && keySize !== 8) {
throw new Error("Invalid aes key size=" + keySize);
}
const ksRows = (this.ksRows = (keySize + 6 + 1) * 4);
let ksRow;
let invKsRow;
const keySchedule = (this.keySchedule = new Uint32Array(ksRows));
const invKeySchedule = (this.invKeySchedule = new Uint32Array(ksRows));
const sbox = this.sBox;
const rcon = this.rcon;
const invSubMix = this.invSubMix;
const invSubMix0 = invSubMix[0];
const invSubMix1 = invSubMix[1];
const invSubMix2 = invSubMix[2];
const invSubMix3 = invSubMix[3];
let prev;
let t;
for (ksRow = 0; ksRow < ksRows; ksRow++) {
if (ksRow < keySize) {
prev = keySchedule[ksRow] = key[ksRow];
continue;
}
t = prev;
if (ksRow % keySize === 0) {
// Rot word
t = (t << 8) | (t >>> 24);
// Sub word
t =
(sbox[t >>> 24] << 24) |
(sbox[(t >>> 16) & 0xff] << 16) |
(sbox[(t >>> 8) & 0xff] << 8) |
sbox[t & 0xff];
// Mix Rcon
t ^= rcon[(ksRow / keySize) | 0] << 24;
} else if (keySize > 6 && ksRow % keySize === 4) {
// Sub word
t =
(sbox[t >>> 24] << 24) |
(sbox[(t >>> 16) & 0xff] << 16) |
(sbox[(t >>> 8) & 0xff] << 8) |
sbox[t & 0xff];
}
keySchedule[ksRow] = prev = (keySchedule[ksRow - keySize] ^ t) >>> 0;
}
for (invKsRow = 0; invKsRow < ksRows; invKsRow++) {
ksRow = ksRows - invKsRow;
if (invKsRow & 3) {
t = keySchedule[ksRow];
} else {
t = keySchedule[ksRow - 4];
}
if (invKsRow < 4 || ksRow <= 4) {
invKeySchedule[invKsRow] = t;
} else {
invKeySchedule[invKsRow] =
invSubMix0[sbox[t >>> 24]] ^
invSubMix1[sbox[(t >>> 16) & 0xff]] ^
invSubMix2[sbox[(t >>> 8) & 0xff]] ^
invSubMix3[sbox[t & 0xff]];
}
invKeySchedule[invKsRow] = invKeySchedule[invKsRow] >>> 0;
}
}
// Adding this as a method greatly improves performance.
networkToHostOrderSwap(word) {
return (
(word << 24) |
((word & 0xff00) << 8) |
((word & 0xff0000) >> 8) |
(word >>> 24)
);
}
decrypt(inputArrayBuffer, offset, aesIV, removePKCS7Padding) {
const nRounds = this.keySize + 6;
const invKeySchedule = this.invKeySchedule;
const invSBOX = this.invSBox;
const invSubMix = this.invSubMix;
const invSubMix0 = invSubMix[0];
const invSubMix1 = invSubMix[1];
const invSubMix2 = invSubMix[2];
const invSubMix3 = invSubMix[3];
const initVector = this.uint8ArrayToUint32Array_(aesIV);
let initVector0 = initVector[0];
let initVector1 = initVector[1];
let initVector2 = initVector[2];
let initVector3 = initVector[3];
const inputInt32 = new Int32Array(inputArrayBuffer);
const outputInt32 = new Int32Array(inputInt32.length);
let t0, t1, t2, t3;
let s0, s1, s2, s3;
let inputWords0, inputWords1, inputWords2, inputWords3;
let ksRow, i;
const swapWord = this.networkToHostOrderSwap;
while (offset < inputInt32.length) {
inputWords0 = swapWord(inputInt32[offset]);
inputWords1 = swapWord(inputInt32[offset + 1]);
inputWords2 = swapWord(inputInt32[offset + 2]);
inputWords3 = swapWord(inputInt32[offset + 3]);
s0 = inputWords0 ^ invKeySchedule[0];
s1 = inputWords3 ^ invKeySchedule[1];
s2 = inputWords2 ^ invKeySchedule[2];
s3 = inputWords1 ^ invKeySchedule[3];
ksRow = 4;
// Iterate through the rounds of decryption
for (i = 1; i < nRounds; i++) {
t0 =
invSubMix0[s0 >>> 24] ^
invSubMix1[(s1 >> 16) & 0xff] ^
invSubMix2[(s2 >> 8) & 0xff] ^
invSubMix3[s3 & 0xff] ^
invKeySchedule[ksRow];
t1 =
invSubMix0[s1 >>> 24] ^
invSubMix1[(s2 >> 16) & 0xff] ^
invSubMix2[(s3 >> 8) & 0xff] ^
invSubMix3[s0 & 0xff] ^
invKeySchedule[ksRow + 1];
t2 =
invSubMix0[s2 >>> 24] ^
invSubMix1[(s3 >> 16) & 0xff] ^
invSubMix2[(s0 >> 8) & 0xff] ^
invSubMix3[s1 & 0xff] ^
invKeySchedule[ksRow + 2];
t3 =
invSubMix0[s3 >>> 24] ^
invSubMix1[(s0 >> 16) & 0xff] ^
invSubMix2[(s1 >> 8) & 0xff] ^
invSubMix3[s2 & 0xff] ^
invKeySchedule[ksRow + 3];
// Update state
s0 = t0;
s1 = t1;
s2 = t2;
s3 = t3;
ksRow = ksRow + 4;
}
// Shift rows, sub bytes, add round key
t0 =
(invSBOX[s0 >>> 24] << 24) ^
(invSBOX[(s1 >> 16) & 0xff] << 16) ^
(invSBOX[(s2 >> 8) & 0xff] << 8) ^
invSBOX[s3 & 0xff] ^
invKeySchedule[ksRow];
t1 =
(invSBOX[s1 >>> 24] << 24) ^
(invSBOX[(s2 >> 16) & 0xff] << 16) ^
(invSBOX[(s3 >> 8) & 0xff] << 8) ^
invSBOX[s0 & 0xff] ^
invKeySchedule[ksRow + 1];
t2 =
(invSBOX[s2 >>> 24] << 24) ^
(invSBOX[(s3 >> 16) & 0xff] << 16) ^
(invSBOX[(s0 >> 8) & 0xff] << 8) ^
invSBOX[s1 & 0xff] ^
invKeySchedule[ksRow + 2];
t3 =
(invSBOX[s3 >>> 24] << 24) ^
(invSBOX[(s0 >> 16) & 0xff] << 16) ^
(invSBOX[(s1 >> 8) & 0xff] << 8) ^
invSBOX[s2 & 0xff] ^
invKeySchedule[ksRow + 3];
// Write
outputInt32[offset] = swapWord(t0 ^ initVector0);
outputInt32[offset + 1] = swapWord(t3 ^ initVector1);
outputInt32[offset + 2] = swapWord(t2 ^ initVector2);
outputInt32[offset + 3] = swapWord(t1 ^ initVector3);
// reset initVector to last 4 unsigned int
initVector0 = inputWords0;
initVector1 = inputWords1;
initVector2 = inputWords2;
initVector3 = inputWords3;
offset = offset + 4;
}
return removePKCS7Padding
? this.removePadding(outputInt32.buffer)
: outputInt32.buffer;
}
destroy() {
this.key = undefined;
this.keySize = undefined;
this.ksRows = undefined;
this.sBox = undefined;
this.invSBox = undefined;
this.subMix = undefined;
this.invSubMix = undefined;
this.keySchedule = undefined;
this.invKeySchedule = undefined;
this.rcon = undefined;
}
}

2
lib/mpd-parser.min.js vendored Normal file

File diff suppressed because one or more lines are too long

19
lib/mqtt.min.js vendored Normal file

File diff suppressed because one or more lines are too long

2
lib/mux.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,64 @@
lib/mux.min.js
Source: https://github.com/videojs/mux.js
License: Apache-2.0 license
Version: 7.1.0
Build:
`https://github.com/videojs/mux.js/releases/tag/v7.1.0`
`Source code` -> `npm run build` -> `dist/mux.min.js`
lib/mpd-parser.min.js
Source: https://github.com/videojs/mpd-parser
License: Apache-2.0 license
Version: 1.3.1
Download:
`https://github.com/videojs/mpd-parser/releases/tag/v1.3.1` -> `mpd-parser.min.js`
lib/jquery.qrcode.min.js
Source: https://github.com/jeromeetienne/jquery-qrcode
License: MIT license
Version: 1.0
Download:
https://github.com/jeromeetienne/jquery-qrcode/releases/tag/v1.0
`Source code` -> `jquery.qrcode.min.js`
lib/jquery.min.js
Source: https://github.com/jquery/jquery
License: MIT license
Version: 3.7.1
Download:
`https://jquery.com/download/` -> `https://code.jquery.com/jquery-3.7.1.min.js`
lib/hls.min.js
Source: https://github.com/video-dev/hls.js
License: Apache-2.0 license
Version: 1.6.13
Download:
`https://github.com/video-dev/hls.js/releases/tag/v1.6.13` -> `release.zip` -> `dist/hls.min.js`
lib/mqtt.min.js
Source: https://github.com/mqttjs/MQTT.js
License: MIT license
Version: 5.14.1
Build:
`https://github.com/mqttjs/MQTT.js/releases/tag/v5.14.1`
`Source code` -> `npm run build` -> `dist/mqtt.min.js`
lib/jquery.json-viewer.js
Source: https://github.com/abodelot/jquery.json-viewer
License: MIT license
Version: 1.5.0
Download:
`https://github.com/abodelot/jquery.json-viewer/releases/tag/v1.5.0`
`Source code` -> `json-viewer/jquery.json-viewer.js`
lib/StreamSaver.js
Source: https://github.com/jimmywarting/StreamSaver.js
License: MIT license
Version: 2.0.6
Download:
`https://github.com/jimmywarting/StreamSaver.js/releases/tag/2.0.6`
`Source code` -> `StreamSaver.js`
lib/m3u8-decrypt.js
Source: https://github.com/video-dev/hls.js/blob/master/src/crypt/aes-decryptor.ts
License: Apache-2.0 license

195
m3u8.html Normal file
View File

@@ -0,0 +1,195 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>titleM3U8</title>
<link rel="stylesheet" type="text/css" href="css/public.css" media="all" />
<link rel="stylesheet" type="text/css" href="css/options.css" media="all" />
<script src="lib/jquery.min.js"></script>
<script src="js/init.js"></script>
<script src="js/firefox.js"></script>
<script src="lib/mux.min.js"></script>
<script src="js/function.js"></script>
</head>
<body>
<div class="m3u8_wrapper wrapper1080">
<section id="loading">
<div class="optionBox" data-i18n="loading"></div>
</section>
<section id="m3u8Custom" class="hide">
<h1 class="optionsTitle" data-i18n="titleM3U8"></h1>
<div class="optionBox">
<textarea id="m3u8Text" spellcheck="false" data-type="link" data-i18n-placeholder="m3u8Placeholder"
class="width100"></textarea>
<input type="text" id="baseUrl" placeholder="BaseURL" class="fullInput" />
<div class="line"></div>
<input type="text" id="referer" placeholder="Referer" class="fullInput" />
<button id="parse" type="button" data-i18n="parser"></button>
<input id="uploadM3U8" type="file" />
</div>
</section>
<section id="more_m3u8" class="hide">
<h1 class="optionsTitle" data-i18n="selectVideo"></h1>
<span class="explain" data-i18n="nextLevelTip"></span>
<div class="optionBox">
<div id="next_m3u8"></div>
</div>
</section>
<section id="more_audio" class="hide">
<h1 class="optionsTitle" data-i18n="selectAudio"></h1>
<span class="explain" data-i18n="multipleAudiosTip"></span>
<div class="optionBox">
<div id="next_audio"></div>
</div>
</section>
<section id="more_subtitle" class="hide">
<h1 class="optionsTitle" data-i18n="multipleSubtitles"></h1>
<span class="explain" data-i18n="multipleSubtitlesTip"></span>
<div class="optionBox">
<div id="next_subtitle"></div>
</div>
</section>
<section id="more_options" class="hide">
<div class="optionBox">
<button id="more_options_merge" class="button2" data-i18n="onlineMerge"></button>
</div>
</section>
<section id="m3u8" class="hide">
<h1 class="optionsTitle" data-i18n="titleM3U8"></h1>
<span class="explain" id="key"></span>
<div class="optionBox">
<div class="block">
<p data-i18n-outer="m3u8Url"></p>
<p><a id="m3u8_url"></a></p>
</div>
<div class="block" id="tips"></div>
<div class="block hide" id="maybeKey">
<select class="m3u8Key select">
<option value="tips" data-i18n="possibleKey"></option>
</select>
<button id="searchingForRealKey" data-i18n="searchingForRealKey"></button>
</div>
<div class="videoInfo flex">
<div id="count"></div>
<div id="info"></div>
<div id="estimateFileSize"></div>
</div>
<div class="block" id="textarea">
<details>
<summary data-i18n="viewSlices" class="button"></summary>
<textarea id="media_file" spellcheck="false" data-type="link" class="width100"
data-i18n="loading"></textarea>
<div id="downList" data-i18n="waitDownload"></div>
<div class="merge">
<button id="downText" data-i18n="downloadSegmentList"></button>
<button id="originalM3U8" data-i18n="originalM3u8"></button>
<button id="localFile" class="hide" data-i18n="localM3u8"></button>
<button id="downProgress" data-i18n="downloadProgress"></button>
<button id="getTs" data-i18n="segmentList"></button>
</div>
</details>
</div>
<video id="video" class="hide" controls></video>
<div class="block" id="button">
<div class="merge">
<button id="tsAddArg" data-i18n="getParameters"></button>
<button id="setRequestHeaders" data-i18n="requestHeaders"></button>
<button id="play" data-switch="on" data-i18n="play"></button>
<button id="m3u8DL" class="button2" data-i18n="invokeM3u8DL"></button>
<button id="copyM3U8dl" class="hide" data-i18n="copyCommand"></button>
<button id="setM3u8dl" data-i18n="previewCommand"></button>
<label class="m3u8checkbox textColor">
<p data-i18n-outer="addSettingParameters"></p><input type="checkbox" id="addParam" save="change" />
</label>
<button id="invoke" class="button2" data-i18n="invoke"></button>
<button class="openDir hide" data-i18n="downloadDir"></button>
<button id="sendFfmpeg" class="hide button" data-i18n="sendFfmpeg"></button>
</div>
<textarea id="m3u8dlArg" type="text" class="m3u8dlArg hide"></textarea>
<div class="line"></div>
<div class="merge customKey">
<input type="text" id="customFilename" spellcheck="false" data-i18n-placeholder="customSaveFileName"
size="30" />
<input type="text" id="customKey" spellcheck="false" data-i18n-placeholder="customKeyPlaceholder"
size="60" />
<input type="text" id="customIV" spellcheck="false" placeholder="IV" size="30" />
<button id="uploadKey" data-i18n="uploadKey"></button>
<input id="uploadKeyFile" type="file" class="hide" />
</div>
<div class="merge" id="mergeDown">
<div id="threadDiv" class="textColor">
<p data-i18n-outer="downloadThreads"></p> <input type="number" value="6" id="thread" spellcheck="false"
style="width: 35px;" step="1" min="1" max="256" save="change" />
</div>
<label class="m3u8checkbox textColor">
<p data-i18n-outer="ffmpegTranscoding"></p><input type="checkbox" id="ffmpeg" save="change" />
</label>
<label class="m3u8checkbox textColor">
<p data-i18n-outer="mp4Format"></p><input type="checkbox" id="mp4" save="change" />
</label>
<label class="m3u8checkbox textColor firefoxHide">
<p data-i18n-outer="downloadWhileSaving"></p><input type="checkbox" id="StreamSaver" save="change" />
</label>
<label class="m3u8checkbox textColor">
<p data-i18n-outer="audioOnly"></p><input type="checkbox" id="onlyAudio" save="change" />
</label>
<label class="m3u8checkbox textColor">
<p data-i18n-outer="saveAs"></p><input type="checkbox" id="saveAs" save="change" />
</label>
<label class="m3u8checkbox textColor">
<p data-i18n-outer="skipDecryption"></p><input type="checkbox" id="skipDecrypt" save="change" />
</label>
<label class="m3u8checkbox textColor">
<p data-i18n-outer="autoClosePageAfterDownload"></p><input type="checkbox" id="autoClose" save="change" />
</label>
<div class="rangeDown textColor">
<p data-i18n-outer="downloadRange"></p>
<select id="cc" class="hide">
<option disabled selected hidden>playlist</option>
</select>
<div class="merge">
<input type="text" id="rangeStart" spellcheck="false" data-i18n-placeholder="start" value="1" />
<input type="text" id="rangeEnd" spellcheck="false" data-i18n-placeholder="end" />
</div>
</div>
<button id="recorder" class="button2 hide" data-switch="on" data-switch="on"
data-i18n="recordLive"></button>
<div class="textColor m3u8checkbox">
<p data-i18n-outer="retryCount"></p>(test) <input type="number" value="0" id="retryCount"
spellcheck="false" style="width: 35px;" step="1" min="0" />
</div>
<button id="mergeTs" class="button2" data-i18n="mergeDownloads"></button>
<button id="errorDownload" class="hide button2" data-i18n="redownloadFailedItems"></button>
<button id="ForceDownload" class="hide button2" data-i18n="downloadExistingData"></button>
<button id="stopDownload" class="hide button2" data-i18n="stopDownload"></button>
</div>
<div style="display: flex; margin-top: 10px;">
<div id="progress"></div>
<div id="fileSize"></div>
<div id="fileDuration"></div>
</div>
<div class="block hide" id="errorTsList"></div>
</div>
</div>
</section>
</div>
<script src="lib/m3u8-decrypt.js"></script>
<script src="lib/hls.min.js"></script>
<script src="lib/base64.js"></script>
<script src="lib/StreamSaver.js"></script>
<script src="js/m3u8.downloader.js"></script>
<script src="js/m3u8.js"></script>
<script src="js/i18n.js"></script>
</body>
</html>

84
manifest.firefox.json Normal file
View File

@@ -0,0 +1,84 @@
{
"background": {
"scripts": [
"js/firefox.js",
"js/background.js"
]
},
"action": {
"default_icon": "img/icon.png",
"default_title": "__MSG_catCatch__",
"default_popup": "popup.html"
},
"description": "__MSG_description__",
"icons": {
"64": "img/icon.png",
"128": "img/icon128.png"
},
"manifest_version": 3,
"name": "__MSG_catCatch__",
"homepage_url": "https://github.com/xifangczy/cat-catch",
"options_ui": {
"page": "options.html",
"open_in_tab": true
},
"permissions": [
"tabs",
"webRequest",
"downloads",
"storage",
"webNavigation",
"alarms",
"scripting",
"declarativeNetRequest"
],
"commands": {
"_execute_browser_action": {},
"enable": {
"description": "__MSG_pause__ / __MSG_enable__"
},
"auto_down": {
"description": "__MSG_autoDownload__"
},
"catch": {
"description": "__MSG_cacheCapture__"
},
"m3u8": {
"description": "__MSG_m3u8Parser__"
},
"clear": {
"description": "__MSG_clear__"
},
"reboot": {
"description": "__MSG_restartExtension__"
},
"deepSearch": {
"description": "__MSG_deepSearch__"
}
},
"browser_specific_settings": {
"gecko": {
"id": "xifangczy@gmail.com",
"strict_min_version": "113.0"
}
},
"host_permissions": [
"*://*/*",
"<all_urls>"
],
"content_scripts": [
{
"matches": [
"https://*/*",
"http://*/*"
],
"js": [
"js/content-script.js"
],
"all_frames": true,
"run_at": "document_start"
}
],
"default_locale": "en",
"version": "2.6.4"
}

81
manifest.json Normal file
View File

@@ -0,0 +1,81 @@
{
"background": {
"service_worker": "js/background.js"
},
"action": {
"default_icon": "img/icon.png",
"default_title": "__MSG_catCatch__",
"default_popup": "popup.html"
},
"side_panel": {
"default_path": "popup.html"
},
"description": "__MSG_description__",
"icons": {
"64": "img/icon.png",
"128": "img/icon128.png"
},
"manifest_version": 3,
"minimum_chrome_version": "93",
"name": "__MSG_catCatch__",
"homepage_url": "https://github.com/xifangczy/cat-catch",
"options_ui": {
"page": "options.html",
"open_in_tab": true
},
"permissions": [
"tabs",
"webRequest",
"downloads",
"storage",
"webNavigation",
"alarms",
"declarativeNetRequest",
"scripting",
"sidePanel"
],
"commands": {
"_execute_action": {},
"enable": {
"description": "__MSG_pause__ / __MSG_enable__"
},
"auto_down": {
"description": "__MSG_autoDownload__"
},
"catch": {
"description": "__MSG_cacheCapture__"
},
"m3u8": {
"description": "__MSG_m3u8Parser__"
},
"clear": {
"description": "__MSG_clear__"
},
"reboot": {
"description": "__MSG_restartExtension__"
},
"deepSearch": {
"description": "__MSG_deepSearch__"
}
},
"host_permissions": [
"*://*/*",
"<all_urls>"
],
"content_scripts": [
{
"matches": [
"https://*/*",
"http://*/*"
],
"js": [
"js/content-script.js"
],
"run_at": "document_start",
"all_frames": true
}
],
"default_locale": "en",
"version": "2.6.4",
"incognito": "split"
}

72
mpd.html Normal file
View File

@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>titledash</title>
<link rel="stylesheet" type="text/css" href="css/public.css" media="all" />
<link rel="stylesheet" type="text/css" href="css/options.css" media="all" />
<script src="lib/jquery.min.js"></script>
<script src="js/init.js"></script>
<script src="js/firefox.js"></script>
<script src="lib/mpd-parser.min.js"></script>
<script src="js/function.js"></script>
<script src="js/mpd.js"></script>
</head>
<body>
<div class="wrapper1024 dash">
<section id="loading">
<div class="optionBox" data-i18n="loading"></div>
</section>
<section id="mpdCustom" class="hide">
<h1 class="optionsTitle" data-i18n="titledash"></h1>
<div class="optionBox">
<input type="text" id="mpdUrl" placeholder="mpd URL" class="fullInput" />
<input type="text" id="referer" placeholder="Referer" class="fullInput" />
<button id="parse" type="button" data-i18n="parser"></button>
</div>
</section>
<section id="main" class="hide">
<h1 class="optionsTitle" data-i18n="titledash"></h1>
<div class="optionBox">
<div class="block">
mpd url
<p><a id="mpd_url"></a></p>
</div>
<div class="block" id="tips"></div>
<div class="videoInfo flex">
<div id="count"></div>
<div id="info"></div>
</div>
<div class="block" id="textarea">
<textarea id="media_file" spellcheck="false" data-type="link" class="width100"></textarea>
</div>
<div class="flex">
<div>
<p data-i18n-outer="selectVideo"></p>: <select id="mpdVideoLists" class="select"></select>
</div>
<button id="getVideo" data-i18n="extractSlices"></button>
<button id="videoToM3u8" data-i18n="convertToM3U8"></button>
</div>
<div class="line"></div>
<div class="flex">
<div>
<p data-i18n-outer="selectAudio"></p>: <select id="mpdAudioLists" class="select"></select>
</div>
<button id="getAudio" data-i18n="extractSlices"></button>
<button id="audioToM3u8" data-i18n="convertToM3U8"></button>
</div>
<div style="display: flex; margin-top: 10px;">
<div id="progress"></div>
<div id="fileSize"></div>
</div>
</div>
</section>
</div>
<script src="js/i18n.js"></script>
</body>
</html>

915
options.html Normal file
View File

@@ -0,0 +1,915 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>titleOption</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="css/public.css" media="all" />
<link rel="stylesheet" type="text/css" href="css/options.css" media="all" />
<script src="lib/jquery.min.js" defer></script>
<script src="js/init.js" defer></script>
<script src="js/function.js" defer></script>
</head>
<body>
<!-- 添加导航栏 -->
<nav class="sidebar" aria-label="Main Navigation">
<ul>
<li><a href="#anchorSuffix" data-i18n="suffix" title="suffix"></a></li>
<li><a href="#anchorType" data-i18n="type" title="type"></a></li>
<li><a href="#anchorRegexMatch" data-i18n="regexMatch" title="regexMatch"></a></li>
<li><a href="#anchorBlockUrl" data-i18n="blockUrl" title="blockUrl"></a></li>
<li><a href="#anchorScriptSettings" data-i18n="Script" title="Script"></a></li>
<li><a href="#anchorCopy" data-i18n="copy" title="copy"></a></li>
<li><a href="#anchorAria2Rpc" title="Aria2 RPC">Aria2 RPC</a></li>
<li><a href="#anchorSend2local" data-i18n="send2local" title="send2local"></a></li>
<li><a href="#anchorM3u8dl" title="URL Protocol m3u8dl">URL Protocol m3u8dl</a></li>
<li><a href="#anchorInvokeApp" data-i18n="invokeApp" title="invokeApp"></a></li>
<li><a href="#anchorReplaceTags" data-i18n="replaceTags" title="replaceTags"></a></li>
<li><a href="#anchorDownloader" data-i18n="downloader" title="downloader"></a></li>
<li><a href="#anchorM3u8Parser" data-i18n="m3u8Parser" title="m3u8Parser"></a></li>
<li><a href="#anchorOtherSettings" data-i18n="otherSettings" title="otherSettings"></a></li>
<li><a href="#anchorCustomCSS" data-i18n="customCSS" title="customCSS"></a></li>
<li><a href="#anchorMQTT" data-i18n="MQTT" title="MQTT"></a></li>
<li><a href="#anchorOperation" data-i18n="operation" title="operation"></a></li>
<li><a href="#anchorAbout" data-i18n="about" title="about"></a></li>
</ul>
</nav>
<div class="wrapper options">
<!-- 抓取后缀 -->
<section id="anchorSuffix">
<h1 class="optionsTitle" data-i18n="suffix"></h1>
<div class="optionBox">
<span class="explain" data-i18n="suffixTip"></span>
<table id="extList">
<tr>
<th data-i18n="suffix"></th>
<th data-i18n="filterSize"></th>
<th data-i18n="enable"></th>
<th data-i18n="delete"></th>
</tr>
</table>
<div class="flex-end">
<button type="button" id="AddExt" class="button2" data-i18n="addSuffix" title="add Suffix"></button>
<button type="button" id="ResetExt" data-reset="Ext" data-i18n="resetSettings" title="reset Suffixs"></button>
<button type="button" id="allDisable" data-switch="Ext" data-i18n="disableAll"
title="disable All Ext"></button>
<button type="button" id="allEnable" data-switch="Ext" data-i18n="enableAll" title="enable All Ext"></button>
</div>
</div>
</section>
<!-- 抓取类型 -->
<section id="anchorType">
<h1 class="optionsTitle" data-i18n="type"></h1>
<div class="optionBox">
<span class="explain" data-i18n="typeTip"></span>
<table id="typeList">
<tr>
<th data-i18n="type"></th>
<th data-i18n="filterSize"></th>
<th data-i18n="enable"></th>
<th data-i18n="delete"></th>
</tr>
</table>
<div class="flex-end">
<button type="button" id="AddType" class="button2" data-i18n="addType" title="add Type"></button>
<button type="button" id="ResetType" data-reset="Type" data-i18n="resetSettings" title="reset Types"></button>
<button type="button" id="allDisable" data-switch="Type" data-i18n="disableAll"
title="disable All Types"></button>
<button type="button" id="allEnable" data-switch="Type" data-i18n="enableAll"
title="enable All Types"></button>
</div>
</div>
</section>
<!-- 正则匹配 -->
<section id="anchorRegexMatch">
<h1 class="optionsTitle">
<img src="img/regex.png" style="width: 18px" class="regex" /><span data-i18n-outer="regexMatch"></span> / <span
data-i18n-outer="blockResource"></span>
</h1>
<div class="optionBox">
<span class="explain"><b data-i18n="blockResource"></b>
<p data-i18n-outer="blockResourceTip"></p>
</span><br>
<span class="explain"><b data-i18n="flag"></b>
</span><br>
<span class="explain"><b data-i18n="suffix"></b>
<p data-i18n-outer="regexSuffixTip"></p>
</span><br>
<span class="explain"><b data-i18n="regexTip"></b></span><br><br>
<table id="regexList">
<tr>
<th data-i18n="flag"></th>
<th data-i18n="regexExpression"></th>
<th data-i18n="suffix"></th>
<th data-i18n="blockResource"></th>
<th data-i18n="enable"></th>
<th data-i18n="delete"></th>
</tr>
</table>
<div class="flex-end">
<button type="button" id="AddRegex" class="button2" data-i18n="addRegex" title="add Regex"></button>
<button type="button" id="ResetRegex" data-reset="Regex" data-i18n="resetSettings"
title="reset Regex"></button>
<button type="button" id="allDisable" data-switch="Regex" data-i18n="disableAll"
title="disable All Regex"></button>
<button type="button" id="allEnable" data-switch="Regex" data-i18n="enableAll"
title="enable All Regex"></button>
</div>
<span style="font-weight: bold; font-size: 15px" data-i18n="regexTest"></span><br />
<label for="testUrl">URL</label><br />
<input type="text" id="testUrl" style="width: 590px" /><br />
<label for="testRegex" data-i18n="regex"></label><br />
<input type="text" id="testRegex" style="width: 590px" /><br />
<label for="testFlag" data-i18n="flag"></label><br />
<input type="text" id="testFlag" style="width: 20px" value="ig" maxlength="4" />
<span style="color: #ff0000" id="testResult" data-i18n="noMatch"></span>
</div>
</section>
<!-- 屏蔽网站 -->
<section id="anchorBlockUrl">
<h1 class="optionsTitle" data-i18n="blockUrl"></h1>
<div class="optionBox">
<div class="item">
<div data-i18n="setWhiteList"></div>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" id="blockUrlWhite" save="click" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
<span class="explain" data-i18n="blockUrlTips"></span>
<table id="blockUrlList">
<tr>
<th>URL</th>
<th data-i18n="enable"></th>
<th data-i18n="delete"></th>
</tr>
</table>
<div class="flex-end">
<button type="button" id="blockAddUrl" class="button2" data-i18n="addUrl" title="add Url"></button>
<button type="button" id="ResetBlockUrl" data-reset="blockUrl" data-i18n="resetSettings"
title="reset block Url"></button>
<button type="button" id="allDisable" data-switch="blockUrl" data-i18n="disableAll"
title="disable All block Url"></button>
<button type="button" id="allEnable" data-switch="blockUrl" data-i18n="enableAll"
title="enable All block Url"></button>
</div>
</div>
</section>
<!-- 脚本设置 -->
<section id="anchorScriptSettings">
<h1 class="optionsTitle" data-i18n="Script"></h1>
<div class="optionBox" id="scriptOption">
<div class="list loose">
<div class="item">
<div data-i18n="alwaysSearch"></div>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" save="click" id="deepSearch" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
</div>
<div class="flex-end">
<button type="button" class="resetOption" data-i18n="resetSettings"></button>
</div>
</div>
</section>
<!-- 复制选项 -->
<section id="anchorCopy">
<h1 class="optionsTitle">
<img src="img/copy.png" style="width: 18px" class="copy" alt="Copy icon" />
<p data-i18n-outer="copy"></p>
</h1>
<div class="optionBox">
<span class="explain">
<p data-i18n-outer="copyTip"></p><br />
<a href="https://cat-catch.bmmmd.com/docs/tag" target="_blank" data-i18n="replaceKeywordList"></a><br /><br />
</span>
<div class="item">
<div>HLS m3u8</div>
<textarea id="copyM3U8" save="input" type="text" class="width100"></textarea>
</div>
<div class="item" style="margin-top: 10px;">
<div>DASH mpb</div>
<textarea id="copyMPD" save="input" type="text" class="width100"></textarea>
</div>
<div class="item" style="margin-top: 10px;">
<div data-i18n="otherFiles"></div>
<textarea id="copyOther" save="input" type="text" class="width100"></textarea>
</div>
<div class="flex-end">
<button type="button" class="resetOption" data-i18n="resetSettings"></button>
</div>
</div>
</section>
<!-- Aria2 RPC -->
<section id="anchorAria2Rpc">
<h1 class="optionsTitle">
<img src="img/aria2.png" style="width: 18px" class="aria2" />
Aria2 RPC
</h1>
<div class="optionBox">
<span class="explain">
<p data-i18n-outer="aria2Tip"></p>
<a href="https://aria2.github.io/manual/en/html/aria2c.html#rpc-interface" target="_blank"
data-i18n="documentation"></a>
</span>
<div class="list">
<div class="item">
<div><span data-i18n-outer="enable"></span> Aria2 RPC</div>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" id="enableAria2Rpc" save="click" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
<div class="item">
<div data-i18n="autoSetRefererCookieParams"></div>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" id="enableAria2RpcReferer" save="click" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
<div class="item">
<div data-i18n="secretKey"></div>
<input id="aria2RpcToken" save="input" type="password" class="width100">
</div>
<div class="item">
<div>Aria2 RPC <span data-i18n-outer="address"></span></div>
<input id="aria2Rpc" save="input" type="text" class="width100">
</div>
<div class="flex-end">
<button type="button" class="resetOption" data-i18n="resetSettings"></button>
</div>
</div>
</div>
</section>
<!-- 发送数据 -->
<section id="anchorSend2local">
<h1 class="optionsTitle">
<img src="img/send.svg" style="width: 18px" class="regex" alt="Send icon" />
<p data-i18n-outer="send2local"></p>
</h1>
<div class="optionBox">
<span class="explain">
<a href="https://cat-catch.bmmmd.com/docs/settings#send" target="_blank" data-i18n="documentation"></a>
</span>
<div class="list">
<div class="item">
<div><span data-i18n-outer="autoSend"></span></div>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" id="send2local" save="click" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
<div class="item">
<div><span data-i18n="manualSend"></span></div>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" id="send2localManual" save="click" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
<div class="item">
<div><span data-i18n-outer="requestMethod"></span> </div>
<div class="switch switchSelect">
<select id="send2localMethod" class="select" save="select">
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
</select>
</div>
</div>
<div class="item">
<div><span>Content-Type</span> </div>
<div class="switch switchSelect send2localType">
<select id="send2localType" class="select" save="select">
<option value="0">application/json;charset=utf-8</option>
<option value="1">multipart/form-data</option>
<option value="2">application/x-www-form-urlencoded</option>
<option value="3">text/plain</option>
</select>
</div>
</div>
<div class="item">
<div><span data-i18n-outer="address"></span></div>
<input id="send2localURL" save="input" type="text" class="width100">
</div>
<div class="item">
<div>
<p data-i18n="requestBody"></p>
<a href="https://cat-catch.bmmmd.com/docs/tag" target="_blank" data-i18n="replaceKeywordList"></a>
</div>
<textarea id="send2localBody" save="input" class="width100 break-all" rows="3"></textarea>
</div>
<div class="flex-end">
<button type="button" class="resetOption" data-i18n="resetSettings"></button>
</div>
</div>
</div>
</section>
<!-- m3u8DL -->
<section id="anchorM3u8dl">
<h1 class="optionsTitle">URL Protocol m3u8dl</h1>
<div class="optionBox" id="m3u8dlOption">
<span class="explain">
<a href="https://github.com/nilaoda/N_m3u8DL-CLI" target="_blank">N_m3u8DL-CLI</a> / <a
href="https://github.com/nilaoda/N_m3u8DL-RE" target="_blank">N_m3u8DL-RE</a>
<p data-i18n-outer="m3u8DLTips"></p>
<a href="https://cat-catch.bmmmd.com/docs/m3u8dl" target="_blank" data-i18n="documentation"></a>
</span>
<div class="list">
<div class="item">
<div><span data-i18n-outer="enable"></span> m3u8dl:// <span data-i18n-outer="download"></span> m3u8 or mpd
</div>
<div class="switch m3u8DL">
<select id="m3u8dl" class="select" save="select">
<option value="0" data-i18n="disable"></option>
<option value="1">N_m3u8DL-CLI</option>
<option value="2">N_m3u8DL-RE</option>
</select>
</div>
</div>
<div class="item">
<div data-i18n="confirmParameters"></div>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" id="m3u8dlConfirm" save="click" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
<!-- <div class="item"></div> -->
<div class="item">
<div style="margin-bottom: 5px;margin-top: 5px;">
<p data-i18n="parameterSetting"></p>
<a href="https://cat-catch.bmmmd.com/docs/settings#keywords" target="_blank"
data-i18n="replaceKeywordList"></a>
<a href="https://nilaoda.github.io/N_m3u8DL-CLI/Advanced.html" target="_blank"
style="margin-left: 10px;">N_m3u8DL-CLI <span data-i18n-outer="parameter"></span></a>
<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"
target="_blank" style="margin-left: 10px;">N_m3u8DL-RE <span data-i18n-outer="parameter"></span></a>
</div>
<textarea id="m3u8dlArg" save="input" type="text" class="width100 break-all" rows="3"></textarea>
</div>
<div class="flex-end">
<button type="button" class="resetOption" data-i18n="resetSettings"></button>
</div>
</div>
</div>
</section>
<!-- 第三方本地程序调用 -->
<section id="anchorInvokeApp">
<h1 class="optionsTitle">
<img src="img/invoke.svg" style="width: 18px" class="invoke" />
<p data-i18n-outer="invokeApp"></p>
</h1>
<div class="optionBox" id="invokeOption">
<div class="list">
<div class="item">
<div data-i18n="enable"></div>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" id="invoke" save="click" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
<div class="item">
<div data-i18n="confirmParameters"></div>
<br>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" id="invokeConfirm" save="click" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
<div class="item" style="margin-bottom: 5px;margin-top: 5px;">
<div>
<p data-i18n="parameterSetting"></p>
<a href="https://cat-catch.bmmmd.com/docs/tag" target="_blank" data-i18n="replaceKeywordList"></a>
</div>
<textarea id="invokeText" save="input" type="text" class="width100 break-all" rows="3"></textarea>
</div>
<div class="flex-end">
<button type="button" class="resetOption" data-i18n="resetSettings"></button>
</div>
</div>
</div>
</section>
<!-- 替换标签 -->
<section id="anchorReplaceTags">
<h1 class="optionsTitle" data-i18n="replaceTags"></h1>
<div class="optionBox" id="tag">
<span class="explain"></span>
<div style="margin-bottom: 5px;">
<a href="https://cat-catch.bmmmd.com/docs/settings#keywords" target="_blank"
data-i18n="replaceKeywordList"></a>
</div>
<div class="list">
<div class="item">
<div data-i18n="customSaveFileName"></div>
<textarea id="downFileName" save="input" type="text" class="width100"></textarea>
</div>
<div class="item">
<div>${userAgent} <span data-i18n-outer="userAgentTip"></span></div>
<textarea id="userAgent" save="input" type="text" class="width100"></textarea>
</div>
<div class="item">
<div>${mobileUserAgent} / <span data-i18n-outer="simulateMobile"></span>User Agent</div>
<textarea id="MobileUserAgent" save="input" type="text" class="width100"></textarea>
</div>
<div id="testTag" class="hide break-all">
<div class="item">
<div>
<p data-i18n="test"></p>
</div>
<textarea id="testTextarea" class="width100 break-all"
rows="3">${url} ${referer|exists:'--headers "Referer:*"'} ${url|regexp:"(https?://[^?]*)"|replace:"http://","https://"|to:base64}</textarea>
</div>
<label for="url">${url}</label><input type="text" class="width100" value="https://bmmmd.com/test.m3u8"
id="url">
<label for="referer">${referer}</label><input type="text" class="width100" value="https://bmmmd.com/"
id="referer">
<label for="initiator">${initiator}</label><input type="text" class="width100" value="https://bmmmd.com"
id="initiator">
<label for="webUrl">${webUrl}</label><input type="text" class="width100" value="https://bmmmd.com/test.html"
id="webUrl">
<label for="title">${title}</label><input type="text" class="width100" value="test Video" id="title">
<span data-i18n-outer="result"></span>:<br><span id="tagTestResult"></span>
</div>
<div class="flex-end">
<button type="button" id="showTestTag" data-i18n="test"></button>
<button type="button" class="resetOption" data-i18n="resetSettings"></button>
</div>
</div>
</div>
</section>
<!-- 下载器 -->
<section id="anchorDownloader">
<h1 class="optionsTitle">
<img src="img/cat-down.png" style="width: 18px" class="cat-down" alt="Downloader icon" />
<p data-i18n-outer="downloader"></p>
</h1>
<div class="optionBox" id="downOption">
<span class="explain" data-i18n="downloaderTip"></span>
<div class="list">
<div class="item">
<div data-i18n="alwaysDisableCatCatcher"></div>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" id="catDownload" save="click" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
<div class="item">
<div data-i18n="autoClosePageAfterDownload"></div>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" id="downAutoClose" save="click" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
<div class="item">
<div data-i18n="openDownloaderPageInBackground"></div>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" id="downActive" save="click" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
<div class="item">
<div data-i18n="downloadWhileSaving"></div>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" id="downStream" save="click" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
<div class="flex-end">
<button type="button" class="resetOption" data-i18n="resetSettings"></button>
</div>
</div>
</div>
</section>
<!-- m3u8解析器 -->
<section id="anchorM3u8Parser">
<h1 class="optionsTitle">
<img src="img/parsing.png" style="width: 18px" class="parsing" alt="Parser icon" />
<p data-i18n-outer="m3u8Parser"></p>
</h1>
<div class="optionBox" id="m3u8Option">
<div class="list">
<div class="item">
<div><img src="img/download.svg" style="width: 18px" class="download"> <span
data-i18n-outer="autoDownM3u8Tip"></span></div>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" id="m3u8AutoDown" save="click" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
<div class="item">
<div data-i18n="downloadThreads"></div>
<div class="switch">
<input id="M3u8Thread" save="input" type="number" min="1" max="32" class="width3rem">
</div>
</div>
<div class="item">
<div data-i18n="mp4Format"></div>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" id="M3u8Mp4" save="click" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
<div class="item">
<div data-i18n="audioOnly"></div>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" id="M3u8OnlyAudio" save="click" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
<div class="item">
<div data-i18n="skipDecryption"></div>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" id="M3u8SkipDecrypt" save="click" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
<div class="item">
<div data-i18n="downloadWhileSaving"></div>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" id="M3u8StreamSaver" save="click" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
<div class="item">
<div data-i18n="ffmpegTranscoding"></div>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" id="M3u8Ffmpeg" save="click" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
<div class="item">
<div data-i18n="autoClosePageAfterDownload"></div>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" id="M3u8AutoClose" save="click" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
<div class="flex-end">
<button type="button" class="resetOption" data-i18n="resetSettings"></button>
</div>
</div>
</div>
</section>
<!-- 其他设置 -->
<section id="anchorOtherSettings">
<h1 class="optionsTitle" data-i18n="otherSettings"></h1>
<div class="optionBox" id="OtherOption">
<div class="list loose">
<div class="item">
<div>
<span data-i18n-outer="previewMode"></span> <select id="PlayerTemplate" class="select"></select>
</div>
<input id="Player" save="input" type="text" class="width100"
data-i18n-placeholder="previewModePlaceholder" />
</div>
<div class="item">
<div data-i18n="customFilenameOption"></div>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" save="click" id="TitleName" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
<div class="item">
<div data-i18n="saveAsOption"></div>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" id="saveAs" save="click" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
<div class="item">
<div data-i18n="badgeNumber"></div>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" id="badgeNumber" save="click" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
<div class="item">
<div data-i18n="iconOption"></div>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" save="click" id="ShowWebIco" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
<div class="item">
<div data-i18n="clearOption"></div>
<div class="switch switchSelect">
<select id="autoClearMode" class="select" save="select">
<option value="0" data-i18n="doNotClear"></option>
<option value="1" data-i18n="normalClear"></option>
<option value="2" data-i18n="moreFrequent"></option>
</select>
</div>
</div>
<div class="item">
<div data-i18n="excludeDuplicateResources"></div>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" id="checkDuplicates" save="click" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
<div class="item">
<div data-i18n="onlineServiceAddress"></div>
<div class="switch switchSelect">
<select id="onlineServiceAddress" class="select" save="select">
<option value="0" data-i18n="withinChina"></option>
<option value="1">cloudflare</option>
</select>
</div>
</div>
<div class="item">
<div data-i18n="defaultPopup"></div>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" id="popup" save="click" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
<div class="item">
<div data-i18n="selectWebpage" style="margin-left: 2rem;"></div>
<div class="switch switchSelect">
<select id="popupMode" class="select" save="select">
<option value="0">preview</option>
<option value="1">popup</option>
<option value="2">window preview</option>
<option value="3">window popup</option>
</select>
</div>
</div>
<div class="item">
<div data-i18n="useSidePanel"></div>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" id="sidePanel" save="click" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
</div>
<div class="flex-end">
<button type="button" class="resetOption" data-i18n="resetSettings"></button>
</div>
</div>
</section>
<!-- 自定义css -->
<section id="anchorCustomCSS">
<h1 class="optionsTitle" data-i18n="customCSS"></h1>
<div class="optionBox">
<div class="item">
<textarea id="css" save="input" type="text" class="width100" rows="10"></textarea>
</div>
</div>
</section>
<!-- MQTT Configuration -->
<section id="anchorMQTT">
<h1 class="optionsTitle">
<img src="img/mqtt.svg" style="width: 18px" class="mqtt" />
<i18n>MQTT</i18n>
</h1>
<div class="optionBox" id="mqttOption">
<div class="list loose">
<div class="item">
<div data-i18n="enable"></div>
<div class="switch">
<label class="switchLabel switchRadius">
<input type="checkbox" id="mqttEnable" save="click" class="switchInput" />
<span class="switchRound switchRadius"><em class="switchRoundBtn switchRadius"></em></span>
</label>
</div>
</div>
<div class="item">
<div data-i18n="mqttBroker"></div>
<input id="mqttBroker" save="input" type="text" class="width100" placeholder="test.mosquitto.org"
data-i18n-placeholder="mqttBrokerHelp" />
</div>
<div class="item">
<div data-i18n="port"></div>
<input id="mqttPort" save="input" type="number" min="1" max="65535" class="width100" placeholder="8081" />
</div>
<div class="item">
<div data-i18n="mqttPath"></div>
<input id="mqttPath" save="input" type="text" class="width100" placeholder="/mqtt"
data-i18n-placeholder="mqttPathHelp" />
</div>
<div class="item">
<div data-i18n="mqttProtocol"></div>
<div class="switch switchSelect">
<select id="mqttProtocol" class="select" save="select">
<option value="wss" data-i18n="mqttProtocolWss"></option>
<option value="ws" data-i18n="mqttProtocolWs"></option>
</select>
</div>
</div>
<div class="item">
<div data-i18n="mqttClientId"></div>
<input id="mqttClientId" save="input" type="text" class="width100" placeholder="cat-catch-client"
data-i18n-placeholder="mqttClientIdHelp" />
</div>
<div class="item">
<div data-i18n="mqttTitleLength"></div>
<input id="mqttTitleLength" save="input" type="number" min="1" max="1000" class="width100" value="100"
data-i18n-placeholder="mqttTitleLengthHelp" />
</div>
<div class="item">
<div data-i18n="mqttUsername"></div>
<input id="mqttUser" save="input" type="text" class="width100"
data-i18n-placeholder="mqttCredentialsHelp" />
</div>
<div class="item">
<div data-i18n="mqttPassword"></div>
<input id="mqttPassword" save="input" type="password" class="width100"
data-i18n-placeholder="mqttCredentialsHelp" />
</div>
<div class="item">
<div data-i18n="mqttTopic"></div>
<input id="mqttTopic" save="input" type="text" class="width100" placeholder="cat-catch/media"
data-i18n-placeholder="mqttTopicHelp" />
</div>
<div class="item">
<div data-i18n="mqttQos"></div>
<div class="switch switchSelect">
<select id="mqttQos" class="select" save="select">
<option value="0">0 <span data-i18n="mqttQos0"></span></option>
<option value="1">1 <span data-i18n="mqttQos1"></span></option>
<option value="2">2 <span data-i18n="mqttQos2"></span></option>
</select>
</div>
<div class="help-text" data-i18n="mqttQosHelp"></div>
</div>
<div class="item">
<div data-i18n="mqttDataFormat"></div>
<textarea id="mqttDataFormat" save="input" class="width100" rows="4"
data-i18n-placeholder="mqttDataFormatHelp"></textarea>
<div class="help-text">
<a href="https://cat-catch.bmmmd.com/docs/tag" target="_blank" data-i18n="replaceKeywordList"></a>
<span data-i18n="mqttDataFormatDefault"></span>
</div>
</div>
<div class="flex-end">
<button type="button" class="resetOption" data-i18n="resetSettings"></button>
</div>
</div>
</div>
</section>
<!-- 操作按钮 -->
<section id="anchorOperation">
<h1 class="optionsTitle" data-i18n="operation"></h1>
<div class="optionBox">
<div class="flex-end" style="justify-content: center">
<input id="importOptionsFile" type="file" class="hide" />
<button type="button" id="exportOptions" data-i18n="exportSettings"></button>
<button type="button" id="importOptions" data-i18n="importConfiguration"></button>
<button type="button" id="ClearData" data-i18n="clearCapturedData"></button>
<button type="button" id="ResetAllOption" data-i18n="resetAllSettings"></button>
<button type="button" id="extensionReload" data-i18n="restartExtension"></button>
</div>
</div>
</section>
<!-- 关于 -->
<section id="anchorAbout">
<h1 class="optionsTitle" data-i18n="about"></h1>
<div class="optionBox">
<div class="item">
<div id="version"></div>
</div>
<div class="item">
<span data-i18n-outer="documentation"></span>:
<a href="https://cat-catch.bmmmd.com/" target="_blank">https://cat-catch.bmmmd.com/</a>
</div>
<div class="item">
Github:
<a href="https://github.com/xifangczy/cat-catch" target="_blank">https://github.com/xifangczy/cat-catch</a>
</div>
</div>
</section>
</div>
<script src="lib/base64.js" defer></script>
<script src="js/options.js" defer></script>
<script src="js/i18n.js" defer></script>
</body>
</html>

142
popup.html Normal file
View File

@@ -0,0 +1,142 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>catCatch</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="css/public.css" media="all" />
<link rel="stylesheet" type="text/css" href="css/popup.css" media="all" />
<script src="js/init.js"></script>
<script src="lib/jquery.min.js"></script>
<script src="js/function.js"></script>
</head>
<body class="popupBody">
<div class="Tabs">
<div id="currentTab" class="TabButton flex Active" title="Current Page">
<span data-i18n-outer="currentPage"></span>
<div id="quantity"></div>
</div>
<div id="allTab" class="TabButton flex" title="Other Pages">
<span data-i18n-outer="otherPage"></span>
<div id="quantity"></div>
</div>
<div id="otherTab" class="TabButton" title="Other Features / Media Control">
<span data-i18n-outer="otherFeatures"></span> / <span data-i18n-outer="mediaControl"></span>
</div>
<div id="maybeKeyTab" class="TabButton hide" title="Possible Key">
<span data-i18n-outer="possibleKey"></span>
</div>
</div>
<div id="Tips" data-i18n="loadingData">
</div>
<div id="TipsFixed">~</div>
<div id="mediaList" class="container hide TabShow"></div>
<div id="allMediaList" class="container hide"></div>
<div id="otherOptions" class="container hide">
<div class="otherTips" data-i18n="mediaControl"></div>
<div class="flexRow">
<b class="nowrap" data-i18n="selectWebpage"></b>
<select id="videoTabIndex" title="Video Tab">
<option value="-1" id="videoTabTips" data-i18n="noMediaDetected"></option>
</select>
</div>
<div class="flexRow">
<b class="nowrap" data-i18n="selectMedia"></b>
<select id="videoIndex" title="Video">
<option value="-1" id="videoIndexTips" data-i18n="noControllableMediaDetected"></option>
</select>
</div>
<div id="PlayControl" class="textColor">
<span data-i18n-outer="multiplier"></span><input id="playbackRate" type="number" value="2" min="0.1" max="16"
title="Play Back Rate" step="1" />
<button id="speed" class="button2" data-i18n="speedPlayback" title="Adjust playback speed"></button>
<button id="control" data-switch="play" data-i18n="play" title="Play/Pause media"></button>
<button id="pip" class="firefoxHide" data-i18n="pictureInPicture" title="Toggle picture-in-picture mode"></button>
<button id="fullScreen" class="firefoxHide" data-i18n="fullscreen" title="Toggle fullscreen mode"></button>
<button id="screenshot" data-switch="play" data-i18n="screenshot"
title="Take screenshot of current frame"></button>
<label class="flexColumn loop" title="Loop">
<div data-i18n="loop"></div><input type="checkbox" id="loop" placeholder="Loop">
</label>
<label class="flexColumn muted" title="Mute">
<div data-i18n="mute"></div><input type="checkbox" id="muted" placeholder="Mute">
</label>
<div class="flexColumn">
<div data-i18n="volume" title="Volume"></div><input type="range" id="volume" class="volume" min="0" max="1"
value="1" step="0.01" title="Volume Control" />
</div>
<div class="flexColumn width100">
<div id="timeShow" title="Time"></div><input type="range" id="time" class="width100" min="0" max="100" value="0"
step="1" title="Time Control" />
</div>
</div>
<div class="line"></div>
<div class="otherTips" data-i18n="functionEntry"></div>
<div class="otherFeat flexRow">
<button go="downloader.html" class="button2" data-i18n="downloader" title="Downloader"></button>
<button go="m3u8.html" class="button2" data-i18n="m3u8Parser" title="M3U8 Parser"></button>
<button go="mpd.html" class="button2" data-i18n="mpdParser" title="MPD Parser"></button>
<button go="json.html" class="button2" data-i18n="jsonFormatter" title="JSON Formatter"></button>
<button go="ffmpegURL" class="button2" title="FFmpeg">FFmpeg</button>
</div>
</div>
<div id="maybeKey" class="container hide"></div>
<div id="filter" class="more">
<span id="regular" class="width100 regular">
<input type="text" data-i18n-placeholder="regularFilterPlaceholder" title="Regular Filter" /><button
id="regularFilter" class="hide" data-i18n="confirm" title="Confirm"></button>
</span>
<div id="ext" title="Extensions"></div>
<button id="duplicateFilenames" class="button2" data-i18n="deleteDuplicateFilenames"
title="Delete Duplicate Filenames"></button>
</div>
<div id="features" class="more flex-end">
<button id="unfoldAll" data-i18n="expandAll" title="Expand All"></button>
<button id="unfoldPlay" data-i18n="expandPlayable" title="Expand Playable"></button>
<button id="unfoldFilter" data-i18n="expandSelected" title="Expand Selected"></button>
<button id="fold" data-i18n="collapseAll" title="Collapse All"></button>
<button id="recorder" class="button2 hide firefoxHideScript" type="script" data-i18n="videoRecording"
title="Start video recording"></button>
<button id="webrtc" class="button2 hide firefoxHideScript" type="script" data-i18n="recordWebRTC"
title="Record WebRTC streams"></button>
<button id="recorder2" class="button2 hide" type="script" data-i18n="screenCapture"
title="Capture screen content"></button>
<button id="MobileUserAgent" class="button2 firefoxHideScript" data-i18n="simulateMobile"
title="Simulate mobile device"></button>
<button id="AutoDown" class="button2" data-i18n="autoDownload" title="Enable automatic downloads"></button>
<button id="send2localSelect" class="button2" data-i18n="send2local"
title="Send selected items to local storage"></button>
</div>
<div id="down">
<button id="mergeDown" class="button2" data-i18n="onlineMerge" disabled
title="Merge selected items for download"></button>
<button id="DownFile" data-i18n="download" title="Download selected items"></button>
<button id="AllCopy" data-i18n="copy" title="Copy selected items to clipboard"></button>
<button id="AllSelect" data-i18n="selectAll" class="hide" title="Select all items"></button>
<button id="invertSelection" data-i18n="invertSelection" title="Invert selection"></button>
<button id="openFilter" panel="filter" data-i18n="filter" title="Open filter options"></button>
<button id="Clear" data-i18n="clear" title="Clear all items"></button>
<button id="search" class="button2 hide firefoxHideScript" type="script" data-i18n="deepSearch"
title="Perform deep search"></button>
<button id="catch" class="button2 hide firefoxHideScript" type="script" data-i18n="cacheCapture"
title="Capture cached content"></button>
<button id="more" class="button2" panel="features" data-i18n="moreFeatures" title="Show more features"></button>
<button id="enable" class="button2" data-i18n="pause" title="Pause/Resume capturing"></button>
<button id="Options" go="options.html" class="button2" data-i18n="settings" title="Open settings"></button>
<button id="popup" class="button2" data-i18n="popup" title="Open in popup window"></button>
<button id="currentPage" class="hide" data-i18n="currentPage" title="Switch to current page"></button>
</div>
<!-- <script src="js/firefox.js"></script> -->
<script src="lib/base64.js"></script>
<script src="lib/mqtt.min.js"></script>
<script src="js/pupup-utils.js"></script>
<script src="js/popup.js"></script>
<script src="js/media-control.js"></script>
<script src="lib/jquery.qrcode.min.js"></script>
<script src="lib/hls.min.js"></script>
<script src="js/i18n.js"></script>
</body>
</html>

119
preview.html Normal file
View File

@@ -0,0 +1,119 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>filter</title>
<link rel="stylesheet" type="text/css" href="css/public.css" media="all" />
<link rel="stylesheet" type="text/css" href="css/preview.css" media="all" />
<script src="js/init.js"></script>
<script src="js/firefox.js"></script>
<script src="js/function.js"></script>
</head>
<body>
<div class="play-container preview-container hide">
<video controls id="video-player"></video>
</div>
<div class="image-container preview-container hide">
<img src="" id="image-player" />
</div>
<div class="container">
<!-- 筛选条件 -->
<div class="filters">
<div class="filter-row">
<span data-i18n="suffix"></span>
<div id="extensionFilters"></div>
</div>
<div class="filter-row">
<span data-i18n="type"></span>
<div id="typeFilters"></div>
</div>
<div class="filter-row">
<span data-i18n="sort"></span>
<div class="sort-options">
<div class="sort-group">
<label><input type="radio" name="sortField" value="getTime" checked>
<i18n>getTime</i18n>
</label>
<label><input type="radio" name="sortField" value="size">
<i18n>fileSize</i18n>
</label>
<label><input type="radio" name="sortField" value="duration">
<i18n>duration</i18n>
</label>
<!-- <label><input type="radio" name="sortField" value="name">
文件名
</label> -->
</div>
<div class="sort-order">
<label><input type="radio" name="sortOrder" value="asc" data-i18n="asc" checked>
<i18n>asc</i18n>
</label>
<label><input type="radio" name="sortOrder" value="desc" data-i18n="desc">
<i18n>desc</i18n>
</label>
</div>
</div>
</div>
<div class="filter-row">
<span data-i18n="option"></span>
<label><input type="checkbox" id="showTitle">
<i18n>title</i18n>
</label>
<label><input type="checkbox" id="deleteDuplicateFilenames">
<i18n>deleteDuplicateFilenames</i18n>
</label>
</div>
<div class="filter-row">
<span data-i18n="regex"></span>
<input type="text" data-i18n-placeholder="regularFilterPlaceholder" id="regular" />
</div>
<div class="filter-row">
<span data-i18n="operation"></span>
<button id="select-all" class="button" data-i18n="selectAll"></button>
<button id="select-reverse" class="button" data-i18n="invertSelection"></button>
<button id="download-selected" class="button2" data-i18n="download" disabled></button>
<button id="merge-download" class="button2" data-i18n="onlineMerge" disabled></button>
<button id="copy-selected" class="button2" data-i18n="copy" disabled></button>
<button id="delete-selected" class="button2" data-i18n="delete" disabled></button>
<button id="aria2-selected" class="button2 hide" disabled>aria2</button>
<button id="send-selected" class="button2 hide" data-i18n="send2local" disabled></button>
<button id="clear" class="button" data-i18n="clear"></button>
<button id="debug" class="button">debug</button>
<button id="search" class="button2" type="script" data-i18n="deepSearch"></button>
<button id="catch" class="button2" type="script" data-i18n="cacheCapture"></button>
<label>
<input type="checkbox" name="defaultPopup" id="defaultPopup">
<i18n>defaultPopup</i18n>
</label>
</div>
</div>
<!-- 媒体列表 -->
<div id="file-container" class="file-grid"></div>
<div id="selection-box"></div>
<!-- 分页 -->
<div class="pagination hide">
<button class="button" id="prev-page" disabled>&lt;&lt;</button>
<div class="page-numbers"></div>
<button class="button" id="next-page" disabled>&gt;&gt;</button>
</div>
</div>
<script src="lib/base64.js"></script>
<script src="lib/hls.min.js"></script>
<script src="lib/mqtt.min.js"></script>
<script src="js/pupup-utils.js"></script>
<script src="js/preview.js"></script>
<script src="js/i18n.js"></script>
</body>
</html>

81
tools/sync-locales.js Normal file
View File

@@ -0,0 +1,81 @@
const fs = require('fs');
const path = require('path');
// Paths
const localesDir = path.join(__dirname, '../_locales');
const enFile = path.join(localesDir, 'en/messages.json');
// Read and parse JSON file
function readJsonFile(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf8');
return JSON.parse(content);
} catch (error) {
console.error(`Error reading ${filePath}:`, error);
return null;
}
}
// Write JSON file with 2-space indentation
function writeJsonFile(filePath, data) {
try {
const content = JSON.stringify(data, null, 2) + '\n';
fs.writeFileSync(filePath, content, 'utf8');
console.log(`Updated: ${filePath}`);
return true;
} catch (error) {
console.error(`Error writing ${filePath}:`, error);
return false;
}
}
// Get all locale directories except 'en'
function getLocaleDirs() {
return fs.readdirSync(localesDir, { withFileTypes: true })
.filter(dirent => dirent.isDirectory() && dirent.name !== 'en')
.map(dirent => dirent.name);
}
// Main function to sync locale files
function syncLocales() {
// Read English file as baseline
const enMessages = readJsonFile(enFile);
if (!enMessages) return;
// Get all locale directories
const locales = getLocaleDirs();
// Process each locale
locales.forEach(locale => {
const localeFile = path.join(localesDir, locale, 'messages.json');
const localeMessages = readJsonFile(localeFile) || {};
const syncedMessages = {};
let added = 0;
let removed = 0;
// Create new messages object with English key order
Object.keys(enMessages).forEach(key => {
if (localeMessages[key]) {
// Use existing translation
syncedMessages[key] = localeMessages[key];
} else {
// Add English message as placeholder
syncedMessages[key] = enMessages[key];
added++;
}
});
// Count removed keys (present in locale but not in English)
removed = Object.keys(localeMessages).length - (Object.keys(syncedMessages).length - added);
// Write the synchronized file
if (writeJsonFile(localeFile, syncedMessages)) {
console.log(`Synced ${locale}: ${added} added, ${removed} removed`);
}
});
}
// Run the synchronization
console.log('Starting locale synchronization...');
syncLocales();
console.log('Locale synchronization complete!');