├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug.yaml │ └── 功能-添加-修改-增强-请求.md ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README ├── edgeqrcode.png ├── m3u8.png └── popup.png ├── README_en.md ├── _locales ├── en │ └── messages.json ├── ja │ └── messages.json ├── pt_BR │ └── messages.json ├── zh_CN │ └── messages.json └── zh_TW │ └── messages.json ├── catch-script ├── catch.js ├── i18n.js ├── recorder.js ├── recorder2.js ├── search.js └── webrtc.js ├── css ├── mobile.css ├── options.css ├── popup.css ├── preview.css └── public.css ├── downloader.html ├── img ├── aria2-dark.png ├── aria2.png ├── cat-down-dark.png ├── cat-down.png ├── copy-dark.png ├── copy.png ├── delete-dark.svg ├── delete.svg ├── download-dark.svg ├── download.svg ├── icon-disable.png ├── icon.png ├── icon128.png ├── invoke-dark.svg ├── invoke.svg ├── music.svg ├── parsing-dark.png ├── parsing.png ├── play-dark.png ├── play.png ├── qrcode-dark.png ├── qrcode.png ├── regex-dark.png ├── regex.png ├── send-dark.svg └── send.svg ├── install.html ├── js ├── background.js ├── content-script.js ├── downloader.js ├── firefox.js ├── function.js ├── i18n.js ├── init.js ├── install.js ├── json.js ├── m3u8.downloader.js ├── m3u8.js ├── media-control.js ├── mpd.js ├── options.js ├── popup.js ├── preview.js └── pupup-utils.js ├── json.html ├── lib ├── StreamSaver.js ├── base64.js ├── hls.min.js ├── jquery.json-viewer.js ├── jquery.min.js ├── jquery.qrcode.min.js ├── m3u8-decrypt.js ├── mpd-parser.min.js └── mux.min.js ├── m3u8.html ├── manifest.firefox.json ├── manifest.json ├── mpd.html ├── options.html ├── popup.html └── preview.html /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: [https://paypal.me/o2bmm] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yaml: -------------------------------------------------------------------------------- 1 | name: Bug 报告 Bug Report 2 | description: 创建一个bug报告. File a bug report 3 | body: 4 | - type: input 5 | id: version 6 | attributes: 7 | label: 扩展版本号 extension version 8 | placeholder: e.g. vX.Y.Z 9 | - type: dropdown 10 | id: browser 11 | attributes: 12 | label: 浏览器 browser 13 | options: 14 | - Google Chrome 15 | - Microsoft Edge 16 | - Firefox 17 | - Chromium 18 | - 360浏览器 19 | - 其他基于 Chromium 的浏览器 20 | validations: 21 | required: true 22 | - type: input 23 | id: browserVersion 24 | attributes: 25 | label: 浏览器版本号 browser version 26 | placeholder: e.g. vX.Y.Z 27 | - type: input 28 | id: url 29 | attributes: 30 | label: 涉及网址 related URL 31 | placeholder: e.g. https://example.com 32 | description: 请提供发生问题的网址 需要授权登陆才能播放的请通过邮箱提交bug 33 | - type: checkboxes 34 | id: checklist 35 | attributes: 36 | label: Checklist 37 | options: 38 | - 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) 39 | required: true 40 | - 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. 41 | required: true 42 | - type: textarea 43 | id: description 44 | attributes: 45 | label: 请详细描述问题 What actually happened? 46 | validations: 47 | required: true 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/功能-添加-修改-增强-请求.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 功能 添加/修改/增强 请求 3 | about: 请求一个功能修改或添加 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ** 详细描述想要添 加/修改/增强 的功能 ** 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 更新说明 2 | 3 | ### 2.6.2 4 | 5 | [Added] m3u8 解析器 录制失败重试功能 (测试) 6 | 7 | [Added] m3u8 解析器 尝试估算文件大小 8 | 9 | [Added] 增加 其他设置 `使用侧边栏` 选项。从 popup 模式改为浏览器侧边栏打开扩展 (不支持 firefox) 10 | 11 | [Updated] m3u8 预览现在支持 hevc/h265 编码 12 | 13 | [Updated] 深度搜索 支持解析 vimeo playlist.json 14 | 15 | [Changed] 重构 缓存捕捉 脚本 减少头部数据缺失问题 16 | 17 | [Changed] 重构 排除重复的资源 减少资源占用 18 | 19 | [Fixed] 缓存捕捉脚本导致视频无法播放问题 20 | 21 | [Deleted] m3u8 解析器 删除了旧版本下载器 22 | 23 | [Deleted] 启用新弹出页 删除旧弹出页 24 | 25 | ### 2.6.1 26 | 27 | [Changed] 对手机浏览器进行一些适配 28 | 29 | ### 2.6.0 30 | 31 | [Added] 全新的弹出页面(`弹出`按钮) 文件预览/筛选帮助你下载需要的文件 (设置`feat newPopup`关闭新版) 32 | 33 | [Changed] 增强数据发送功能,现在能自定义发送数据 感谢 @helson-lin 的支持 34 | 35 | [Changed] 正则匹配 现在能获取到请求头 36 | 37 | [Changed] 支持夸克浏览器 (部分功能不可用) 38 | 39 | [Updated] 深度搜索脚本 找到更多资源 40 | 41 | [Fixed] Fifefox 导入功能 bug 导致扩展不可用 42 | 43 | [Fixed] 偶尔会弹出多个 ffmpeg 页面的 bug 44 | 45 | [Fixed] 下载器 打开`边下边存` 无法自动关闭的 bug 46 | 47 | ### 2.5.9 48 | 49 | [Added] 增加屏蔽网址功能 添加不希望开启扩展的网站 (可设为白名单, 只允许添加网址开启扩展) 50 | 51 | [Fixed] 新版下载器 下载大文件时 出错 #610 52 | 53 | [Changed] 限制每页面最大储存 9999 条资源 54 | 55 | [Changed] 设置增加导航栏 56 | 57 | [Changed] 自动下载 允许自定义保存文件名 58 | 59 | ### 2.5.8 60 | 61 | [Changed] 如果资源 url 不存在文件名 尝试使用页面标题作为文件名 62 | 63 | ### 2.5.7 64 | 65 | [Fixed] 自定义保存文件名使用 `/` 无法创建目录 66 | 67 | [Changed] firefox 升级 manifest v3 68 | 69 | [Changed] firefox 128 以上版本 支持使用深度搜索 缓存录制 等脚本功能 70 | 71 | [Fixed] firefox 无法发送到在线 ffmpeg 问题 72 | 73 | [Added] 重构 猫抓下载器 如需旧版本请在设置 关闭 `Test version` 选项 74 | 75 | [Added] `URL Protocol m3u8dl` `调用程序` 增加下载前确认参数设置 76 | 77 | [Added] m3u8 为疑似密钥增加验证密钥功能 78 | 79 | [Changed] 增强 深度搜索 现在能找到更多疑似密钥 80 | 81 | ### 2.5.6 82 | 83 | [Fixed] m3u8 解析器 自动关闭 bug #531 84 | 85 | [Fixed] chrome 130 自定义 url 新规范导致 `m3u8dl://` 调用失败 #528 86 | 87 | [Fixed] m3u8 解析器 文件不正确无法解析 造成死循环占用 CPU 问题 88 | 89 | [Changed] 猫抓下载器 添加更多请求头 增加下载成功率 90 | 91 | ### 2.5.5 92 | 93 | [Fixed] 修复一个严重 bug #483 94 | 95 | [Added] 在线 ffmpeg 提供服务器选择 96 | 97 | [Fixed] m3u8 解析器 文件名存在`|`字符 无法下载问题 98 | 99 | [Changed] 发送数据 提供完整请求头 100 | 101 | ### 2.5.4 102 | 103 | [Added] m3u8DL 增加切换 RE 版本 (RE 版 需[URLProtocol](https://github.com/xifangczy/URLProtocol)) 104 | 105 | [Added] 录制相关脚本 增加码率设置 106 | 107 | [Fixed] 深度搜索 脚本错误导致无法使用 108 | 109 | [Fixed] m3u8 解析器录制直播 录制时间显示错误 110 | 111 | ### 2.5.3 112 | 113 | [Added] 增加`弹出`模式 (以新窗口打开资源列表页面) 114 | 115 | [Added] 增加`调用本地程序`设置, 程序没有调用协议, 可以使用[URLProtocol](https://github.com/xifangczy/URLProtocol)帮助程序注册调用协议。具体使用方法查看 [调用本地协议](https://o2bmm.gitbook.io/cat-catch/docs/invoke) 116 | 117 | [Added] 下载器 增加`边下边存`选项 可以用来下载一些直播视频链接 118 | 119 | [Added] 现在使用`深度搜索` 或其他脚本得到的疑似密钥, 直接显示在 popup 页面 `疑似密钥` 标签内。 120 | 121 | [Added] 增加 葡萄牙语 122 | 123 | [Changed] 重写 `录制webRTC` 脚本 124 | 125 | [Changed] `m3u8解析器` `下载器`页面内更改设置不会被储存。所有设置更改统一到扩展设置页面。 126 | 127 | [Changed] storage.local 更改为 storage.session 以减少 IO 错误导致扩展无法使用.(要求 chrome 104 以上) 128 | 129 | [Changed] 优化与 ffmpeg 网页端的通信, 避免多任务时的数据错乱。 130 | (请提前打开 [在线 ffmpeg](https://ffmpeg.bmmmd.com/) ctrl+f5 刷新页面 避免页面缓存造成的问题) 131 | 132 | [Changed] 稍微增大一些按钮图标 不再训练大家的鼠标精准度 🙄...如果你不喜欢想还原 设置-自定义 css 填入 `body{font-size:12px;width:550px;}.icon,.favicon{width:18px;height:18px;}.DownCheck{width:15px;height:15px;}` 133 | 134 | ### 2.5.2 135 | 136 | [Added] 添加测试功能 数据发送 嗅探数据和密钥发送到指定地址 137 | 138 | [Added] 替换标签 增加 `${origin}` 139 | 140 | [Added] 显示 图标数字角标 开关 141 | 142 | [Fixed] 猫抓下载器 小部分网站需要指定 range 143 | 144 | [Fixed] 修复 标题作为文件名 文件名含有非法字符问题 #339 145 | 146 | ### 2.5.1 147 | 148 | [Added] 多语言 增加繁体中文 149 | 150 | [Fixed] 修复 深度搜索 死循环 bug 151 | 152 | [Fixed] 兼容低版本 chromium 缺少 API 导致扩展无法使用 153 | 154 | [Changed] popup 页面 现在能合并两个 m3u8 文件 155 | 156 | ### 2.5.0 157 | 158 | [Added] 多语言支持 159 | 160 | [Changed] m3u8 解析 新下载器 性能优化 161 | 162 | [Fixed] 视频捕捉 不使用`从头捕获`也会丢掉头部数据的问题 163 | 164 | [Changed] 深度搜索 现在能找到更多密钥 165 | 166 | ### 2.4.9 167 | 168 | [Fixed] `$url$` 标签 修复(自动更新成`${url}`) #281 169 | 170 | [Fixed] 修复 加密 m3u8 存在 EXT-X-MAP 标签,解密会失败的 bug 171 | 172 | [Added] 设置页面 添加自动合并 m3u8 选项 #286 (测试) 173 | 174 | [Added] 增加录制 webRTC 流脚本 更多功能-录制 webRTC (测试) 175 | 176 | ### 2.4.8 177 | 178 | [Fixed] 修复 m3u8 新下载器 ${referer} 标签问题 #272 179 | 180 | [Fixed] 修复 m3u8 新下载器 全部重新下载 bug #274 181 | 182 | [Fixed] 修复 m3u8 新下载器 下载失败丢失线程 #276 183 | 184 | [Fixed] 修复 m3u8 新下载器 勾选 ffmpeg 转码 下载超过 2G 大小 不会强制下载 185 | 186 | [Changed] 完善 Aria2 Rpc 协议 增加密钥 和 cookie 支持 187 | 188 | [Added] 增加${cookie}标签 如果资源存在 cookie 189 | 190 | ### 2.4.7 191 | 192 | [Fixed] 缓存捕获 延迟获取标题 #241 193 | 194 | [Fixed] 特殊字符造成无法下载的问题 #253 195 | 196 | [Fixed] m3u8 解析器 没有解析出全部嵌套 m3u8 的 bug #265 197 | 198 | [Added] firefox 增加 privacy 协议页面 第一次安装显示 199 | 200 | [Added] 增加 Aria2 Rpc 协议下载 感谢 @aar0u 201 | 202 | [Changed] 重写录制脚本 203 | 204 | [Changed] 增强深度搜索 205 | 206 | [Changed] m3u8 解析器 现在可以自定义头属性 207 | 208 | [Changed] m3u8 解析器 最大下载线程调整为 6 209 | 210 | [Changed] m3u8 解析器 默认开启新下载器 211 | 212 | ### 2.4.6 213 | 214 | [Fixed] 缓存捕获 多个视频问题 #239 215 | 216 | [Changed] 更新 mux m3u8-decrypt mpd-parser 版本 217 | 218 | [Changed] 设置 刷新跳转清空当前标签抓取的数据 现在可以调节模式 219 | 220 | [Changed] firefox 版本要求 113+ 221 | 222 | [test] m3u8 解析器 增加测试项 `重构的下载器` 223 | 224 | ### 2.4.5 225 | 226 | [Changed] 增强 深度搜索 解决"一次性"m3u8 227 | 228 | [Changed] m3u8 解析器 下载范围允许填写时间格式 HH:MM:SS 229 | 230 | [Added] 增加 缓存捕获 从头捕获、正则提取文件名、手动填写文件名 231 | 232 | [Added] 增加 设置 正则匹配 屏蔽资源功能 233 | 234 | [Added] 增加 下载器 后台打开页面设置 235 | 236 | [deleted] 删除 "幽灵资源" 设定 不确定来源的资源归于当前标签 237 | 238 | [Fixed] 修复 缓存捕获 清理缓存 239 | 240 | [Fixed] 修复 正则匹配 有时会匹配失效(lastIndex 没有复位) 241 | 242 | [Fixed] 修复 媒体控制 有时检测不到媒体 243 | 244 | [Fixed] 修复 重置所有设置 丢失配置 245 | 246 | [Fixed] 修复 firefox 兼容问题 247 | 248 | ### 2.4.4 249 | 250 | [Changed] 增强 深度搜索 251 | 252 | [Fixed] m3u8 解析器 无限触发错误的 bug 253 | 254 | ### 2.4.3 255 | 256 | [Fixed] 修复 缓存捕获 获取文件名为空 257 | 258 | [Changed] 增强 深度搜索 可以搜到更多密钥 259 | 260 | [Changed] 增强 注入脚本 现在会注入到所有 iframe 261 | 262 | [Changed] 删除 youtube 支持 可以使用缓存捕捉 263 | 264 | ### 2.4.2 265 | 266 | [Added] 设置页面增加 排除重复的资源 选项 267 | 268 | [Added] popup 增加暂停抓取按钮 269 | 270 | [Changed] 超过 500 条资源 popup 可以中断加载 271 | 272 | [Changed] 调整默认配置 默认不启用 ts 文件 删除多余正则 273 | 274 | [Changed] 正则匹配的性能优化 275 | 276 | [Fixed] 修复 m3u8 解析器录制功能 直播结束导致自动刷新页面丢失已下载数据的问题 277 | 278 | [Fixed] 修复 m3u8 解析器边下边存和 mp4 转码一起使用 编码不正确的 bug 279 | 280 | [Fixed] 修复 扩展重新启动后 造成的死循环 281 | 282 | ### 2.4.1 283 | 284 | [Added] 捕获脚本 现在可以通过表达式获取文件名 285 | 286 | [Changed] 删除 打开自动下载的烦人提示 287 | 288 | [Changed] 优化 firefox 下 资源严重占用问题 289 | 290 | [Fixed] 猫抓下载器 不再限制 2G 文件大小 #179 291 | 292 | ### 2.4.0 293 | 294 | [Added] 加入自定义 css 295 | 296 | [Added] 音频 视频 一键合并 297 | 298 | [Added] popup 页面正则筛选 299 | 300 | [Added] 自定义快捷键支持 301 | 302 | [Added] popup 页面支持正则筛选 303 | 304 | [Added] m3u8 碎片文件自定义参数 305 | 306 | [Changed] 筛选 现在能隐藏不要的数据 而不是取消勾选 307 | 308 | [Changed] 重写优化 popup 大部分代码 309 | 310 | [Changed] 重写初始化部分代码 311 | 312 | [Changed] m3u8 解析器 默认设置改为 ffmpeg 转码 而不是 mp4 转码 313 | 314 | [Changed] 删除 调试模式 315 | 316 | [Fixed] 深度搜索 深度判断的 bug 317 | 318 | [Fixed] 很多 bug 319 | 320 | ### 2.3.3 321 | 322 | [Changed] 解析器 m3u8DL 默认不载入设置参数 #149 323 | 324 | [Changed] 可以同时打开多个捕获脚本 325 | 326 | [Changed] popup 页面 css 细节调整 #156 327 | 328 | [Fixed] 清空不会删除角标的 bug 329 | 330 | [Fixed] 替换标签中 参数内包含 "|" 字符处理不正确的 bug 331 | 332 | ### 2.3.2 333 | 334 | [Changed] 设置 增加自定义文件名 删除标题正则提取 335 | 336 | [Added] 支持深色模式 #134 337 | 338 | [Added] popup 增加筛选 339 | 340 | [Fixed] 修复非加密的 m3u8 无法自定义密钥下载 341 | 342 | [Fixed] mp4 转码删除 创建媒体日期 属性 #142 343 | 344 | ### 2.3.1 345 | 346 | [Added] 新的替换标签 347 | 348 | [Changed] 边下边存 支持 mp4 转码 349 | 350 | [Fixed] 修复 BUG #123 #117 #114 #124 351 | 352 | ### 2.3.0 353 | 354 | [Added] m3u8 解析器 边下边存 355 | 356 | [Added] m3u8 解析器 在线 ffmpeg 转码 357 | 358 | [Fixed] 特殊文件名 下载所选无法下载 359 | 360 | [Fixed] m3u8 解析器 某些情况无法下载文件 361 | 362 | [Fixed] Header 属性提取失败 363 | 364 | [Fixed] 添加抓取类型出错 #109 365 | 366 | [Changed] 修改 标题修剪 默认配置 367 | 368 | ### 2.2.9 369 | 370 | [Fixed] 修复 m3u8DL 调用命令范围参数 --downloadRange 不正确 371 | 372 | [Added] 正则修剪标题 [#90](https://github.com/xifangczy/cat-catch/issues/94) 373 | 374 | [Added] 下载前选择保存目录 选项 375 | 376 | [Fixed] m3u8 解析器 部分情况无法下载 ts 文件 377 | 378 | [Changed] `复制所选`按钮 现在能被 `复制选项`设置影响 379 | 380 | ### 2.2.8 381 | 382 | [Changed] m3u8 解析器现在会记忆你设定的参数 383 | 384 | [Changed] 幽灵数据 更改为 其他页面(幽灵数据同样归类其他页面) 385 | 386 | [Changed] popup 页面的性能优化 387 | 388 | [Changed] 增加 始终不启用下载器 选项 389 | 390 | [Fixed] 修复 使用第三方下载器猫抓下载器也会被调用 391 | 392 | ### 2.2.7 393 | 394 | [Fixed] 修正 文件大小显示不正确 395 | 396 | [Changed] 性能优化 397 | 398 | [Fixed] 修复 没有正确清理冗余数据 导致 CPU 占用问题 399 | 400 | ### 2.2.6 401 | 402 | [Added] 深度搜索 尝试收集 m3u8 文件的密钥 具体使用查看 [用户文档](https://o2bmm.gitbook.io/cat-catch/docs/m3u8parse#maybekey) 403 | 404 | [Added] popup 资源详情增加二维码按钮 405 | 406 | [Added] m3u8 解析器 自定义文件名 只要音频 另存为 m3u8DL 命令完善 部分代码来自 [#80](https://github.com/xifangczy/cat-catch/pull/80) 407 | 408 | [Added] 非 Chrome 扩展商店版本 现在支持 Youtube 409 | 410 | [Added] Firefox 版 现在支持 m3u8 视频预览 411 | 412 | [Fixed] m3u8 解析器 超长名字无法保存文件 [#80](https://github.com/xifangczy/cat-catch/pull/80) 413 | 414 | [Fixed] 修正 媒体控制 某些情况检测不到视频 415 | 416 | ### 2.2.5 417 | 418 | [Fixed] 修复 mpd 解析器丢失音轨 [#70](https://github.com/xifangczy/cat-catch/issues/70) 419 | 420 | [Changed] 优化在网络状况不佳下的直播 m3u8 录制 421 | 422 | [Changed] 更新 深度搜索 search.js 进一步增加分析能力 423 | 424 | [Changed] 减少 mp4 转码时内存占用 425 | 426 | [Changed] 自定义调用本地播放器的协议 427 | 428 | ### 2.2.4 429 | 430 | [Changed] 更新 hls.js 431 | 432 | [Changed] m3u8 文件现在能显示更多媒体信息 433 | 434 | [Added] 增加 Dash mpd 文件解析 435 | 436 | [Added] 增加 深度搜索 脚本 437 | 438 | [Fixed] 修复 捕获按钮偶尔失效 439 | 440 | ### 2.2.3 441 | 442 | [Added] m3u8 解析器增加录制直播 443 | 444 | [Added] m3u8 解析器增加处理 EXT-X-MAP 标签 445 | 446 | [Added] 新增捕获脚本 recorder2.js 需要 Chromium 104 以上版本 447 | 448 | [Added] 增加选项 刷新、跳转到新页面 清空当前标签抓取的数据 449 | 450 | [Fixed] 修正 m3u8 解析器使用 mp4 转码生成的文件,媒体时长信息不正确 451 | 452 | ### 2.2.2 453 | 454 | [Changed] m3u8 解析器使用 hls.js 替代,多项改进,自定义功能添加 455 | 456 | [Changed] 分离下载器和 m3u8 解析器 457 | 458 | [Fixed] 修复 m3u8 解析器`调用N_m3u8DL-CLI下载`按钮失效 459 | 460 | [Fixed] 修复幽灵数据随机丢失问题 461 | 462 | [Fixed] 修复 m3u8 解析器 key 下载器在某些时候无法下载的问题 463 | 464 | ### 2.2.1 465 | 466 | [Fixed] 修复浏览器字体过大,按钮遮挡资源列表的问题。 467 | 468 | [Fixed] 调整关键词替换 469 | 470 | [Fixed] 修复 Firefox download API 无法下载 data URL 问题 471 | 472 | [Changed] m3u8 解析器多个 KEY 显示问题 473 | 474 | [Changed] 视频控制现在可以控制其他页面的视频 475 | 476 | [Changed] 视频控制现在可以对视频截图 477 | 478 | [Changed] 自定义复制选项增加 其他文件 选项 479 | 480 | [Added] m3u8 解析器现在可以转换成 mp4 格式 481 | 482 | ### 2.2.0 483 | 484 | [Fixed] 修复文件名出现 "~" 符号 导致 chrome API 无法下载 485 | 486 | [Fixed] 修复 Firefox 中 popup 页面下载按钮被滚动条遮挡 487 | 488 | [Fixed] 储存路劲有中文时 m3u8dl 协议调用错误 489 | 490 | [Changed] 增加/删除一些默认配置 491 | 492 | [Added] 增加操控当前网页视频功能 493 | 494 | [Added] 增加自定义复制选项 495 | 496 | ### 2.1.2 497 | 498 | [Changed] 细节调整 499 | 500 | ### 2.1.1 501 | 502 | [Changed] 调整正则匹配 现在能提取多个网址 503 | 504 | [Fixed] 修复选择脚本在 m3u8 解析器里不起作用 并提高安全性 505 | 506 | [Fixed] m3u8 解析器在 Firefox 中不能正常播放 m3u8 视频 507 | 508 | [Fixed] 修复 Firefox 中手机端模拟无法还原的问题 509 | 510 | [Fixed] 修复初始化错误 BUG 导致扩展失效 511 | 512 | ### 2.1.0 513 | 514 | [Changed] 新增 referer 获取 不存在再使用 initiator 或者直接使用 url 515 | 516 | [Changed] 重新支持 Firefox 需要 93 版本以上 517 | 518 | [Changed] chromium 内核的浏览器最低要求降为 93 小部分功能需要 102 版本以上,低版本会隐藏功能按钮 519 | 520 | [Fixed] 部分 m3u8 key 文件解析错误问题 521 | 522 | [Fixed] 修复 保存文件名使用网页标题 选项在 m3u8 解析器里不起作用 523 | 524 | ### 2.0.0 525 | 526 | [Changed] 模拟手机端,现在会修改 navigator.userAgent 变量 527 | 528 | [Added] 视频捕获功能,解决被动嗅探无法下载视频的问题 529 | 530 | [Added] 视频录制功能,解决被动嗅探无法下载视频的问题 531 | 532 | [Added] 支持 N_m3u8DL-CLI 的 m3u8dl://协议 533 | 534 | [Added] m3u8 解析器增强,现在能在线合并下载 m3u8 文件 535 | 536 | [Added] popup 页面无法下载的视频,会交给 m3u8 解析器修改 Referer 下载 537 | 538 | [Added] popup 页面和 m3u8 页面可以在线预览 m3u8 539 | 540 | [Added] json 查看工具,和 m3u8 解析器一样在 popup 页面显示图标进入 541 | 542 | [Fixed] 无数 BUG 543 | 544 | [Fixed] 解决 1.0.17 以来会丢失数据的问题 545 | 546 | [Fixed] 该死的 Service Worker... 现在后台被杀死能立刻唤醒自己... 继续用肮脏的手段对抗 Manifest V3 547 | 548 | ### 1.0.26 549 | 550 | [Fixed] 解决关闭网页不能正确删除当前页面储存的数据问题 551 | 552 | ### 1.0.25 553 | 554 | [Changed] 正则匹配增强 555 | 556 | [Changed] Heart Beat 557 | 558 | [Added] 手机端模拟,手机环境下有更多资源可以被下载。 559 | 560 | [Added] 自动下载 561 | 562 | ### 1.0.24 563 | 564 | [Added] 导入/导出配置 565 | 566 | [Added] Heart Beat 解决 Service Worker 休眠问题 567 | 568 | [Added] firefox.js 兼容层 并上架 Firefox 569 | 570 | ### 1.0.23 571 | 572 | [Added] 正则匹配 573 | 574 | ### 1.0.22 575 | 576 | [Fixed] 一个严重 BUG,导致 Service Worker 无法使用 \* 577 | 578 | ### 1.0.21 579 | 580 | [Added] 自定义抓取类型 581 | 582 | [Refactor] 设置页面新界面 583 | 584 | ### 1.0.20 585 | 586 | [Added] 抓取 image/\*类型文件选项 587 | 588 | ### 1.0.19 589 | 590 | [Fixed] 重构导致的许多 BUG \* 591 | 592 | ### 1.0.18 593 | 594 | [Added] 抓取 application/octet-stream 选项 595 | 596 | [Refactor] 重构剩余代码 597 | 598 | ### 1.0.17 599 | 600 | [Refactor] Manifest 更新到 V3 部分代码 601 | 602 | [Added] 使用 PotPlayer 预览媒体 603 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

[中文] | [English]

2 | 3 | # 📑简介 4 | 猫抓(cat-catch) 资源嗅探扩展,能够帮你筛选列出当前页面的资源。 5 | 6 | # 📖安装地址 7 | ## 🐴Chrome 8 | https://chrome.google.com/webstore/detail/jfedfbgedapdagkghmgibemcoggfppbb 9 | ## 🦄Edge 10 | https://microsoftedge.microsoft.com/addons/detail/oohmdefbjalncfplafanlagojlakmjci 11 | ## 🦊Firefox 12 | https://addons.mozilla.org/addon/cat-catch/ 😂需非国区IP访问 13 | ## 📱Edge Android 14 | 15 | 16 | 💔猫抓是开源的,任何人都可以下载修改上架到应用商店,已经有不少加上广告代码后上架的伪猫抓,请注意自己的数据安全。所有安装地址以github和用户文档为准。 17 | 18 | # 📒用户文档 19 | https://cat-catch.bmmmd.com/ 20 | 21 | # 🌏翻译 22 | [![gitlocalized ](https://gitlocalize.com/repo/9392/whole_project/badge.svg)](https://gitlocalize.com/repo/9392?utm_source=badge) 23 | 24 | # 📘安装方法 25 | ## 应用商店安装 26 | 通过安装地址的链接到官方扩展商店即可安装。 27 | ## 源码安装 28 | 1. Git Clone 代码。 29 | 2. 扩展管理页面 打开 "开发者模式"。 30 | 3. 点击 "加载已解压的扩展程序" 选中扩展文件夹即可。 31 | ## crx安装 32 | 1. [Releases](https://github.com/xifangczy/cat-catch/releases) **右键另存为**下载crx文件。 33 | 2. 扩展管理页面 打开 "开发者模式"。 34 | 3. 将crx文件拖入扩展程序页面即可。 35 | 36 | # 📚兼容性说明 37 | 1.0.17版本之后需要Chromium内核版本93以上。 38 | 低于93请使用1.0.16版本。 39 | 要体验完整功能,请使用104版本以上。 40 | 41 | # 🔍界面 42 | ![popup界面](https://raw.githubusercontent.com/xifangczy/cat-catch/master/README/popup.png) 43 | ![m3u8解析器界面](https://raw.githubusercontent.com/xifangczy/cat-catch/master/README/m3u8.png) 44 | 45 | # 🤚🏻免责 46 | 本扩展仅供下载用户拥有版权或已获授权的视频,禁止用于下载受版权保护且未经授权的内容。用户需自行承担使用本工具的全部法律责任,开发者不对用户的任何行为负责。本工具按“原样”提供,开发者不承担任何直接或间接责任。 47 | 48 | # 🔒隐私政策 49 | 本扩展收集所有信息都在本地储存处理,不会发送到远程服务器,不包含任何跟踪器。 50 | 51 | # 💖鸣谢 52 | - [hls.js](https://github.com/video-dev/hls.js) 53 | - [jQuery](https://github.com/jquery/jquery) 54 | - [mux.js](https://github.com/videojs/mux.js) 55 | - [js-base64](https://github.com/dankogai/js-base64) 56 | - [jquery.json-viewer](https://github.com/abodelot/jquery.json-viewer) 57 | - [Momo707577045](https://github.com/Momo707577045) 58 | - [mpd-parser](https://github.com/videojs/mpd-parser) 59 | - [StreamSaver.js](https://github.com/jimmywarting/StreamSaver.js) 60 | 61 | # 📜License 62 | GPL-3.0 license 63 | 64 | 1.0版 使用 MIT许可 65 | 66 | 2.0版 更改为GPL v3许可 67 | 68 | 为了资源嗅探扩展有良好发展,希望使用猫抓源码的扩展仍然保持开源。 69 | -------------------------------------------------------------------------------- /README/edgeqrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xifangczy/cat-catch/9461680524c1a452cba90f2c35d5f2f754d9003b/README/edgeqrcode.png -------------------------------------------------------------------------------- /README/m3u8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xifangczy/cat-catch/9461680524c1a452cba90f2c35d5f2f754d9003b/README/m3u8.png -------------------------------------------------------------------------------- /README/popup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xifangczy/cat-catch/9461680524c1a452cba90f2c35d5f2f754d9003b/README/popup.png -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 |

[中文] | [English]

2 | 3 | # 📑Introduction 4 | Cat-Catch is a resource sniffing extension that can help you filter and list the resources of the current page. 5 | 6 | # 📖Installation 7 | ## 🐴Chrome 8 | https://chrome.google.com/webstore/detail/jfedfbgedapdagkghmgibemcoggfppbb 9 | ## 🦄Edge 10 | https://microsoftedge.microsoft.com/addons/detail/oohmdefbjalncfplafanlagojlakmjci 11 | ## 🦊Firefox 12 | https://addons.mozilla.org/addon/cat-catch/ 😂Non-China IP required for access 13 | ## 📱Edge Android 14 | 15 | 16 | 💔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. 17 | 18 | # 📒Documentation 19 | https://cat-catch.bmmmd.com/ 20 | 21 | # 🌏Translations 22 | [![gitlocalized ](https://gitlocalize.com/repo/9392/whole_project/badge.svg)](https://gitlocalize.com/repo/9392?utm_source=badge) 23 | 24 | # 📘 Installation Methods 25 | ## App Store Installation 26 | Install directly from the official extension store using the link provided. 27 | ## Source Code Installation 28 | 1. Git clone the repository. 29 | 2. Open the extensions management page and enable "Developer Mode." 30 | 3. Click "Load unpacked" and select the extension folder. 31 | ## CRX Installation 32 | 1. **Right-click** and save the CRX file from [Releases](https://github.com/xifangczy/cat-catch/releases). 33 | 2. Open the extensions management page and enable "Developer Mode." 34 | 3. Drag the CRX file into the extensions page. 35 | 36 | # 📚Compatibility 37 | After version 1.0.17, Chromium kernel version 93 or above is required. 38 | Use version 1.0.16 if below 93. 39 | For full functionality, use version 104 or above. 40 | 41 | # 🔍Screenshot 42 | ![popup Screenshot](https://raw.githubusercontent.com/xifangczy/cat-catch/master/README/popup.png) 43 | ![m3u8 parser Screenshot](https://raw.githubusercontent.com/xifangczy/cat-catch/master/README/m3u8.png) 44 | 45 | # 🤚🏻Disclaimer 46 | 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. 47 | 48 | # 🔒Privacy Policy 49 | The extension collects and processes all information locally without sending it to remote servers and does not include any trackers. 50 | 51 | # 💖Acknowledgements 52 | - [hls.js](https://github.com/video-dev/hls.js) 53 | - [jQuery](https://github.com/jquery/jquery) 54 | - [mux.js](https://github.com/videojs/mux.js) 55 | - [js-base64](https://github.com/dankogai/js-base64) 56 | - [jquery.json-viewer](https://github.com/abodelot/jquery.json-viewer) 57 | - [Momo707577045](https://github.com/Momo707577045) 58 | - [mpd-parser](https://github.com/videojs/mpd-parser) 59 | - [StreamSaver.js](https://github.com/jimmywarting/StreamSaver.js) 60 | 61 | # 📜License 62 | GPL-3.0 license 63 | 64 | Version 1.0 uses the MIT license. 65 | 66 | Version 2.0 has changed to the GPL v3 license. 67 | 68 | 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. 69 | -------------------------------------------------------------------------------- /catch-script/i18n.js: -------------------------------------------------------------------------------- 1 | window.CatCatchI18n = { 2 | languages: ["en", "zh"], 3 | downloadCapturedData: { 4 | en: "Download the captured data", 5 | zh: "下载已捕获的数据" 6 | }, 7 | deleteCapturedData: { 8 | en: "Delete the captured data", 9 | zh: "删除已捕获数据" 10 | }, 11 | capturedBeginning: { 12 | en: "Capture from the beginning", 13 | zh: "从头捕获" 14 | }, 15 | alwaysCapturedBeginning: { 16 | en: "Always Capture from the beginning", 17 | zh: "始终从头捕获" 18 | }, 19 | hide: { 20 | en: "Hide", 21 | zh: "隐藏" 22 | }, 23 | close: { 24 | en: "Close", 25 | zh: "关闭" 26 | }, 27 | save: { 28 | en: "Save", 29 | zh: "保存" 30 | }, 31 | automaticDownload: { 32 | en: "Automatic download", 33 | zh: "完成捕获自动下载" 34 | }, 35 | ffmpeg: { 36 | en: "using ffmpeg", 37 | zh: "使用ffmpeg" 38 | }, 39 | fileName: { 40 | en: "File name", 41 | zh: "文件名" 42 | }, 43 | selector: { 44 | en: "Selector", 45 | zh: "表达式" 46 | }, 47 | regular: { 48 | en: "Regular", 49 | zh: "正则" 50 | }, 51 | notSet: { 52 | en: "Not set", 53 | zh: "未设置" 54 | }, 55 | usingSelector: { 56 | en: "selector", 57 | zh: "表达式提取" 58 | }, 59 | usingRegular: { 60 | en: "regular", 61 | zh: "正则提取" 62 | }, 63 | customize: { 64 | en: "Customize", 65 | zh: "自定义" 66 | }, 67 | cleanHeader: { 68 | en: "Clean up redundant header data", 69 | zh: "清理多余头部数据" 70 | }, 71 | clearCache: { 72 | en: "Clear cache", 73 | zh: "清理缓存" 74 | }, 75 | cleanupCompleted: { 76 | en: "Cleanup completed", 77 | zh: "清理完成" 78 | }, 79 | downloadConfirmation: { 80 | en: "Downloading in advance may cause data confusion. Confirm?", 81 | zh: "提前下载可能会造成数据混乱.确认?" 82 | }, 83 | fileNameError: { 84 | en: "Unable to fetch or the content is empty!", 85 | zh: "无法获取或内容为空!" 86 | }, 87 | noData: { 88 | en: "No data", 89 | zh: "没抓到有效数据!" 90 | }, 91 | waiting: { 92 | en: "Waiting for video to play", 93 | zh: "等待视频播放" 94 | }, 95 | capturingData: { 96 | en: "Capturing data", 97 | zh: "捕获数据中" 98 | }, 99 | captureCompleted: { 100 | en: "Capture completed", 101 | zh: "捕获完成" 102 | }, 103 | downloadCompleted: { 104 | en: "Download completed", 105 | zh: "下载完毕" 106 | }, 107 | selectVideo: { 108 | en: "Select Video", 109 | zh: "选择视频" 110 | }, 111 | recordEncoding: { 112 | en: "Record Encoding", 113 | zh: "录制编码" 114 | }, 115 | readVideo: { 116 | en: "Read Video", 117 | zh: "读取视频" 118 | }, 119 | startRecording: { 120 | en: "Start Recording", 121 | zh: "开始录制" 122 | }, 123 | stopRecording: { 124 | en: "Stop Recording", 125 | zh: "停止录制" 126 | }, 127 | noVideoDetected: { 128 | en: "No video detected, Please read again", 129 | zh: "没有检测到视频, 请重新读取" 130 | }, 131 | recording: { 132 | en: "Recording", 133 | zh: "视频录制中" 134 | }, 135 | recordingNotSupported: { 136 | en: "recording Not Supported", 137 | zh: "不支持录制" 138 | }, 139 | formatNotSupported: { 140 | en: "Format not supported", 141 | zh: "不支持此格式" 142 | }, 143 | clickToStartRecording: { 144 | en: "Click to start recording", 145 | zh: "请点击开始录制" 146 | }, 147 | sentToFfmpeg: { 148 | en: "Sent to ffmpeg", 149 | zh: "发送到ffmpeg" 150 | }, 151 | recordingFailed: { 152 | en: "Recording failed", 153 | zh: "录制失败" 154 | }, 155 | scriptNotSupported: { 156 | en: "This script is not supported", 157 | zh: "当前网页不支持此脚本" 158 | }, 159 | dragWindow: { 160 | en: "Drag window", 161 | zh: "拖动窗口" 162 | }, 163 | autoToBuffered: { 164 | en: "Automatically jump to buffer", 165 | zh: "自动跳转到缓冲尾" 166 | }, 167 | save1hour: { 168 | en: "Save once every hour", 169 | zh: "1小时保存一次" 170 | }, 171 | recordingChangeEncoding: { 172 | en: "Cannot change encoding during recording", 173 | zh: "录制中不能更改编码" 174 | }, 175 | streamEmpty: { 176 | en: "Media stream is empty", 177 | zh: "媒体流为空" 178 | }, 179 | notStream: { 180 | en: "Not a media stream object", 181 | zh: "非媒体流对象" 182 | }, 183 | notStream: { 184 | en: "Not a media stream object", 185 | zh: "非媒体流对象" 186 | }, 187 | streamAdded: { 188 | en: "Stream added", 189 | zh: "流已添加" 190 | }, 191 | videoAndAudio: { 192 | en: "Includes both audio and video streams", 193 | zh: "已包含音频和视频流" 194 | }, 195 | audioBits: { 196 | en: "Audio bit", 197 | zh: "音频码率" 198 | }, 199 | videoBits: { 200 | en: "Video bits", 201 | zh: "视频码率" 202 | }, 203 | frameRate: { 204 | en: "frame Rate", 205 | zh: "帧率" 206 | }, 207 | noHeader: { 208 | en: "No header data detected, please process with local tools", 209 | zh: "没有检测到视频头部数据, 请使用本地工具处理" 210 | }, 211 | headData: { 212 | en: "Multiple header data found in media file, Clear it?", 213 | zh: "检测到多余头部数据, 是否清除?" 214 | }, 215 | clearCacheConfirmation: { 216 | en: "Are you sure you want to clear the cache?", 217 | zh: "确定要清除缓存吗?" 218 | }, 219 | closeConfirmation: { 220 | en: "Are you sure you want to close?", 221 | zh: "确定要关闭吗?" 222 | } 223 | }; -------------------------------------------------------------------------------- /catch-script/recorder.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | console.log("recorder.js Start"); 3 | if (document.getElementById("catCatchRecorder")) { return; } 4 | 5 | // let language = "en"; 6 | let language = navigator.language.replace("-", "_"); 7 | if (window.CatCatchI18n) { 8 | if (!window.CatCatchI18n.languages.includes(language)) { 9 | language = language.split("_")[0]; 10 | if (!window.CatCatchI18n.languages.includes(language)) { 11 | language = "en"; 12 | } 13 | } 14 | } 15 | 16 | const buttonStyle = 'style="border:solid 1px #000;margin:2px;padding:2px;background:#fff;border-radius:4px;border:solid 1px #c7c7c780;color:#000;"'; 17 | const checkboxStyle = 'style="-webkit-appearance: auto;"'; 18 | 19 | const CatCatch = document.createElement("div"); 20 | CatCatch.setAttribute("id", "catCatchRecorder"); 21 | CatCatch.innerHTML = ` 22 |
23 | 选择视频 24 | 录制编码 25 | 26 | 47 |
48 | 49 | 50 | 51 | 52 | 53 |
`; 54 | CatCatch.style = ` 55 | position: fixed; 56 | z-index: 999999; 57 | top: 10%; 58 | left: 80%; 59 | background: rgb(255 255 255 / 85%); 60 | border: solid 1px #c7c7c7; 61 | border-radius: 4px; 62 | color: rgb(26, 115, 232); 63 | padding: 5px 5px 5px 5px; 64 | font-size: 12px; 65 | font-family: "Microsoft YaHei", "Helvetica", "Arial", sans-serif; 66 | user-select: none; 67 | display: flex; 68 | align-items: flex-start; 69 | justify-content: space-evenly; 70 | flex-direction: column; 71 | line-height: 20px;`; 72 | 73 | // 创建 Shadow DOM 放入CatCatch 74 | const divShadow = document.createElement('div'); 75 | const shadowRoot = divShadow.attachShadow({ mode: 'closed' }); 76 | shadowRoot.appendChild(CatCatch); 77 | // 页面插入Shadow DOM 78 | document.getElementsByTagName('html')[0].appendChild(divShadow); 79 | 80 | const $tips = CatCatch.querySelector("#tips"); 81 | const $videoList = CatCatch.querySelector("#videoList"); 82 | const $mimeTypeList = CatCatch.querySelector("#mimeTypeList"); 83 | const $start = CatCatch.querySelector("#start"); 84 | const $stop = CatCatch.querySelector("#stop"); 85 | let videoList = []; 86 | $tips.innerHTML = i18n("noVideoDetected", "没有检测到视频, 请重新读取"); 87 | let recorder = {}; 88 | let option = { mimeType: 'video/webm;codecs=vp9,opus' }; 89 | 90 | CatCatch.querySelector("#hide").addEventListener('click', function (event) { 91 | CatCatch.style.display = "none"; 92 | }); 93 | CatCatch.querySelector("#close").addEventListener('click', function (event) { 94 | recorder?.state && recorder.stop(); 95 | CatCatch.style.display = "none"; 96 | window.postMessage({ action: "catCatchToBackground", Message: "script", script: "recorder.js", refresh: false }); 97 | }); 98 | 99 | function init() { 100 | getVideo(); 101 | $start.style.display = 'inline'; 102 | $stop.style.display = 'none'; 103 | } 104 | setTimeout(init, 500); 105 | 106 | // #region 视频编码选择 107 | function setMimeType() { 108 | function getSupportedMimeTypes(media, types, codecs) { 109 | const supported = []; 110 | types.forEach((type) => { 111 | const mimeType = `${media}/${type}`; 112 | codecs.forEach((codec) => [`${mimeType};codecs=${codec}`].forEach(variation => { 113 | if (MediaRecorder.isTypeSupported(variation)) { 114 | supported.push(variation); 115 | } 116 | })); 117 | if (MediaRecorder.isTypeSupported(mimeType)) { 118 | supported.push(mimeType); 119 | } 120 | }); 121 | return supported; 122 | }; 123 | const videoTypes = ["webm", "ogg", "mp4", "x-matroska"]; 124 | const codecs = ["should-not-be-supported", "vp9", "vp8", "avc1", "av1", "h265", "h.265", "h264", "h.264", "opus", "pcm", "aac", "mpeg", "mp4a"]; 125 | const supportedVideos = getSupportedMimeTypes("video", videoTypes, codecs); 126 | supportedVideos.forEach(function (type) { 127 | $mimeTypeList.options.add(new Option(type, type)); 128 | }); 129 | option.mimeType = supportedVideos[0]; 130 | 131 | $mimeTypeList.addEventListener('change', function (event) { 132 | if (recorder && recorder.state && recorder.state === 'recording') { 133 | $tips.innerHTML = i18n("recordingChangeEncoding", "录制中不能更改编码"); 134 | return; 135 | } 136 | if (MediaRecorder.isTypeSupported(event.target.value)) { 137 | option.mimeType = event.target.value; 138 | $tips.innerHTML = event.target.value; 139 | } else { 140 | $tips.innerHTML = i18n("formatNotSupported", "不支持此格式"); 141 | } 142 | }); 143 | } 144 | setMimeType(); 145 | // #endregion 视频编码选择 146 | 147 | // #region 获取视频列表 148 | function getVideo() { 149 | videoList = []; 150 | $videoList.options.length = 0; 151 | document.querySelectorAll("video, audio").forEach(function (video, index) { 152 | if (video.currentSrc) { 153 | const src = video.currentSrc.split("/").pop(); 154 | videoList.push(video); 155 | $videoList.options.add(new Option(src, index)); 156 | } 157 | }); 158 | $tips.innerHTML = videoList.length ? i18n("clickToStartRecording", "请点击开始录制") : i18n("noVideoDetected", "没有检测到视频, 请重新读取"); 159 | } 160 | CatCatch.querySelector("#getVideo").addEventListener('click', getVideo); 161 | CatCatch.querySelector("#stop").addEventListener('click', function () { 162 | recorder.stop(); 163 | }); 164 | // #endregion 获取视频列表 165 | 166 | CatCatch.querySelector("#start").addEventListener('click', function (event) { 167 | if (!MediaRecorder.isTypeSupported(option.mimeType)) { 168 | $tips.innerHTML = i18n("formatNotSupported", "不支持此格式"); 169 | return; 170 | } 171 | init(); 172 | const index = $videoList.value; 173 | if (index && videoList[index]) { 174 | let stream = null; 175 | try { 176 | const frameRate = +CatCatch.querySelector("#frameRate").value; 177 | if (frameRate) { 178 | stream = videoList[index].captureStream(frameRate); 179 | } else { 180 | stream = videoList[index].captureStream(); 181 | } 182 | } catch (e) { 183 | $tips.innerHTML = i18n("recordingNotSupported", "不支持录制"); 184 | return; 185 | } 186 | // 码率 187 | option.audioBitsPerSecond = +CatCatch.querySelector("#audioBits").value; 188 | option.videoBitsPerSecond = +CatCatch.querySelector("#videoBits").value; 189 | 190 | recorder = new MediaRecorder(stream, option); 191 | recorder.ondataavailable = function (event) { 192 | if (CatCatch.querySelector("#ffmpeg").checked) { 193 | window.postMessage({ 194 | action: "catCatchFFmpeg", 195 | use: "transcode", 196 | files: [{ data: URL.createObjectURL(event.data), type: option.mimeType }], 197 | title: document.title.trim() 198 | }); 199 | $tips.innerHTML = i18n("clickToStartRecording", "请点击开始录制"); 200 | return; 201 | } 202 | const a = document.createElement('a'); 203 | a.href = URL.createObjectURL(event.data); 204 | a.download = `${document.title}`; 205 | a.click(); 206 | a.remove(); 207 | $tips.innerHTML = i18n("downloadCompleted", "下载完成");; 208 | } 209 | recorder.onstart = function (event) { 210 | $stop.style.display = 'inline'; 211 | $start.style.display = 'none'; 212 | $tips.innerHTML = i18n("recording", "视频录制中"); 213 | } 214 | recorder.onstop = function (event) { 215 | $tips.innerHTML = i18n("stopRecording", "停止录制"); 216 | init(); 217 | } 218 | recorder.onerror = function (event) { 219 | init(); 220 | $tips.innerHTML = i18n("recordingFailed", "录制失败");; 221 | console.log(event); 222 | }; 223 | recorder.start(); 224 | videoList[index].play(); 225 | setTimeout(() => { 226 | if (recorder.state === 'recording') { 227 | $stop.style.display = 'inline'; 228 | $start.style.display = 'none'; 229 | $tips.innerHTML = i18n("recording", "视频录制中"); 230 | } 231 | }, 500); 232 | } else { 233 | $tips.innerHTML = i18n("noVideoDetected", "请确认视频是否存在"); 234 | } 235 | }); 236 | 237 | // #region 移动逻辑 238 | let x, y; 239 | function move(event) { 240 | CatCatch.style.left = event.pageX - x + 'px'; 241 | CatCatch.style.top = event.pageY - y + 'px'; 242 | } 243 | CatCatch.addEventListener('mousedown', function (event) { 244 | x = event.pageX - CatCatch.offsetLeft; 245 | y = event.pageY - CatCatch.offsetTop; 246 | document.addEventListener('mousemove', move); 247 | document.addEventListener('mouseup', function () { 248 | document.removeEventListener('mousemove', move); 249 | }); 250 | }); 251 | // #endregion 移动逻辑 252 | 253 | // i18n 254 | if (window.CatCatchI18n) { 255 | CatCatch.querySelectorAll('[data-i18n]').forEach(function (element) { 256 | element.innerHTML = window.CatCatchI18n[element.dataset.i18n][language]; 257 | }); 258 | CatCatch.querySelectorAll('[data-i18n-outer]').forEach(function (element) { 259 | element.outerHTML = window.CatCatchI18n[element.dataset.i18nOuter][language]; 260 | }); 261 | } 262 | function i18n(key, original = "") { 263 | if (!window.CatCatchI18n) { return original }; 264 | return window.CatCatchI18n[key][language]; 265 | } 266 | })(); -------------------------------------------------------------------------------- /catch-script/recorder2.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | console.log("recorder2.js Start"); 3 | if (document.getElementById("catCatchRecorder2")) { 4 | return; 5 | } 6 | if (!navigator.mediaDevices) { 7 | alert("当前网页不支持屏幕分享"); 8 | return; 9 | } 10 | 11 | let language = navigator.language.replace("-", "_"); 12 | if (window.CatCatchI18n) { 13 | if (!window.CatCatchI18n.languages.includes(language)) { 14 | language = language.split("_")[0]; 15 | if (!window.CatCatchI18n.languages.includes(language)) { 16 | language = "en"; 17 | } 18 | } 19 | } 20 | 21 | // 添加style 22 | const style = document.createElement("style"); 23 | style.innerHTML = ` 24 | @keyframes color-change{ 25 | 0% { outline: 4px solid rgb(26, 115, 232); } 26 | 50% { outline: 4px solid red; } 27 | 100% { outline: 4px solid rgb(26, 115, 232); } 28 | } 29 | #catCatchRecorder2 { 30 | font-weight: bold; 31 | position: absolute; 32 | cursor: move; 33 | z-index: 999999999; 34 | outline: 4px solid rgb(26, 115, 232); 35 | resize: both; 36 | overflow: hidden; 37 | height: 720px; 38 | width: 1024px; 39 | top: 30%; 40 | left: 30%; 41 | pointer-events: none; 42 | font-size: 10px; 43 | } 44 | #catCatchRecorderHeader { 45 | background: rgb(26, 115, 232); 46 | color: white; 47 | text-align: center; 48 | height: 20px; 49 | cursor: pointer; 50 | display: flex; 51 | justify-content: space-evenly; 52 | align-items: center; 53 | pointer-events: auto; 54 | } 55 | #catCatchRecorderTitle { 56 | cursor: move; 57 | user-select: none; 58 | width: 45%; 59 | } 60 | #catCatchRecorderinnerCropArea { 61 | height: calc(100% - 20px); 62 | width: 100%; 63 | } 64 | .animation { 65 | animation: color-change 5s infinite; 66 | } 67 | .input-group { 68 | display: flex; 69 | align-items: center; 70 | } 71 | .input-group label { 72 | margin-right: 5px; 73 | } 74 | #videoBitrate, #audioBitrate { 75 | width: 4rem; 76 | } 77 | .input-group label{ 78 | width: 5rem; 79 | }`; 80 | 81 | // 添加div 82 | let cat = document.createElement("div"); 83 | cat.setAttribute("id", "catCatchRecorder2"); 84 | cat.innerHTML = `
85 |
86 |
87 | 94 |
95 |
96 | 101 |
102 |
开始录制
103 |
拖动窗口
104 |
关闭
105 |
`; 106 | 107 | // 创建 Shadow DOM 放入CatCatch 108 | const divShadow = document.createElement('div'); 109 | const shadowRoot = divShadow.attachShadow({ mode: 'closed' }); 110 | shadowRoot.appendChild(cat); 111 | shadowRoot.appendChild(style); 112 | document.getElementsByTagName('html')[0].appendChild(divShadow); 113 | 114 | // 事件绑定 115 | const catCatchRecorderStart = cat.querySelector("#catCatchRecorderStart"); 116 | catCatchRecorderStart.onclick = function () { 117 | if (recorder) { 118 | recorder.stop(); 119 | return; 120 | } 121 | try { startRecording(); } catch (e) { console.log(e); return; } 122 | } 123 | cat.querySelector("#catCatchRecorderClose").onclick = function () { 124 | recorder && recorder.stop(); 125 | cat.remove(); 126 | } 127 | 128 | // 拖动div 129 | const catCatchRecorderinnerCropArea = cat.querySelector("#catCatchRecorderinnerCropArea"); 130 | cat.querySelector("#catCatchRecorderTitle").onpointerdown = (e) => { 131 | let pos1, pos2, pos3, pos4; 132 | pos3 = e.clientX; 133 | pos4 = e.clientY; 134 | if (pos3 - cat.offsetWidth - cat.offsetLeft > - 20 && 135 | pos4 - cat.offsetHeight - cat.offsetTop > - 20) { 136 | return; 137 | } 138 | document.onpointermove = (e) => { 139 | pos1 = pos3 - e.clientX; 140 | pos2 = pos4 - e.clientY; 141 | pos3 = e.clientX; 142 | pos4 = e.clientY; 143 | cat.style.top = cat.offsetTop - pos2 + "px"; 144 | cat.style.left = cat.offsetLeft - pos1 + "px"; 145 | } 146 | document.onpointerup = () => { 147 | document.onpointerup = null; 148 | document.onpointermove = null; 149 | } 150 | } 151 | // document.getElementsByTagName('html')[0].appendChild(cat); 152 | 153 | // 初始化位置 154 | const video = document.querySelector("video"); 155 | if (video) { 156 | // 调整和video一样大小 157 | if (video.clientHeight >= 0 && video.clientWidth >= 0) { 158 | cat.style.height = video.clientHeight + 20 + "px"; 159 | cat.style.width = video.clientWidth + "px"; 160 | } 161 | // 调整到video的位置 162 | const videoOffset = getElementOffset(video); 163 | if (videoOffset.top >= 0 && videoOffset.left >= 0) { 164 | cat.style.top = videoOffset.top + "px"; 165 | cat.style.left = videoOffset.left + "px"; 166 | } 167 | // 防止遮挡菜单 168 | let catAttr = cat.getBoundingClientRect(); 169 | if (document.documentElement.scrollTop + catAttr.bottom > document.documentElement.scrollTop + window.innerHeight) { 170 | cat.style.top = document.documentElement.scrollTop + window.innerHeight - catAttr.height + "px"; 171 | } 172 | } 173 | 174 | // 录制 175 | var recorder; 176 | async function startRecording() { 177 | const buffer = []; 178 | let option = { 179 | mimeType: 'video/webm;codecs=vp8,opus', 180 | videoBitsPerSecond: +cat.querySelector("#videoBits").value, 181 | audioBitsPerSecond: +cat.querySelector("#audioBits").value 182 | }; 183 | 184 | if (MediaRecorder.isTypeSupported('video/webm;codecs=vp9,opus')) { 185 | option.mimeType = 'video/webm;codecs=vp9,opus'; 186 | } else if (MediaRecorder.isTypeSupported('video/webm;codecs=h264')) { 187 | option.mimeType = 'video/webm;codecs=h264'; 188 | } 189 | const cropTarget = await CropTarget.fromElement(catCatchRecorderinnerCropArea); 190 | const stream = await navigator.mediaDevices 191 | .getDisplayMedia({ 192 | preferCurrentTab: true, 193 | video: { 194 | cursor: "never" 195 | }, 196 | audio: { 197 | sampleRate: 48000, 198 | sampleSize: 16, 199 | channelCount: 2 200 | } 201 | }); 202 | const [track] = stream.getVideoTracks(); 203 | await track.cropTo(cropTarget); 204 | recorder = new MediaRecorder(stream, option); 205 | recorder.start(); 206 | recorder.onstart = function (e) { 207 | buffer.slice(0); 208 | catCatchRecorderStart.innerHTML = i18n("stopRecording", "停止录制"); 209 | cat.classList.add("animation"); 210 | } 211 | recorder.ondataavailable = function (e) { 212 | buffer.push(e.data); 213 | } 214 | recorder.onstop = function () { 215 | const fileBlob = new Blob(buffer, { type: option }); 216 | const a = document.createElement('a'); 217 | a.href = URL.createObjectURL(fileBlob); 218 | a.download = `${document.title}.webm`; 219 | a.click(); 220 | a.remove(); 221 | buffer.slice(0); 222 | stream.getTracks().forEach(track => track.stop()); 223 | recorder = undefined; 224 | catCatchRecorderStart.innerHTML = i18n("startRecording", "开始录制"); 225 | cat.classList.remove("animation"); 226 | } 227 | } 228 | function getElementOffset(el) { 229 | const rect = el.getBoundingClientRect(); 230 | const scrollTop = window.pageYOffset || document.documentElement.scrollTop; 231 | const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; 232 | return { 233 | top: rect.top + scrollTop, 234 | left: rect.left + scrollLeft 235 | }; 236 | } 237 | 238 | // i18n 239 | if (window.CatCatchI18n) { 240 | CatCatch.querySelectorAll('[data-i18n]').forEach(function (element) { 241 | const translation = window.CatCatchI18n[element.dataset.i18n]?.[language]; 242 | if (translation) { 243 | element.innerHTML = translation; 244 | } 245 | }); 246 | CatCatch.querySelectorAll('[data-i18n-outer]').forEach(function (element) { 247 | const outerTranslation = window.CatCatchI18n[element.dataset.i18nOuter]?.[language]; 248 | if (outerTranslation) { 249 | element.outerHTML = outerTranslation; 250 | } 251 | }); 252 | } 253 | function i18n(key, original = "") { 254 | if (!window.CatCatchI18n) { return original }; 255 | return window.CatCatchI18n[key][language]; 256 | } 257 | })(); -------------------------------------------------------------------------------- /catch-script/webrtc.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | console.log("webrtc.js Start"); 3 | if (document.getElementById("catCatchWebRTC")) { return; } 4 | 5 | // 多语言 6 | let language = navigator.language.replace("-", "_"); 7 | if (window.CatCatchI18n) { 8 | if (!window.CatCatchI18n.languages.includes(language)) { 9 | language = language.split("_")[0]; 10 | if (!window.CatCatchI18n.languages.includes(language)) { 11 | language = "en"; 12 | } 13 | } 14 | } 15 | 16 | const buttonStyle = 'style="border:solid 1px #000;margin:2px;padding:2px;background:#fff;border-radius:4px;border:solid 1px #c7c7c780;color:#000;"'; 17 | const checkboxStyle = 'style="-webkit-appearance: auto;"'; 18 | const CatCatch = document.createElement("div"); 19 | CatCatch.innerHTML = ` 20 |
正在等待视频流..."
21 |
22 | ${i18n("recordEncoding", "录制编码")}: 23 | 24 | 38 |
39 | 40 | 41 | 42 | 43 | 44 |
`; 45 | CatCatch.style = ` 46 | position: fixed; 47 | z-index: 999999; 48 | top: 10%; 49 | left: 80%; 50 | background: rgb(255 255 255 / 85%); 51 | border: solid 1px #c7c7c7; 52 | border-radius: 4px; 53 | color: rgb(26, 115, 232); 54 | padding: 5px 5px 5px 5px; 55 | font-size: 12px; 56 | font-family: "Microsoft YaHei", "Helvetica", "Arial", sans-serif; 57 | user-select: none; 58 | display: flex; 59 | align-items: flex-start; 60 | justify-content: space-evenly; 61 | flex-direction: column; 62 | line-height: 20px;`; 63 | 64 | // 创建 Shadow DOM 放入CatCatch 65 | const divShadow = document.createElement('div'); 66 | const shadowRoot = divShadow.attachShadow({ mode: 'closed' }); 67 | shadowRoot.appendChild(CatCatch); 68 | // 页面插入Shadow DOM 69 | document.getElementsByTagName('html')[0].appendChild(divShadow); 70 | 71 | // 提示 72 | const $tips = CatCatch.querySelector("#tips"); 73 | const tips = (text) => { 74 | $tips.innerHTML = text; 75 | } 76 | 77 | // 开始 结束 按钮切换 78 | const $start = CatCatch.querySelector("#start"); 79 | const $stop = CatCatch.querySelector("#stop"); 80 | const buttonState = (state = true) => { 81 | $start.style.display = state ? 'inline' : 'none'; 82 | $stop.style.display = state ? 'none' : 'inline'; 83 | } 84 | $start.style.display = 'inline'; 85 | $stop.style.display = 'none'; 86 | 87 | // 关闭 88 | CatCatch.querySelector("#close").addEventListener('click', function (event) { 89 | recorder?.state && recorder.stop(); 90 | CatCatch.style.display = "none"; 91 | window.postMessage({ action: "catCatchToBackground", Message: "script", script: "webrtc.js", refresh: true }); 92 | }); 93 | 94 | // 隐藏 95 | CatCatch.querySelector("#hide").addEventListener('click', function (event) { 96 | CatCatch.style.display = "none"; 97 | }); 98 | 99 | /* 核心变量 */ 100 | let recorder = null; // 录制器 101 | let mediaStream = null; // 媒体流 102 | let autoSave1Timer = null; // 1小时保存一次 103 | 104 | // #region 编码选择 105 | let option = { mimeType: 'video/webm;codecs=vp9,opus' }; 106 | function getSupportedMimeTypes(media, types, codecs) { 107 | const supported = []; 108 | types.forEach((type) => { 109 | const mimeType = `${media}/${type}`; 110 | codecs.forEach((codec) => [`${mimeType};codecs=${codec}`].forEach(variation => { 111 | if (MediaRecorder.isTypeSupported(variation)) { 112 | supported.push(variation); 113 | } 114 | })); 115 | if (MediaRecorder.isTypeSupported(mimeType)) { 116 | supported.push(mimeType); 117 | } 118 | }); 119 | return supported; 120 | }; 121 | const $mimeTypeList = CatCatch.querySelector("#mimeTypeList"); 122 | const videoTypes = ["webm", "ogg", "mp4", "x-matroska"]; 123 | const codecs = ["should-not-be-supported", "vp9", "vp8", "avc1", "av1", "h265", "h.265", "h264", "h.264", "opus", "pcm", "aac", "mpeg", "mp4a"]; 124 | const supportedVideos = getSupportedMimeTypes("video", videoTypes, codecs); 125 | supportedVideos.forEach(function (type) { 126 | $mimeTypeList.options.add(new Option(type, type)); 127 | }); 128 | option.mimeType = supportedVideos[0]; 129 | $mimeTypeList.addEventListener('change', function (event) { 130 | if (recorder && recorder.state && recorder.state === 'recording') { 131 | tips(i18n("recordingChangeEncoding", "录制中不能更改编码")); 132 | return; 133 | } 134 | if (MediaRecorder.isTypeSupported(event.target.value)) { 135 | option.mimeType = event.target.value; 136 | tips(`${i18n("recordEncoding", "录制编码")}:` + event.target.value); 137 | } else { 138 | tips(i18n("formatNotSupported", "不支持此格式")); 139 | } 140 | }); 141 | // #endregion 编码选择 142 | 143 | // 录制 144 | $time = CatCatch.querySelector("#time"); 145 | CatCatch.querySelector("#start").addEventListener('click', function () { 146 | if (!mediaStream) { 147 | tips(i18n("streamEmpty", "媒体流为空")); 148 | return; 149 | } 150 | if (!mediaStream instanceof MediaStream) { 151 | tips(i18n("notStream", "非媒体流对象")); 152 | return; 153 | } 154 | let recorderTime = 0; 155 | let recorderTimeer = undefined; 156 | let chunks = []; 157 | 158 | // 码率 159 | option.audioBitsPerSecond = +CatCatch.querySelector("#audioBits").value; 160 | option.videoBitsPerSecond = +CatCatch.querySelector("#videoBits").value; 161 | 162 | recorder = new MediaRecorder(mediaStream, option); 163 | recorder.ondataavailable = event => { 164 | chunks.push(event.data) 165 | }; 166 | recorder.onstop = () => { 167 | recorderTime = 0; 168 | clearInterval(recorderTimeer); 169 | clearInterval(autoSave1Timer); 170 | $time.innerHTML = ""; 171 | tips(i18n("stopRecording", "已停止录制!")); 172 | download(chunks); 173 | buttonState(); 174 | } 175 | recorder.onstart = () => { 176 | chunks = []; 177 | tips(i18n("recording", "视频录制中")); 178 | $time.innerHTML = "00:00"; 179 | recorderTimeer = setInterval(function () { 180 | recorderTime++; 181 | $time.innerHTML = secToTime(recorderTime); 182 | }, 1000); 183 | buttonState(false); 184 | } 185 | recorder.start(60000); 186 | }); 187 | // 停止录制 188 | CatCatch.querySelector("#stop").addEventListener('click', function () { 189 | if (recorder) { 190 | recorder.stop(); 191 | recorder = undefined; 192 | } 193 | }); 194 | // 保存 195 | CatCatch.querySelector("#save").addEventListener('click', function () { 196 | if (recorder) { 197 | recorder.stop(); 198 | recorder.start(); 199 | } 200 | }); 201 | // 每1小时 保存一次 202 | CatCatch.querySelector("#autoSave1").addEventListener('click', function () { 203 | clearInterval(autoSave1Timer); 204 | if (CatCatch.querySelector("#autoSave1").checked) { 205 | autoSave1Timer = setInterval(function () { 206 | if (recorder) { 207 | recorder.stop(); 208 | recorder.start(); 209 | } 210 | }, 3600000); 211 | } 212 | }); 213 | 214 | // 获取webRTC流 215 | window.RTCPeerConnection = new Proxy(window.RTCPeerConnection, { 216 | construct(target, args) { 217 | const pc = new target(...args); 218 | mediaStream = new MediaStream(); 219 | pc.addEventListener('track', (event) => { 220 | const track = event.track; 221 | if (track.kind === 'video' || track.kind === 'audio') { 222 | mediaStream.addTrack(track); 223 | tips(`${track.kind} ${i18n("streamAdded", "流已添加")}`); 224 | const hasVideo = mediaStream.getVideoTracks().length > 0; 225 | const hasAudio = mediaStream.getAudioTracks().length > 0; 226 | if (hasVideo && hasAudio) { 227 | tips(i18n("videoAndAudio", "已包含音频和视频流")); 228 | } 229 | } 230 | }); 231 | pc.addEventListener('iceconnectionstatechange', (event) => { 232 | if (pc.iceConnectionState === 'disconnected' && recorder?.state === 'recording') { 233 | recorder.stop(); 234 | tips(i18n("stopRecording", "连接已断开,录制已停止")); 235 | } 236 | }); 237 | return pc; 238 | } 239 | }); 240 | 241 | // #region 移动逻辑 242 | let x, y; 243 | const move = (event) => { 244 | CatCatch.style.left = event.pageX - x + 'px'; 245 | CatCatch.style.top = event.pageY - y + 'px'; 246 | } 247 | CatCatch.addEventListener('mousedown', function (event) { 248 | x = event.pageX - CatCatch.offsetLeft; 249 | y = event.pageY - CatCatch.offsetTop; 250 | document.addEventListener('mousemove', move); 251 | document.addEventListener('mouseup', function () { 252 | document.removeEventListener('mousemove', move); 253 | }); 254 | }); 255 | // #endregion 移动逻辑 256 | 257 | function download(chunks) { 258 | const blob = new Blob(chunks, { type: option.mimeType }); 259 | const url = URL.createObjectURL(blob); 260 | const a = document.createElement('a'); 261 | a.style.display = 'none'; 262 | a.href = url; 263 | a.download = 'recorded-video.mp4'; 264 | document.body.appendChild(a); 265 | a.click(); 266 | window.URL.revokeObjectURL(url); 267 | document.body.removeChild(a); 268 | } 269 | 270 | // 秒转换成时间 271 | function secToTime(sec) { 272 | let hour = (sec / 3600) | 0; 273 | let min = ((sec % 3600) / 60) | 0; 274 | sec = (sec % 60) | 0; 275 | let time = hour > 0 ? hour + ":" : ""; 276 | time += min.toString().padStart(2, '0') + ":"; 277 | time += sec.toString().padStart(2, '0'); 278 | return time; 279 | } 280 | 281 | // 防止网页意外关闭跳转 282 | window.addEventListener('beforeunload', function (e) { 283 | recorder && recorder.stop(); 284 | return true; 285 | }); 286 | 287 | // i18n 288 | if (window.CatCatchI18n) { 289 | CatCatch.querySelectorAll('[data-i18n]').forEach(function (element) { 290 | element.innerHTML = window.CatCatchI18n[element.dataset.i18n][language]; 291 | }); 292 | CatCatch.querySelectorAll('[data-i18n-outer]').forEach(function (element) { 293 | element.outerHTML = window.CatCatchI18n[element.dataset.i18nOuter][language]; 294 | }); 295 | } 296 | function i18n(key, original = "") { 297 | if (!window.CatCatchI18n) { return original }; 298 | return window.CatCatchI18n[key][language]; 299 | } 300 | })(); -------------------------------------------------------------------------------- /css/mobile.css: -------------------------------------------------------------------------------- 1 | .popupBody { 2 | width: 100%; 3 | } 4 | 5 | .wrapper.options { 6 | margin-right: 10px; 7 | } 8 | 9 | .m3u8_wrapper #mergeTs { 10 | font-size: 2rem; 11 | } 12 | 13 | .newDownload { 14 | width: 100%; 15 | padding: 0 2rem; 16 | } 17 | -------------------------------------------------------------------------------- /css/options.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: var(--background-color); 3 | font-size: 13px; 4 | font-family: "Microsoft YaHei", "Helvetica", "Arial", sans-serif; 5 | margin: 0; 6 | } 7 | 8 | .wrapper { 9 | margin: 0 auto; 10 | width: 45rem; 11 | } 12 | 13 | .error { 14 | color: var(--text-error-color); 15 | } 16 | 17 | h1 { 18 | font-size: 1.125em; 19 | font-weight: normal; 20 | margin: 0; 21 | } 22 | 23 | h2 { 24 | font-size: 1.125em; 25 | font-weight: normal; 26 | margin: 0; 27 | } 28 | 29 | p { 30 | margin: auto; 31 | } 32 | 33 | .optionBox { 34 | background: var(--optionBox-color); 35 | border-radius: 4px; 36 | box-shadow: 0 1px 2px 0 rgb(60 64 67 / 30%), 0 1px 3px 1px rgb(60 64 67 / 15%); 37 | padding: 0.75em 1.25em; 38 | margin-top: 5px; 39 | } 40 | 41 | table { 42 | width: 100%; 43 | text-align: center; 44 | } 45 | 46 | input, 47 | textarea { 48 | padding: 5px 5px; 49 | } 50 | 51 | input.ext { 52 | width: 100px; 53 | text-align: center; 54 | } 55 | 56 | input.type { 57 | width: 200px; 58 | text-align: center; 59 | } 60 | 61 | input.size { 62 | width: 100px; 63 | text-align: center; 64 | } 65 | 66 | input.regexType { 67 | width: 20px; 68 | text-align: center; 69 | } 70 | 71 | input.regexExt { 72 | width: 35px; 73 | text-align: center; 74 | } 75 | 76 | input.regex { 77 | width: 320px; 78 | text-align: center; 79 | } 80 | 81 | /* input#OtherAutoClear { 82 | margin-left: 250px; 83 | width: 45px; 84 | } */ 85 | /* 滑动开关 组件 */ 86 | .switch { 87 | height: 22px; 88 | width: 50px; 89 | margin: auto; 90 | } 91 | 92 | .switch .switchRound { 93 | position: relative; 94 | display: block; 95 | width: 100%; 96 | height: 100%; 97 | background-color: var(--switch-off-color); 98 | transition: all 0.2s ease-in-out; 99 | } 100 | 101 | .switch .switchRoundBtn { 102 | display: block; 103 | position: absolute; 104 | top: 2px; 105 | left: 3px; 106 | bottom: 3px; 107 | width: 18px; 108 | height: 18px; 109 | background-color: var(--switch-round-color); 110 | transition: all 0.2s ease-in-out; 111 | } 112 | 113 | .switch .switchInput { 114 | display: none; 115 | } 116 | 117 | .switch .switchInput:checked + .switchRound { 118 | background-color: var(--switch-on-color); 119 | } 120 | 121 | .switch .switchInput:checked + .switchRound > .switchRoundBtn { 122 | left: 29px; 123 | } 124 | 125 | .switch .switchRadius { 126 | border-radius: 50px; 127 | } 128 | 129 | /* 滑动开关 组件 END */ 130 | .list { 131 | padding-left: 10px; 132 | padding-top: 5px; 133 | } 134 | 135 | .item { 136 | align-items: center; 137 | display: flex; 138 | min-height: 30px; 139 | border-bottom: solid 1px rgba(0, 0, 0, 0.06); 140 | flex-wrap: wrap; 141 | align-items: flex-end; 142 | align-content: space-around; 143 | } 144 | 145 | .item .switch { 146 | margin-right: 50px; 147 | } 148 | 149 | .item .switchSelect { 150 | margin-right: 85px; 151 | } 152 | 153 | .optionsTitle { 154 | margin-top: 20px; 155 | } 156 | 157 | .RemoveButton { 158 | fill: var(--text-color); 159 | height: 20px; 160 | cursor: pointer; 161 | } 162 | 163 | button, 164 | .button, 165 | .button2 { 166 | padding: calc(0.5em - 1px) 1em; 167 | margin: 5px 5px 5px 5px; 168 | /* font-size: 13px; */ 169 | } 170 | 171 | .flex-end { 172 | display: flex; 173 | justify-content: flex-end; 174 | } 175 | 176 | .explain { 177 | color: #6c6c6c; 178 | } 179 | 180 | #typeList, 181 | #extList { 182 | margin-top: 10px; 183 | } 184 | 185 | .otherOption .item { 186 | margin-bottom: 5px; 187 | min-height: 35px; 188 | } 189 | 190 | #m3u8_url, 191 | #mpd_url, 192 | .test_url { 193 | overflow: hidden; 194 | display: block; 195 | text-overflow: ellipsis; 196 | word-break: break-all; 197 | color: var(--text2-color); 198 | } 199 | 200 | .block { 201 | border-bottom: solid 1px rgba(0, 0, 0, 0.06); 202 | padding-bottom: 5px; 203 | margin-bottom: 5px; 204 | } 205 | 206 | .m3u8_wrapper .block { 207 | border-bottom: 0px; 208 | } 209 | 210 | .wrapper1024 { 211 | margin: 0 auto; 212 | width: 1024px; 213 | } 214 | 215 | .wrapper1080 { 216 | margin: 0 auto; 217 | width: 1080px; 218 | } 219 | 220 | textarea { 221 | font-size: 12px; 222 | font-family: "Microsoft YaHei", "Helvetica", "Arial", sans-serif; 223 | } 224 | 225 | #textarea { 226 | text-align: center; 227 | } 228 | 229 | .m3u8_wrapper video { 230 | max-height: 80vh; 231 | max-width: 100%; 232 | } 233 | 234 | #media_file { 235 | word-break: break-all; 236 | } 237 | 238 | #media_file, 239 | #jsonText, 240 | #m3u8Text { 241 | height: 55vh; 242 | } 243 | 244 | /* #media_file { 245 | font-size: 12px; 246 | font-family: "Microsoft YaHei", "Helvetica", "Arial", sans-serif; 247 | height: 700px; 248 | overflow-y: auto; 249 | border: solid 1.5px rgb(0 0 0 / 50%); 250 | word-break: break-all; 251 | } */ 252 | #formatStr { 253 | width: 145px; 254 | } 255 | 256 | #tips input { 257 | color: var(--text2-color); 258 | } 259 | 260 | .keyUrl { 261 | width: 1034px; 262 | } 263 | 264 | .fullInput { 265 | /* width: 975px; */ 266 | width: 100%; 267 | margin: 5px 0 5px 0; 268 | } 269 | 270 | .select { 271 | appearance: none; 272 | background: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIxMiIgZmlsbD0iIzVGNjM2OCI+PHBhdGggZD0iTTAgMGgyNEwxMiAxMnoiLz48L3N2Zz4=) 273 | calc(100% - 8px) center no-repeat; 274 | /* background-color: rgb(241, 243, 244); */ 275 | background-color: var(--background-color-two); 276 | background-size: 10px; 277 | border: none; 278 | border-radius: 4px; 279 | cursor: pointer; 280 | padding: 5px 10px; 281 | } 282 | 283 | .select { 284 | width: 8rem; 285 | } 286 | 287 | .m3u8Key { 288 | width: 300px; 289 | } 290 | 291 | #PlayerTemplate { 292 | width: 200px; 293 | } 294 | 295 | #errorTsList p { 296 | color: red; 297 | word-break: break-all; 298 | } 299 | 300 | .progress-bar { 301 | height: 15px; 302 | background-color: rgb(189, 193, 198); 303 | border-radius: 3px; 304 | margin: 3px; 305 | margin-bottom: 10px; 306 | } 307 | 308 | .progress { 309 | width: 0px; 310 | height: 100%; 311 | background-color: var(--text2-color); 312 | border-radius: 3px; 313 | } 314 | 315 | #fileSize, 316 | #fileDuration { 317 | margin-left: 20px; 318 | } 319 | 320 | .not-allowed { 321 | cursor: not-allowed; 322 | background-color: #ccc; 323 | color: #fff; 324 | } 325 | 326 | .not-allowed:hover { 327 | background: #ccc; 328 | } 329 | 330 | .not-allowed:active { 331 | background: #ccc; 332 | } 333 | 334 | #showM3u8Help { 335 | margin-left: 10px; 336 | margin-top: 1px; 337 | margin-right: 0px; 338 | padding: 2px; 339 | } 340 | 341 | .m3u8checkbox { 342 | display: flex; 343 | cursor: pointer; 344 | flex-direction: column; 345 | user-select: none; 346 | margin: 0 5px 0 5px; 347 | } 348 | 349 | .merge { 350 | display: flex; 351 | justify-content: flex-start; 352 | margin-top: 5px; 353 | align-items: center; 354 | } 355 | 356 | .customKey input { 357 | margin-right: 5px; 358 | } 359 | 360 | /* .wrapper .button { 361 | margin-top: 5px; 362 | } */ 363 | .rangeDown { 364 | display: flex; 365 | flex-direction: column; 366 | align-items: center; 367 | margin-right: 10px; 368 | } 369 | 370 | .rangeDown .merge { 371 | margin-top: 0; 372 | } 373 | 374 | #rangeStart, 375 | #rangeEnd { 376 | width: 55px; 377 | /* text-align:center; 378 | vertical-align:middle; */ 379 | margin-left: 2px; 380 | margin-right: 2px; 381 | padding-top: 3px; 382 | padding-bottom: 3px; 383 | } 384 | 385 | #loading a { 386 | word-break: break-all; 387 | } 388 | 389 | #next_m3u8 a { 390 | word-break: break-all; 391 | } 392 | 393 | .key { 394 | align-items: flex-end; 395 | } 396 | 397 | .key div { 398 | display: flex; 399 | flex-direction: column; 400 | margin-right: 10px; 401 | } 402 | 403 | .key input { 404 | width: 265px; 405 | } 406 | 407 | .method input { 408 | width: 100px; 409 | } 410 | 411 | .offset { 412 | width: 256px; 413 | } 414 | 415 | .videoInfo div { 416 | margin-right: 5px; 417 | } 418 | 419 | .flex { 420 | display: flex; 421 | } 422 | 423 | .m3u8dlArg { 424 | margin-top: 10px; 425 | height: 100px; 426 | word-break: break-all; 427 | width: 100%; 428 | } 429 | 430 | .m3u8DL { 431 | margin-right: 70px !important; 432 | } 433 | 434 | /* .m3u8DL #m3u8dl{ 435 | width: 8rem; 436 | } */ 437 | .break-all { 438 | word-break: break-all; 439 | } 440 | 441 | /* MPD*/ 442 | .dash .select { 443 | padding-right: 20px; 444 | margin-bottom: 10px; 445 | } 446 | 447 | /* JSON格式化 */ 448 | .json-document { 449 | margin-top: 0px; 450 | } 451 | 452 | ul.json-dict, 453 | ol.json-array { 454 | list-style-type: none; 455 | margin: 0 0 0 1px; 456 | border-left: 1px dotted #ccc; 457 | padding-left: 2em; 458 | } 459 | 460 | .json-string { 461 | color: #0b7500; 462 | word-break: break-all; 463 | white-space: break-spaces; 464 | } 465 | 466 | .json-literal { 467 | color: #1a01cc; 468 | font-weight: bold; 469 | } 470 | 471 | a.json-toggle { 472 | position: relative; 473 | color: inherit; 474 | text-decoration: none; 475 | } 476 | 477 | a.json-toggle:focus { 478 | outline: none; 479 | } 480 | 481 | a.json-toggle:before { 482 | font-size: 1.1em; 483 | color: #c0c0c0; 484 | content: "\25BC"; 485 | position: absolute; 486 | display: inline-block; 487 | width: 1em; 488 | text-align: center; 489 | line-height: 1em; 490 | left: -1.2em; 491 | } 492 | 493 | a.json-toggle:hover:before { 494 | color: #aaa; 495 | } 496 | 497 | a.json-toggle.collapsed:before { 498 | transform: rotate(-90deg); 499 | } 500 | 501 | a.json-placeholder { 502 | color: #aaa; 503 | padding: 0 1em; 504 | text-decoration: none; 505 | } 506 | 507 | a.json-placeholder:hover { 508 | text-decoration: underline; 509 | } 510 | 511 | #downList a { 512 | white-space: nowrap; 513 | overflow: hidden; 514 | text-overflow: ellipsis; 515 | display: block; 516 | color: var(--text2-color); 517 | } 518 | 519 | #downList { 520 | overflow: scroll; 521 | height: 60vh; 522 | text-align: left; 523 | display: none; 524 | width: 100%; 525 | border: solid 1px var(--text-color); 526 | } 527 | 528 | .width3rem { 529 | width: 3rem; 530 | } 531 | 532 | .popupAttr { 533 | margin-left: 0.5rem; 534 | } 535 | 536 | .progress-container { 537 | display: flex; 538 | align-items: center; 539 | gap: 10px; 540 | } 541 | 542 | .progress-wrapper { 543 | flex: 1; 544 | } 545 | 546 | .newDownload .downItem { 547 | margin-bottom: 1rem; 548 | } 549 | 550 | .newDownload .downItem .progress-bar { 551 | margin-bottom: 0; 552 | height: 20px; 553 | } 554 | 555 | .newDownload .downItem button { 556 | margin: 0; 557 | } 558 | 559 | .newDownload .downItem .progress { 560 | color: var(--background-color-two); 561 | text-align: center; 562 | transition: width 0.2s; 563 | } 564 | 565 | /** 导航条 **/ 566 | .sidebar { 567 | position: fixed; 568 | top: 0; 569 | left: 0; 570 | width: 10rem; 571 | height: 100%; 572 | padding: 10px; 573 | background-color: var(--background-color-two); 574 | box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1); 575 | overflow-y: auto; 576 | text-align: center; 577 | margin-right: 0; 578 | } 579 | .sidebar ul { 580 | list-style-type: none; 581 | padding: 0; 582 | } 583 | .sidebar li { 584 | margin: 10px 0; 585 | } 586 | .sidebar a { 587 | text-decoration: none; 588 | color: var(--text-color); 589 | display: block; 590 | padding: 5px; 591 | border-radius: 4px; 592 | } 593 | .sidebar a:hover { 594 | background-color: var(--button-hover-color); 595 | } 596 | 597 | .item .send2localType { 598 | margin-right: 196px; 599 | } 600 | .item .send2localType select { 601 | width: 15rem; 602 | } 603 | -------------------------------------------------------------------------------- /css/popup.css: -------------------------------------------------------------------------------- 1 | a { 2 | text-decoration: none; 3 | word-break: break-all; 4 | } 5 | a:hover { 6 | text-decoration: underline; 7 | } 8 | body { 9 | font-family: arial, sans-serif; 10 | font-size: 0.8rem; 11 | width: 40rem; 12 | overflow-x: hidden; 13 | background: var(--background-color); 14 | margin: 0; 15 | } 16 | .fixFirefoxRight { 17 | margin-right: 5px; 18 | } 19 | .panel { 20 | border: 1px solid #ddd0; 21 | margin-bottom: 1px; 22 | } 23 | .panel-heading { 24 | padding: 5px 5px 5px 5px; 25 | background-color: var(--background-color-two); 26 | cursor: pointer; 27 | display: flex; 28 | flex-direction: row; 29 | align-items: center; 30 | } 31 | .panel-heading .name { 32 | flex: auto; 33 | white-space: nowrap; 34 | text-overflow: ellipsis; 35 | overflow: hidden; 36 | margin-right: 0.2rem; 37 | } 38 | .panel .url, 39 | .panel .confirm { 40 | padding: 5px; 41 | } 42 | .icon, 43 | .favicon { 44 | transition: all 0.1s; 45 | width: 1.5rem; 46 | height: 1.5rem; 47 | cursor: pointer; 48 | } 49 | .faviconFlag { 50 | display: none; 51 | } 52 | .icon:hover { 53 | transform: scale(1.1); 54 | } 55 | .icon:active { 56 | transform: scale(0.9); 57 | } 58 | .panel-heading .icon { 59 | padding-left: 2px; 60 | } 61 | .favicon { 62 | padding-right: 2px; 63 | } 64 | .panel-heading .size { 65 | float: right; 66 | font-weight: bold; 67 | } 68 | #Tips, 69 | #TipsFixed { 70 | left: 0; 71 | right: 0; 72 | text-align: center; 73 | z-index: 9999; 74 | pointer-events: none; 75 | color: var(--text2-color); 76 | font-weight: bold; 77 | border: 1px solid #cdcdcd12; 78 | border-radius: 2px; 79 | background: var(--background-color-two); 80 | padding: 0 10px; 81 | margin-bottom: 1px; 82 | } 83 | #TipsFixed { 84 | position: fixed; 85 | display: none; 86 | } 87 | #preview { 88 | max-height: 300px; 89 | max-width: 100%; 90 | text-align: center; 91 | } 92 | button, 93 | .button2 { 94 | padding: 3px 3px 3px 3px; 95 | /* font-size: 0.9rem; */ 96 | } 97 | .Tabs { 98 | display: flex; 99 | } 100 | .TabButton { 101 | text-align: center; 102 | border: solid 1px #c7c7c700; 103 | color: var(--text2-color); 104 | border-radius: 5px 5px 0 0; 105 | cursor: pointer; 106 | width: 50%; 107 | /* display: flex; */ 108 | padding: 3px; 109 | margin: 1px 2px 0 2px; 110 | flex-direction: row; 111 | align-items: baseline; 112 | justify-content: center; 113 | user-select: none; 114 | } 115 | .flex { 116 | display: flex; 117 | } 118 | .TabButton.Active { 119 | background-color: var(--background-color-two); 120 | border-bottom-color: transparent; 121 | font-weight: bold; 122 | } 123 | .TabButton.Active div { 124 | font-weight: bold; 125 | } 126 | .DownCheck { 127 | margin: 0 2px 0 0; 128 | width: 1.2rem; 129 | height: 1.2rem; 130 | flex: 0 0 auto; 131 | } 132 | .TabShow { 133 | display: block !important; 134 | } 135 | #down, 136 | .more { 137 | display: flex; 138 | flex-wrap: wrap; 139 | position: fixed; 140 | width: 100%; 141 | z-index: 999; 142 | background-color: var(--background-color-opacity); 143 | } 144 | #down { 145 | bottom: 0; 146 | justify-content: space-evenly; 147 | } 148 | .more { 149 | display: none; 150 | bottom: 26px; 151 | justify-content: flex-start; 152 | padding-bottom: 2px; 153 | padding-top: 2px; 154 | z-index: 9999; 155 | } 156 | .more button { 157 | margin-left: 0.1rem; 158 | font-size: 12px; 159 | } 160 | #filter { 161 | flex-wrap: wrap; 162 | } 163 | #filter #regular button { 164 | margin-left: 0px; 165 | } 166 | #filter #regular input { 167 | width: 98%; 168 | } 169 | #filter .regular { 170 | margin-left: 5px; 171 | } 172 | #filter #ext { 173 | display: flex; 174 | color: var(--text-color); 175 | } 176 | #filter div { 177 | margin-left: 5px; 178 | } 179 | .flexFilter { 180 | display: flex; 181 | flex-wrap: wrap; 182 | align-items: center; 183 | } 184 | .container { 185 | margin-bottom: 30px; 186 | } 187 | #screenshots { 188 | max-width: 100%; 189 | max-height: 260px; 190 | cursor: pointer; 191 | margin: auto; 192 | } 193 | .flex-end { 194 | justify-content: flex-end; 195 | } 196 | #otherOptions { 197 | margin: 5px; 198 | } 199 | #PlayControl { 200 | display: flex; 201 | align-items: center; 202 | flex-wrap: wrap; 203 | justify-content: space-evenly; 204 | } 205 | #PlayControl .button2, 206 | #PlayControl .button { 207 | margin-left: 2px; 208 | } 209 | #PlayControl #playbackRate { 210 | width: 3em; 211 | height: 20px; 212 | } 213 | #otherOptions select { 214 | margin-top: 2px; 215 | margin-bottom: 2px; 216 | width: 100%; 217 | } 218 | #PlayControl .loop { 219 | margin: 0 5px 0 5px; 220 | } 221 | label { 222 | cursor: pointer; 223 | user-select: none; 224 | } 225 | #PlayControl .volume { 226 | width: 100px; 227 | } 228 | .flexColumn { 229 | display: flex; 230 | flex-direction: column; 231 | align-items: center; 232 | } 233 | .flexRow { 234 | display: flex; 235 | flex-direction: row; 236 | align-items: center; 237 | } 238 | .nowrap { 239 | word-break: keep-all; 240 | } 241 | .otherScript .button2, 242 | .otherFeat .button2 { 243 | width: 100%; 244 | margin-right: 10px; 245 | text-align: center; 246 | } 247 | .otherTips { 248 | text-align: center; 249 | color: var(--text2-color); 250 | font-weight: bold; 251 | } 252 | .moreButton { 253 | display: flex; 254 | } 255 | .moreButton div { 256 | margin-right: 3px; 257 | } 258 | .panel .confirm { 259 | text-align: center; 260 | } 261 | -------------------------------------------------------------------------------- /css/preview.css: -------------------------------------------------------------------------------- 1 | /* 基础样式 */ 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | height: 100vh; 6 | user-select: none; 7 | } 8 | 9 | /* .container { 10 | padding: 10px; 11 | margin: 0 auto; 12 | } */ 13 | 14 | /* 筛选区域 */ 15 | .filters { 16 | display: grid; 17 | gap: 5px; 18 | /* margin-bottom: 10px; */ 19 | background: var(--background-color-two); 20 | padding: 10px; 21 | border-radius: 8px; 22 | /* position: sticky; */ 23 | /* top: 0; */ 24 | /* z-index: 2; */ 25 | } 26 | 27 | .filter-row { 28 | display: flex; 29 | align-items: center; 30 | gap: 10px; 31 | flex-wrap: wrap; 32 | } 33 | 34 | .sort-options { 35 | display: flex; 36 | gap: 15px; 37 | align-items: center; 38 | } 39 | 40 | .sort-group, 41 | .sort-order { 42 | display: flex; 43 | gap: 8px; 44 | } 45 | 46 | #regular { 47 | width: 512px; 48 | } 49 | 50 | input[type="radio"] { 51 | vertical-align: bottom; 52 | } 53 | input[type="checkbox"] { 54 | vertical-align: middle; 55 | } 56 | 57 | /* 文件列表 */ 58 | .file-grid { 59 | display: grid; 60 | grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); 61 | gap: 10px; 62 | padding: 10px; 63 | } 64 | 65 | .file-item { 66 | display: flex; 67 | flex-direction: column; 68 | min-height: 150px; 69 | padding: 8px; 70 | border: 3px solid transparent; 71 | border-radius: 8px; 72 | cursor: pointer; 73 | box-shadow: 0 0 3px var(--button2-color); 74 | max-height: 233px; 75 | transition: all 0.2s; 76 | } 77 | .file-item:hover { 78 | box-shadow: 0 0 10px var(--button2-color); 79 | } 80 | 81 | .file-item.selected { 82 | border-color: var(--button2-color); 83 | background-color: var(--button-hover-color); 84 | /* box-shadow: 0 0 8px var(--button2-color); */ 85 | } 86 | 87 | .file-name { 88 | font-weight: bold; 89 | color: var(--text2-color); 90 | word-break: break-all; 91 | display: -webkit-box; 92 | -webkit-line-clamp: 2; 93 | line-clamp: 2; 94 | -webkit-box-orient: vertical; 95 | overflow: hidden; 96 | } 97 | 98 | /* 预览容器 */ 99 | .preview-container { 100 | margin: auto 0; 101 | text-align: center; 102 | } 103 | 104 | .preview-container .icon { 105 | /* height: 150px; */ 106 | max-height: 150px; 107 | max-width: 233px; 108 | } 109 | 110 | /* .preview-image { 111 | max-width: 100%; 112 | max-height: 200px; 113 | object-fit: contain; 114 | } */ 115 | 116 | .video-preview { 117 | width: 100%; 118 | max-height: 150px; 119 | } 120 | 121 | .video-preview video { 122 | max-width: 100%; 123 | max-height: 100%; 124 | } 125 | 126 | /* 底部信息栏 */ 127 | .bottom-row { 128 | /* margin-top: auto; */ 129 | display: flex; 130 | justify-content: space-between; 131 | align-items: center; 132 | gap: 2px; 133 | } 134 | 135 | .file-info { 136 | margin: 0 auto; 137 | flex-shrink: 0; 138 | } 139 | 140 | /* 操作图标 */ 141 | .actions { 142 | display: flex; 143 | gap: 2px; 144 | justify-content: center; 145 | margin-bottom: -5px; 146 | } 147 | 148 | .actions .icon { 149 | width: 23px; 150 | transition: all 0.1s; 151 | opacity: 0.5; 152 | } 153 | 154 | .actions .icon:hover { 155 | /* transform: scale(1.1); */ 156 | opacity: 1; 157 | } 158 | 159 | .actions .icon:active { 160 | transform: scale(0.9); 161 | } 162 | 163 | /* 全屏预览 */ 164 | .play-container, 165 | .image-container { 166 | position: fixed; 167 | top: 0; 168 | left: 0; 169 | width: 100vw; 170 | height: 100vh; 171 | background: rgba(0, 0, 0, 0.8); 172 | display: flex; 173 | justify-content: center; 174 | align-items: center; 175 | z-index: 4; 176 | } 177 | 178 | .play-container.hide, 179 | .image-container.hide, 180 | .video-preview.hide { 181 | display: none; 182 | } 183 | 184 | #video-player, 185 | #image-player { 186 | max-width: 90vw; 187 | max-height: 90vh; 188 | width: auto; 189 | height: auto; 190 | object-fit: contain; 191 | } 192 | 193 | /* 框选 */ 194 | #selection-box { 195 | position: absolute; 196 | border: 1px solid var(--button2-color); 197 | background-color: var(--button-active-color); 198 | pointer-events: none; 199 | z-index: 3; 200 | display: none; 201 | } 202 | 203 | /* 提示框 */ 204 | .alert-box { 205 | position: fixed; 206 | top: 50%; 207 | left: 50%; 208 | transform: translate(-50%, -50%); 209 | background: rgba(0, 0, 0, 0.8); 210 | color: white; 211 | padding: 20px 40px; 212 | border-radius: 8px; 213 | opacity: 0; 214 | visibility: hidden; 215 | transition: all 0.3s ease; 216 | z-index: 1000; 217 | } 218 | .alert-box.active { 219 | opacity: 1; 220 | visibility: visible; 221 | } 222 | 223 | /* 分页组件样式 */ 224 | .pagination { 225 | display: flex; 226 | justify-content: center; 227 | align-items: center; 228 | gap: 8px; 229 | /* margin-top: 20px; */ 230 | padding: 15px; 231 | background: var(--background-color-two); 232 | border-radius: 8px; 233 | } 234 | .pagination.hide { 235 | display: none; 236 | } 237 | .page-numbers { 238 | display: flex; 239 | gap: 5px; 240 | flex-wrap: wrap; 241 | } 242 | -------------------------------------------------------------------------------- /css/public.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* 两个背景色 两个文字以及链接文字配色 */ 3 | --background-color: #fff; 4 | --background-color-opacity: #ffffffea; 5 | --background-color-two: #f5f5f5; 6 | --text-color: #000; 7 | --text-error-color: #ff0000; 8 | --text2-color: rgb(26, 115, 232); 9 | --link-color: #3079ed; 10 | 11 | /* 设置页面 设置box 背景色 */ 12 | --optionBox-color: var(--background-color); 13 | 14 | /* 两个按钮 配色 边框 */ 15 | --button-color: #fff; 16 | --button-text-color: rgb(26, 115, 232); 17 | --button-border: solid 1px #c7c7c780; 18 | --button-hover-color: rgb(66 133 244 / 4%); 19 | --button-active-color: rgb(66 133 244 / 10%); 20 | 21 | --button2-color: rgb(26, 115, 232); 22 | --button2-text-color: #fff; 23 | --button2-border: solid 1px #c7c7c780; 24 | --button2-hover-color: rgb(26 115 232 / 90%); 25 | --button2-active-color: rgb(26 115 232 / 50%); 26 | 27 | /* 滚动条配色 */ 28 | --scrollbar-track-color: #f5f5f500; 29 | --scrollbar-thumb-color: #1a73e8; 30 | 31 | /* 设置页面 滑动开关配色 */ 32 | --switch-off-color: rgb(189, 193, 198); 33 | --switch-on-color: rgb(26, 115, 232); 34 | --switch-round-color: #fff; 35 | 36 | /* input textarea select 边框配色 */ 37 | --input-border: solid 1px #000; 38 | } 39 | html { 40 | color: var(--text-color); 41 | background: var(--background-color); 42 | scrollbar-width: thin; 43 | } 44 | input, 45 | textarea, 46 | select { 47 | color: var(--text-color); 48 | background: var(--background-color); 49 | scrollbar-width: thin; 50 | border: var(--input-border); 51 | } 52 | a, 53 | a:link, 54 | a:visited { 55 | color: var(--link-color); 56 | } 57 | button, 58 | .button, 59 | .button2 { 60 | border-radius: 4px; 61 | cursor: pointer; 62 | margin: 0 0 3px 0; 63 | user-select: none; 64 | } 65 | button, 66 | .button { 67 | background: var(--button-color); 68 | border: var(--button-border); 69 | color: var(--button-text-color); 70 | } 71 | button:hover, 72 | .button:hover { 73 | background: var(--button-hover-color); 74 | } 75 | button:active, 76 | .button:active { 77 | background: var(--button-active-color); 78 | } 79 | .button2 { 80 | background: var(--button2-color); 81 | border: var(--button2-border); 82 | color: var(--button2-text-color); 83 | } 84 | .button2:hover { 85 | background: var(--button2-hover-color); 86 | } 87 | .button2:active { 88 | background: var(--button2-active-color); 89 | } 90 | button:disabled, 91 | .button:disabled, 92 | .button2:disabled, 93 | .disabled { 94 | background-color: #ccc; 95 | color: #666; 96 | cursor: not-allowed; 97 | opacity: 0.6; 98 | } 99 | .bold { 100 | font-weight: bold; 101 | } 102 | .hide { 103 | display: none; 104 | } 105 | .textColor { 106 | color: var(--text2-color); 107 | } 108 | .width100 { 109 | width: 100%; 110 | } 111 | .height100 { 112 | height: 100%; 113 | } 114 | .line { 115 | border-top: solid 1px rgb(0 0 0 / 50%); 116 | margin: 10px 0 10px 0; 117 | } 118 | .no-drop { 119 | background-color: #ccc !important; 120 | cursor: no-drop; 121 | color: var(--button2-text-color); 122 | } 123 | .icon { 124 | -webkit-user-drag: none; 125 | } 126 | /*定义整个滚动条高宽及背景:高宽分别对应横竖滚动条的尺寸*/ 127 | ::-webkit-scrollbar { 128 | width: 5px; 129 | } 130 | /*定义滚动条轨道:内阴影+圆角*/ 131 | ::-webkit-scrollbar-track { 132 | background-color: var(--scrollbar-track-color); 133 | } 134 | /*定义滑块:内阴影+圆角*/ 135 | ::-webkit-scrollbar-thumb { 136 | border-radius: 10px; 137 | background-color: var(--scrollbar-thumb-color); 138 | } 139 | @media (prefers-color-scheme: dark) { 140 | :root { 141 | --background-color: #0f172a; 142 | --background-color-opacity: #0f172aea; 143 | --background-color-two: #1e293b; 144 | --text-color: #fff; 145 | --text-error-color: #ff0000; 146 | --text2-color: #fff; 147 | --link-color: #94a3b8; 148 | 149 | --optionBox-color: var(--background-color-two); 150 | 151 | --button-color: #161b22; 152 | --button-border: solid 1px #c7c7c780; 153 | --button-text-color: #fff; 154 | --button-hover-color: rgb(66 133 244 / 4%); 155 | --button-active-color: rgb(66 133 244 / 10%); 156 | 157 | --button2-color: rgb(26 115 232 / 50%); 158 | --button2-border: solid 1px #c7c7c780; 159 | --button2-text-color: #fff; 160 | --button2-hover-color: rgb(26 115 232 / 90%); 161 | --button2-active-color: rgb(26 115 232 / 50%); 162 | 163 | --scrollbar-track-color: #f5f5f500; 164 | --scrollbar-thumb-color: #1a73e8; 165 | 166 | --switch-off-color: rgb(189, 193, 198); 167 | --switch-on-color: rgb(26 115 232 / 50%); 168 | --switch-round-color: #fff; 169 | 170 | --input-border: solid 1px #ffffffb6; 171 | } 172 | img.regex { 173 | content: url(../img/regex-dark.png); 174 | } 175 | img.copy { 176 | content: url(../img/copy-dark.png); 177 | } 178 | img.parsing { 179 | content: url(../img/parsing-dark.png); 180 | } 181 | img.play { 182 | content: url(../img/play-dark.png); 183 | } 184 | img.download { 185 | content: url(../img/download-dark.svg); 186 | } 187 | img.qrcode { 188 | content: url(../img/qrcode-dark.png); 189 | } 190 | img.cat-down { 191 | content: url(../img/cat-down-dark.png); 192 | } 193 | img.aria2 { 194 | content: url(../img/aria2-dark.png); 195 | } 196 | img.invoke { 197 | content: url(../img/invoke-dark.svg); 198 | } 199 | img.send { 200 | content: url(../img/send-dark.svg); 201 | } 202 | img.delete { 203 | content: url(../img/delete-dark.svg); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /downloader.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | titleDownload 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |

20 |
21 | 22 | 23 | 24 | 26 |
27 |
28 |
29 | 30 |
31 |
32 |

33 |
34 |
35 |
36 |
37 | 38 | 39 | 40 | 41 | 43 |
44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /img/aria2-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xifangczy/cat-catch/9461680524c1a452cba90f2c35d5f2f754d9003b/img/aria2-dark.png -------------------------------------------------------------------------------- /img/aria2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xifangczy/cat-catch/9461680524c1a452cba90f2c35d5f2f754d9003b/img/aria2.png -------------------------------------------------------------------------------- /img/cat-down-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xifangczy/cat-catch/9461680524c1a452cba90f2c35d5f2f754d9003b/img/cat-down-dark.png -------------------------------------------------------------------------------- /img/cat-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xifangczy/cat-catch/9461680524c1a452cba90f2c35d5f2f754d9003b/img/cat-down.png -------------------------------------------------------------------------------- /img/copy-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xifangczy/cat-catch/9461680524c1a452cba90f2c35d5f2f754d9003b/img/copy-dark.png -------------------------------------------------------------------------------- /img/copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xifangczy/cat-catch/9461680524c1a452cba90f2c35d5f2f754d9003b/img/copy.png -------------------------------------------------------------------------------- /img/delete-dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/delete.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/download-dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/icon-disable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xifangczy/cat-catch/9461680524c1a452cba90f2c35d5f2f754d9003b/img/icon-disable.png -------------------------------------------------------------------------------- /img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xifangczy/cat-catch/9461680524c1a452cba90f2c35d5f2f754d9003b/img/icon.png -------------------------------------------------------------------------------- /img/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xifangczy/cat-catch/9461680524c1a452cba90f2c35d5f2f754d9003b/img/icon128.png -------------------------------------------------------------------------------- /img/invoke-dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/invoke.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/music.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/parsing-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xifangczy/cat-catch/9461680524c1a452cba90f2c35d5f2f754d9003b/img/parsing-dark.png -------------------------------------------------------------------------------- /img/parsing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xifangczy/cat-catch/9461680524c1a452cba90f2c35d5f2f754d9003b/img/parsing.png -------------------------------------------------------------------------------- /img/play-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xifangczy/cat-catch/9461680524c1a452cba90f2c35d5f2f754d9003b/img/play-dark.png -------------------------------------------------------------------------------- /img/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xifangczy/cat-catch/9461680524c1a452cba90f2c35d5f2f754d9003b/img/play.png -------------------------------------------------------------------------------- /img/qrcode-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xifangczy/cat-catch/9461680524c1a452cba90f2c35d5f2f754d9003b/img/qrcode-dark.png -------------------------------------------------------------------------------- /img/qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xifangczy/cat-catch/9461680524c1a452cba90f2c35d5f2f754d9003b/img/qrcode.png -------------------------------------------------------------------------------- /img/regex-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xifangczy/cat-catch/9461680524c1a452cba90f2c35d5f2f754d9003b/img/regex-dark.png -------------------------------------------------------------------------------- /img/regex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xifangczy/cat-catch/9461680524c1a452cba90f2c35d5f2f754d9003b/img/regex.png -------------------------------------------------------------------------------- /img/send-dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/send.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /install.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 猫抓 cat-catch 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |

恭喜 猫抓 17 | 扩展已成功安装 !

18 |

19 | Installation successful !

20 |

希望本扩展能帮助到你。请仔细阅读以下协议和免责声明,使用过程中出现问题,请到 https://cat-catch.bmmmd.com/issues 提交问题

22 |
23 |

I hope this extension can help you.

24 |

Please read the following agreement and disclaimer carefully. If you encounter any issues during use, 25 | please submit them to GitHub Issues

26 |
27 |
28 | 29 |
30 |

隐私政策 / Privacy Policy

31 |
32 |

本扩展收集所有信息都在本地储存处理,不会发送到远程服务器,不包含任何跟踪器。

33 |
34 |

The extension collects and processes all information locally without sending it to remote servers and 35 | does not include any trackers.

36 |
37 |
38 | 39 |
40 |

免责声明 / Disclaimer

41 |
42 |

本扩展仅供下载用户拥有版权或已获授权的视频,禁止用于下载受版权保护且未经授权的内容。用户需自行承担使用本工具的全部法律责任,开发者不对用户的任何行为负责。本工具按“原样”提供,开发者不承担任何直接或间接责任。 43 |

44 |
45 |

This extension is intended for downloading videos that you own or have authorized access to. It is 46 | prohibited to use this Tool for downloading copyrighted content without permission. Users are solely 47 | responsible for their actions, and the developer is not liable for any user behavior. This Tool is 48 | provided "as-is," and the developer assumes no direct or indirect liability.

49 |
50 |
51 | 52 |
53 |

54 |
55 |

点击“同意”或“关闭本页面”即表示您已阅读并同意以上内容。

56 |

By clicking "Agree" or "Close this page," you confirm that you have read and agree to the above 57 | terms.

58 | 59 | 60 |
61 |
62 |
63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /js/content-script.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var _videoObj = []; 3 | var _videoSrc = []; 4 | var _key = []; 5 | chrome.runtime.onMessage.addListener(function (Message, sender, sendResponse) { 6 | if (chrome.runtime.lastError) { return; } 7 | // 获取页面视频对象 8 | if (Message.Message == "getVideoState") { 9 | let videoObj = []; 10 | let videoSrc = []; 11 | document.querySelectorAll("video, audio").forEach(function (video) { 12 | if (video.currentSrc != "" && video.currentSrc != undefined) { 13 | videoObj.push(video); 14 | videoSrc.push(video.currentSrc); 15 | } 16 | }); 17 | const iframe = document.querySelectorAll("iframe"); 18 | if (iframe.length > 0) { 19 | iframe.forEach(function (iframe) { 20 | if (iframe.contentDocument == null) { return true; } 21 | iframe.contentDocument.querySelectorAll("video, audio").forEach(function (video) { 22 | if (video.currentSrc != "" && video.currentSrc != undefined) { 23 | videoObj.push(video); 24 | videoSrc.push(video.currentSrc); 25 | } 26 | }); 27 | }); 28 | } 29 | if (videoObj.length > 0) { 30 | if (videoObj.length !== _videoObj.length || videoSrc.toString() !== _videoSrc.toString()) { 31 | _videoSrc = videoSrc; 32 | _videoObj = videoObj; 33 | } 34 | Message.index = Message.index == -1 ? 0 : Message.index; 35 | const video = videoObj[Message.index]; 36 | const timePCT = video.currentTime / video.duration * 100; 37 | sendResponse({ 38 | time: timePCT, 39 | currentTime: video.currentTime, 40 | duration: video.duration, 41 | volume: video.volume, 42 | count: _videoObj.length, 43 | src: _videoSrc, 44 | paused: video.paused, 45 | loop: video.loop, 46 | speed: video.playbackRate, 47 | muted: video.muted, 48 | type: video.tagName.toLowerCase() 49 | }); 50 | return true; 51 | } 52 | sendResponse({ count: 0 }); 53 | return true; 54 | } 55 | // 速度控制 56 | if (Message.Message == "speed") { 57 | _videoObj[Message.index].playbackRate = Message.speed; 58 | return true; 59 | } 60 | // 画中画 61 | if (Message.Message == "pip") { 62 | if (document.pictureInPictureElement) { 63 | try { document.exitPictureInPicture(); } catch (e) { return true; } 64 | sendResponse({ state: false }); 65 | return true; 66 | } 67 | try { _videoObj[Message.index].requestPictureInPicture(); } catch (e) { return true; } 68 | sendResponse({ state: true }); 69 | return true; 70 | } 71 | // 全屏 72 | if (Message.Message == "fullScreen") { 73 | if (document.fullscreenElement) { 74 | try { document.exitFullscreen(); } catch (e) { return true; } 75 | sendResponse({ state: false }); 76 | return true; 77 | } 78 | setTimeout(function () { 79 | try { _videoObj[Message.index].requestFullscreen(); } catch (e) { return true; } 80 | }, 500); 81 | sendResponse({ state: true }); 82 | return true; 83 | } 84 | // 播放 85 | if (Message.Message == "play") { 86 | _videoObj[Message.index].play(); 87 | return true; 88 | } 89 | // 暂停 90 | if (Message.Message == "pause") { 91 | _videoObj[Message.index].pause(); 92 | return true; 93 | } 94 | // 循环播放 95 | if (Message.Message == "loop") { 96 | _videoObj[Message.index].loop = Message.action; 97 | return true; 98 | } 99 | // 设置音量 100 | if (Message.Message == "setVolume") { 101 | _videoObj[Message.index].volume = Message.volume; 102 | sendResponse("ok"); 103 | return true; 104 | } 105 | // 静音 106 | if (Message.Message == "muted") { 107 | _videoObj[Message.index].muted = Message.action; 108 | return true; 109 | } 110 | // 设置视频进度 111 | if (Message.Message == "setTime") { 112 | const time = Message.time * _videoObj[Message.index].duration / 100; 113 | _videoObj[Message.index].currentTime = time; 114 | sendResponse("ok"); 115 | return true; 116 | } 117 | // 截图视频图片 118 | if (Message.Message == "screenshot") { 119 | try { 120 | const video = _videoObj[Message.index]; 121 | const canvas = document.createElement("canvas"); 122 | canvas.width = video.videoWidth; 123 | canvas.height = video.videoHeight; 124 | canvas.getContext("2d").drawImage(video, 0, 0, canvas.width, canvas.height); 125 | const link = document.createElement("a"); 126 | link.href = canvas.toDataURL("image/jpeg"); 127 | link.download = `${location.hostname}-${secToTime(video.currentTime)}.jpg`; 128 | link.click(); 129 | delete canvas; 130 | delete link; 131 | sendResponse("ok"); 132 | return true; 133 | } catch (e) { console.log(e); return true; } 134 | } 135 | if (Message.Message == "getKey") { 136 | sendResponse(_key); 137 | return true; 138 | } 139 | if (Message.Message == "ffmpeg") { 140 | if (!Message.files) { 141 | window.postMessage(Message); 142 | sendResponse("ok"); 143 | return true; 144 | } 145 | Message.quantity ??= Message.files.length; 146 | for (let item of Message.files) { 147 | const data = { ...Message, ...item }; 148 | data.type = item.type ?? "video"; 149 | if (data.data instanceof Blob) { 150 | window.postMessage(data); 151 | } else { 152 | fetch(data.data) 153 | .then(response => response.blob()) 154 | .then(blob => { 155 | data.data = blob; 156 | window.postMessage(data); 157 | }); 158 | } 159 | } 160 | sendResponse("ok"); 161 | return true; 162 | } 163 | if (Message.Message == "getPage") { 164 | if (Message.find) { 165 | const DOM = document.querySelector(Message.find); 166 | DOM ? sendResponse(DOM.innerHTML) : sendResponse(""); 167 | return true; 168 | } 169 | sendResponse(document.documentElement.outerHTML); 170 | return true; 171 | } 172 | }); 173 | 174 | // Heart Beat 175 | var Port; 176 | function connect() { 177 | Port = chrome.runtime.connect(chrome.runtime.id, { name: "HeartBeat" }); 178 | Port.postMessage("HeartBeat"); 179 | Port.onMessage.addListener(function (message, Port) { return true; }); 180 | Port.onDisconnect.addListener(connect); 181 | } 182 | connect(); 183 | 184 | function secToTime(sec) { 185 | let time = ""; 186 | let hour = Math.floor(sec / 3600); 187 | let min = Math.floor((sec % 3600) / 60); 188 | sec = Math.floor(sec % 60); 189 | if (hour > 0) { time = hour + "'"; } 190 | if (min < 10) { time += "0"; } 191 | time += min + "'"; 192 | if (sec < 10) { time += "0"; } 193 | time += sec; 194 | return time; 195 | } 196 | window.addEventListener("message", (event) => { 197 | if (!event.data || !event.data.action) { return; } 198 | if (event.data.action == "catCatchAddMedia") { 199 | if (!event.data.url) { return; } 200 | chrome.runtime.sendMessage({ 201 | Message: "addMedia", 202 | url: event.data.url, 203 | href: event.data.href ?? event.source.location.href, 204 | extraExt: event.data.ext, 205 | mime: event.data.mime, 206 | requestHeaders: { referer: event.data.referer }, 207 | requestId: event.data.requestId 208 | }); 209 | } 210 | if (event.data.action == "catCatchAddKey") { 211 | let key = event.data.key; 212 | if (key instanceof ArrayBuffer || key instanceof Array) { 213 | key = ArrayToBase64(key); 214 | } 215 | if (!key || _key.includes(key)) { return; } 216 | _key.push(key); 217 | chrome.runtime.sendMessage({ 218 | Message: "send2local", 219 | action: "addKey", 220 | data: key, 221 | }); 222 | chrome.runtime.sendMessage({ 223 | Message: "popupAddKey", 224 | data: key, 225 | url: event.data.url, 226 | }); 227 | } 228 | if (event.data.action == "catCatchFFmpeg") { 229 | if (!event.data.use || 230 | !event.data.files || 231 | !event.data.files instanceof Array || 232 | event.data.files.length == 0 233 | ) { return; } 234 | event.data.title = event.data.title ?? document.title ?? new Date().getTime().toString(); 235 | event.data.title = event.data.title.replaceAll('"', "").replaceAll("'", "").replaceAll(" ", ""); 236 | let data = { 237 | Message: event.data.action, 238 | action: event.data.use, 239 | files: event.data.files, 240 | url: event.data.href ?? event.source.location.href, 241 | }; 242 | data = { ...event.data, ...data }; 243 | chrome.runtime.sendMessage(data); 244 | } 245 | if (event.data.action == "catCatchFFmpegResult") { 246 | if (!event.data.state || !event.data.tabId) { return; } 247 | chrome.runtime.sendMessage({ Message: "catCatchFFmpegResult", ...event.data }); 248 | } 249 | if (event.data.action == "catCatchToBackground") { 250 | delete event.data.action; 251 | chrome.runtime.sendMessage(event.data); 252 | } 253 | }, false); 254 | 255 | function ArrayToBase64(data) { 256 | try { 257 | let bytes = new Uint8Array(data); 258 | let binary = ""; 259 | for (let i = 0; i < bytes.byteLength; i++) { 260 | binary += String.fromCharCode(bytes[i]); 261 | } 262 | if (typeof _btoa == "function") { 263 | return _btoa(binary); 264 | } 265 | return btoa(binary); 266 | } catch (e) { 267 | return false; 268 | } 269 | } 270 | })(); -------------------------------------------------------------------------------- /js/firefox.js: -------------------------------------------------------------------------------- 1 | // 兼容Firefox 2 | if (typeof (browser) == "object") { 3 | function importScripts() { 4 | for (let script of arguments) { 5 | const js = document.createElement('script'); 6 | js.src = script; 7 | document.head.appendChild(js); 8 | } 9 | } 10 | 11 | // browser.windows.onFocusChanged.addListener 少一个参数 12 | const _onFocusChanged = chrome.windows.onFocusChanged.addListener; 13 | chrome.windows.onFocusChanged.addListener = function (listener) { 14 | _onFocusChanged(listener); 15 | }; 16 | 17 | browser.runtime.onInstalled.addListener(({ reason }) => { 18 | if (reason == "install") { 19 | browser.tabs.create({ url: "install.html" }); 20 | } 21 | }); 22 | } -------------------------------------------------------------------------------- /js/i18n.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | document.querySelectorAll('[data-i18n]').forEach(function (element) { 3 | element.innerHTML = i18n(element.dataset.i18n) ?? element.dataset.i18n; 4 | }); 5 | document.querySelectorAll('[data-i18n-outer]').forEach(function (element) { 6 | element.outerHTML = i18n(element.dataset.i18nOuter) ?? element.dataset.i18nOuter; 7 | }); 8 | document.querySelectorAll('i18n').forEach(function (element) { 9 | element.outerHTML = i18n(element.innerHTML) ?? element.innerHTML; 10 | }); 11 | document.querySelectorAll('[data-i18n-placeholder]').forEach(function (element) { 12 | element.setAttribute('placeholder', i18n(element.dataset.i18nPlaceholder) ?? element.dataset.i18nPlaceholder); 13 | }); 14 | document.title = i18n(document.title) ?? document.title; 15 | })(); -------------------------------------------------------------------------------- /js/install.js: -------------------------------------------------------------------------------- 1 | document.getElementById('installYes').focus(); 2 | document.getElementById('installYes').addEventListener('click', function () { 3 | closeTab(); 4 | }); 5 | document.getElementById('installUninstallSelf').addEventListener('click', function () { 6 | chrome.management.uninstallSelf({ showConfirmDialog: true }); 7 | }); 8 | 9 | if (/Mobile|Android|iPhone|iPad/i.test(navigator.userAgent)) { 10 | document.getElementById('installYes').style.fontSize = '2rem'; 11 | document.getElementById('installUninstallSelf').style.fontSize = '2rem'; 12 | } -------------------------------------------------------------------------------- /js/json.js: -------------------------------------------------------------------------------- 1 | // url 参数解析 2 | const params = new URL(location.href).searchParams; 3 | var _url = params.get("url"); 4 | // const _referer = params.get("referer"); 5 | const _requestHeaders = params.get("requestHeaders"); 6 | 7 | // 修改当前标签下的所有xhr的requestHeaders 8 | let requestHeaders = JSONparse(_requestHeaders); 9 | setRequestHeaders(requestHeaders, () => { awaitG(init); }); 10 | 11 | function init() { 12 | $(``).appendTo("head"); 13 | var jsonContent = ""; 14 | var options = { 15 | collapsed: true, 16 | rootCollapsable: false, 17 | withQuotes: false, 18 | withLinks: true 19 | }; 20 | 21 | if (isEmpty(_url)) { 22 | $("#jsonCustom").show(); $("#main").hide(); 23 | $("#format").click(function () { 24 | _url = $("#jsonUrl").val().trim(); 25 | if (isEmpty(_url)) { 26 | let jsonText = $("#jsonText").val(); 27 | jsonContent = JSON.parse(jsonText); 28 | renderJson(); 29 | $("#jsonCustom").hide(); $("#main").show(); 30 | return; 31 | } 32 | getJson(_url); 33 | }); 34 | } else { 35 | getJson(_url); 36 | } 37 | 38 | function getJson(url) { 39 | $("#jsonCustom").hide(); $("#main").show(); 40 | $.ajax({ 41 | url: url, 42 | dataType: "text", 43 | }).fail(function (result) { 44 | console.log(result); 45 | $('#json-renderer').html(i18n.fileRetrievalFailed); 46 | $("#collapsed").hide(); 47 | }).done(function (result) { 48 | // console.log(result); 49 | result = result.replace(/^try{/, "").replace(/}catch\(e\){.*}$/ig, ""); //去除try{}catch(e){} 50 | try { 51 | jsonContent = JSON.parse(result); 52 | } catch (e) { 53 | console.log(e); 54 | let regexp = [ 55 | /^.*=({.*}).*$/, 56 | /^.*\(({.*})\).*$/ 57 | ] 58 | for (let regex of regexp) { 59 | let res = new RegExp(regex, "ig").exec(result); 60 | if (res) { 61 | // console.log(res); 62 | result = res[1]; 63 | break; 64 | } 65 | } 66 | // console.log(result); 67 | jsonContent = JSON.parse(result); 68 | } 69 | renderJson(); 70 | }); 71 | } 72 | 73 | function renderJson() { 74 | $('#json-renderer').jsonViewer(jsonContent, options); 75 | } 76 | $("#collapsed").click(function () { 77 | options.collapsed = !options.collapsed; 78 | if (options.collapsed) { 79 | collapsed.innerHTML = i18n.expandAllNodes; 80 | } else { 81 | collapsed.innerHTML = i18n.collapseAllNodes; 82 | } 83 | renderJson(); 84 | }); 85 | } -------------------------------------------------------------------------------- /js/m3u8.downloader.js: -------------------------------------------------------------------------------- 1 | class Downloader { 2 | constructor(fragments = [], thread = 6) { 3 | this.fragments = fragments; // 切片列表 4 | this.allFragments = fragments; // 储存所有原始切片列表 5 | this.thread = thread; // 线程数 6 | this.events = {}; // events 7 | this.decrypt = null; // 解密函数 8 | this.transcode = null; // 转码函数 9 | this.init(); 10 | } 11 | /** 12 | * 初始化所有变量 13 | */ 14 | init() { 15 | this.index = 0; // 当前任务索引 16 | this.buffer = []; // 储存的buffer 17 | this.state = 'waiting'; // 下载器状态 waiting running done abort 18 | this.success = 0; // 成功下载数量 19 | this.errorList = new Set(); // 下载错误的列表 20 | this.buffersize = 0; // 已下载buffer大小 21 | this.duration = 0; // 已下载时长 22 | this.pushIndex = 0; // 推送顺序下载索引 23 | this.controller = []; // 储存中断控制器 24 | this.running = 0; // 正在下载数量 25 | } 26 | /** 27 | * 设置监听 28 | * @param {string} eventName 监听名 29 | * @param {Function} callBack 30 | */ 31 | on(eventName, callBack) { 32 | if (this.events[eventName]) { 33 | this.events[eventName].push(callBack); 34 | } else { 35 | this.events[eventName] = [callBack]; 36 | } 37 | } 38 | /** 39 | * 触发监听器 40 | * @param {string} eventName 监听名 41 | * @param {...any} args 42 | */ 43 | emit(eventName, ...args) { 44 | if (this.events[eventName]) { 45 | this.events[eventName].forEach(callBack => { 46 | callBack(...args); 47 | }); 48 | } 49 | } 50 | /** 51 | * 设定解密函数 52 | * @param {Function} callback 53 | */ 54 | setDecrypt(callback) { 55 | this.decrypt = callback; 56 | } 57 | /** 58 | * 设定转码函数 59 | * @param {Function} callback 60 | */ 61 | setTranscode(callback) { 62 | this.transcode = callback; 63 | } 64 | /** 65 | * 停止下载 没有目标 停止所有线程 66 | * @param {number} index 停止下载目标 67 | */ 68 | stop(index = undefined) { 69 | if (index !== undefined) { 70 | this.controller[index] && this.controller[index].abort(); 71 | return; 72 | } 73 | this.controller.forEach(controller => { controller.abort() }); 74 | this.state = 'abort'; 75 | } 76 | /** 77 | * 检查对象是否错误列表内 78 | * @param {object} fragment 切片对象 79 | * @returns {boolean} 80 | */ 81 | isErrorItem(fragment) { 82 | return this.errorList.has(fragment); 83 | } 84 | /** 85 | * 返回所有错误列表 86 | */ 87 | get errorItem() { 88 | return this.errorList; 89 | } 90 | /** 91 | * 按照顺序推送buffer数据 92 | */ 93 | sequentialPush() { 94 | if (!this.events["sequentialPush"]) { return; } 95 | for (; this.pushIndex < this.fragments.length; this.pushIndex++) { 96 | if (this.buffer[this.pushIndex]) { 97 | this.emit('sequentialPush', this.buffer[this.pushIndex]); 98 | delete this.buffer[this.pushIndex]; 99 | continue; 100 | } 101 | break; 102 | } 103 | } 104 | /** 105 | * 限定下载范围 106 | * @param {number} start 下载范围 开始索引 107 | * @param {number} end 下载范围 结束索引 108 | * @returns {boolean} 109 | */ 110 | range(start = 0, end = this.fragments.length) { 111 | if (start > end) { 112 | this.emit('error', 'start > end'); 113 | return false; 114 | } 115 | if (end > this.fragments.length) { 116 | this.emit('error', 'end > total'); 117 | return false; 118 | } 119 | if (start >= this.fragments.length) { 120 | this.emit('error', 'start >= total'); 121 | return false; 122 | } 123 | if (start != 0 || end != this.fragments.length) { 124 | this.fragments = this.fragments.slice(start, end); 125 | // 更改过下载范围 重新设定index 126 | this.fragments.forEach((fragment, index) => { 127 | fragment.index = index; 128 | }); 129 | } 130 | // 总数为空 抛出错误 131 | if (this.fragments.length == 0) { 132 | this.emit('error', 'List is empty'); 133 | return false; 134 | } 135 | return true; 136 | } 137 | /** 138 | * 获取切片总数量 139 | * @returns {number} 140 | */ 141 | get total() { 142 | return this.fragments.length; 143 | } 144 | /** 145 | * 获取切片总时间 146 | * @returns {number} 147 | */ 148 | get totalDuration() { 149 | return this.fragments.reduce((total, fragment) => total + fragment.duration, 0); 150 | } 151 | /** 152 | * 切片对象数组的 setter getter 153 | */ 154 | set fragments(fragments) { 155 | // 增加index参数 为多线程异步下载 根据index属性顺序保存 156 | this._fragments = fragments.map((fragment, index) => ({ ...fragment, index })); 157 | } 158 | get fragments() { 159 | return this._fragments; 160 | } 161 | /** 162 | * 获取 #EXT-X-MAP 标签的文件url 163 | * @returns {string} 164 | */ 165 | get mapTag() { 166 | if (this.fragments[0].initSegment && this.fragments[0].initSegment.url) { 167 | return this.fragments[0].initSegment.url; 168 | } 169 | return ""; 170 | } 171 | /** 172 | * 添加一条新资源 173 | * @param {Object} fragment 174 | */ 175 | push(fragment) { 176 | fragment.index = this.fragments.length; 177 | this.fragments.push(fragment); 178 | } 179 | /** 180 | * 下载器 使用fetch下载文件 181 | * @param {object} fragment 重新下载的对象 182 | */ 183 | downloader(fragment = null) { 184 | if (this.state === 'abort') { return; } 185 | // 是否直接下载对象 186 | const directDownload = !!fragment; 187 | 188 | // 非直接下载对象 从this.fragments获取下一条资源 若不存在跳出 189 | if (!directDownload && !this.fragments[this.index]) { return; } 190 | 191 | // fragment是数字 直接从this.fragments获取 192 | if (typeof fragment === 'number') { 193 | fragment = this.fragments[fragment]; 194 | } 195 | 196 | // 不存在下载对象 从提取fragments 197 | fragment ??= this.fragments[this.index++]; 198 | this.state = 'running'; 199 | this.running++; 200 | 201 | // 资源已下载 跳过 202 | // if (this.buffer[fragment.index]) { return; } 203 | 204 | // 停止下载控制器 205 | const controller = new AbortController(); 206 | this.controller[fragment.index] = controller; 207 | const options = { signal: controller.signal }; 208 | 209 | // 下载前触发事件 210 | this.emit('start', fragment, options); 211 | 212 | // 开始下载 213 | fetch(fragment.url, options) 214 | .then(response => { 215 | if (!response.ok) { 216 | throw new Error(response.status, { cause: 'HTTPError' }); 217 | } 218 | const reader = response.body.getReader(); 219 | const contentLength = parseInt(response.headers.get('content-length')) || 0; 220 | fragment.contentType = response.headers.get('content-type') ?? 'null'; 221 | let receivedLength = 0; 222 | const chunks = []; 223 | const pump = async () => { 224 | while (true) { 225 | const { value, done } = await reader.read(); 226 | if (done) { break; } 227 | 228 | // 流式下载 229 | fragment.fileStream ? fragment.fileStream.write(new Uint8Array(value)) : chunks.push(value); 230 | 231 | receivedLength += value.length; 232 | this.emit('itemProgress', fragment, false, receivedLength, contentLength, value); 233 | } 234 | if (fragment.fileStream) { 235 | return new ArrayBuffer(); 236 | } 237 | const allChunks = new Uint8Array(receivedLength); 238 | let position = 0; 239 | for (const chunk of chunks) { 240 | allChunks.set(chunk, position); 241 | position += chunk.length; 242 | } 243 | this.emit('itemProgress', fragment, true); 244 | return allChunks.buffer; 245 | } 246 | return pump(); 247 | }) 248 | .then(buffer => { 249 | this.emit('rawBuffer', buffer, fragment); 250 | // 存在解密函数 调用解密函数 否则直接返回buffer 251 | return this.decrypt ? this.decrypt(buffer, fragment) : buffer; 252 | }) 253 | .then(buffer => { 254 | this.emit('decryptedData', buffer, fragment); 255 | // 存在转码函数 调用转码函数 否则直接返回buffer 256 | return this.transcode ? this.transcode(buffer, fragment) : buffer; 257 | }) 258 | .then(buffer => { 259 | // 储存解密/转码后的buffer 260 | this.buffer[fragment.index] = buffer; 261 | 262 | // 成功数+1 累计buffer大小和视频时长 263 | this.success++; 264 | this.buffersize += buffer.byteLength; 265 | this.duration += fragment.duration ?? 0; 266 | 267 | // 下载对象来自错误列表 从错误列表内删除 268 | this.errorList.has(fragment) && this.errorList.delete(fragment); 269 | 270 | // 推送顺序下载 271 | this.sequentialPush(); 272 | 273 | this.emit('completed', buffer, fragment); 274 | 275 | // 下载完成 276 | if (this.success == this.fragments.length) { 277 | this.state = 'done'; 278 | this.emit('allCompleted', this.buffer, this.fragments); 279 | } 280 | }).catch((error) => { 281 | console.log(error); 282 | if (error.name == 'AbortError') { 283 | this.emit('stop', fragment, error); 284 | return; 285 | } 286 | this.emit('downloadError', fragment, error); 287 | 288 | // 储存下载错误切片 289 | !this.errorList.has(fragment) && this.errorList.add(fragment); 290 | }).finally(() => { 291 | this.running--; 292 | // 下载下一个切片 293 | if (!directDownload && this.index < this.fragments.length) { 294 | this.downloader(); 295 | } 296 | }); 297 | } 298 | /** 299 | * 开始下载 准备数据 调用下载器 300 | * @param {number} start 下载范围 开始索引 301 | * @param {number} end 下载范围 结束索引 302 | */ 303 | start(start = 0, end = this.fragments.length) { 304 | // 检查下载器状态 305 | if (this.state == 'running') { 306 | this.emit('error', 'state running'); 307 | return; 308 | } 309 | // 从下载范围内 切出需要下载的部分 310 | if (!this.range(start, end)) { 311 | return; 312 | } 313 | // 初始化变量 314 | this.init(); 315 | // 开始下载 多少线程开启多少个下载器 316 | for (let i = 0; i < this.thread && i < this.fragments.length; i++) { 317 | this.downloader(); 318 | } 319 | } 320 | /** 321 | * 销毁 初始化所有变量 322 | */ 323 | destroy() { 324 | this.stop(); 325 | this._fragments = []; 326 | this.allFragments = []; 327 | this.thread = 6; 328 | this.events = {}; 329 | this.decrypt = null; 330 | this.transcode = null; 331 | this.init(); 332 | } 333 | } -------------------------------------------------------------------------------- /js/media-control.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | let _tabId = -1; // 选择的页面ID 3 | let _index = -1; //选择的视频索引 4 | let VideoTagTimer; // 获取所有视频标签的定时器 5 | let VideoStateTimer; // 获取所有视频信息的定时器 6 | let compareTab = []; 7 | let compareVideo = []; 8 | 9 | function setVideoTagTimer() { 10 | clearInterval(VideoTagTimer); 11 | VideoTagTimer = setInterval(getVideoTag, 1000); 12 | } 13 | function getVideoTag() { 14 | chrome.tabs.query({ windowType: "normal" }, function (tabs) { 15 | let videoTabList = []; 16 | for (let tab of tabs) { 17 | videoTabList.push(tab.id); 18 | } 19 | if (compareTab.toString() == videoTabList.toString()) { 20 | return; 21 | } 22 | compareTab = videoTabList; 23 | // 列出所有标签 24 | for (let tab of tabs) { 25 | if ($("#option" + tab.id).length == 1) { continue; } 26 | $("#videoTabIndex").append(``); 27 | } 28 | // 删除没有媒体的标签. 异步的原因,使用一个for去处理无法保证标签顺序一致 29 | for (let tab of videoTabList) { 30 | chrome.tabs.sendMessage(tab, { Message: "getVideoState", index: 0 }, { frameId: 0 }, function (state) { 31 | if (chrome.runtime.lastError || state.count == 0) { 32 | $("#option" + tab).remove(); 33 | return; 34 | } 35 | $("#videoTabTips").remove(); 36 | if (tab == G.tabId && _tabId == -1) { 37 | _tabId = tab; 38 | $("#videoTabIndex").val(tab); 39 | } 40 | }); 41 | } 42 | }); 43 | } 44 | function setVideoStateTimer() { 45 | clearInterval(VideoStateTimer); 46 | VideoStateTimer = setInterval(getVideoState, 500); 47 | } 48 | function getVideoState(setSpeed = false) { 49 | if (_tabId == -1) { 50 | let currentTabId = $("#videoTabIndex").val(); 51 | if (currentTabId == -1) { return; } 52 | _tabId = parseInt(currentTabId); 53 | } 54 | chrome.tabs.sendMessage(_tabId, { Message: "getVideoState", index: _index }, { frameId: 0 }, function (state) { 55 | if (chrome.runtime.lastError || state.count == 0) { return; } 56 | if (state.type == "audio") { 57 | $("#pip").hide(); 58 | $("#screenshot").hide(); 59 | } 60 | $("#volume").val(state.volume); 61 | if (state.duration && state.duration != Infinity) { 62 | $("#timeShow").html(secToTime(state.currentTime) + " / " + secToTime(state.duration)); 63 | $("#time").val(state.time); 64 | } 65 | state.paused ? $("#control").html(i18n.play).data("switch", "play") : $("#control").html(i18n.pause).data("switch", "pause"); 66 | state.speed == 1 ? $("#speed").html(i18n.speedPlayback).data("switch", "speed") : $("#speed").html(i18n.normalPlay).data("switch", "normal"); 67 | $("#loop").prop("checked", state.loop); 68 | $("#muted").prop("checked", state.muted); 69 | if (setSpeed && state.speed != 1) { 70 | $("#playbackRate").val(state.speed); 71 | } 72 | if (compareVideo.toString() != state.src.toString()) { 73 | compareVideo = state.src; 74 | $("#videoIndex").empty(); 75 | for (let i = 0; i < state.count; i++) { 76 | let src = state.src[i].split("/").pop(); 77 | if (src.length >= 60) { 78 | src = src.substr(0, 35) + '...' + src.substr(-35); 79 | } 80 | $("#videoIndex").append(``); 81 | } 82 | } 83 | _index = _index == -1 ? 0 : _index; 84 | $("#videoIndex").val(_index); 85 | }); 86 | } 87 | // 点击其他设置标签页 开始读取tab信息以及视频信息 88 | getVideoTag(); 89 | $("#otherTab").click(function () { 90 | chrome.tabs.get(G.mediaControl.tabid, function (tab) { 91 | if (chrome.runtime.lastError) { 92 | _tabId = -1; 93 | _index = -1; 94 | setVideoTagTimer(); getVideoState(); setVideoStateTimer(); 95 | return; 96 | } 97 | chrome.tabs.sendMessage(G.mediaControl.tabid, { Message: "getVideoState", index: 0 }, function (state) { 98 | _tabId = G.mediaControl.tabid; 99 | if (state.count > G.mediaControl.index) { 100 | _index = G.mediaControl.index; 101 | } 102 | $("#videoTabIndex").val(_tabId); 103 | setVideoTagTimer(); getVideoState(true); setVideoStateTimer(); 104 | (chrome.storage.session ?? chrome.storage.local).set({ mediaControl: { tabid: _tabId, index: _index } }); 105 | }); 106 | }); 107 | // setVideoTagTimer(); getVideoState(); setVideoStateTimer(); 108 | }); 109 | // 切换标签选择 切换视频选择 110 | $("#videoIndex, #videoTabIndex").change(function () { 111 | if (!G.isFirefox) { $("#pip").show(); } 112 | $("#screenshot").show(); 113 | if (this.id == "videoTabIndex") { 114 | _tabId = parseInt($("#videoTabIndex").val()); 115 | } else { 116 | _index = parseInt($("#videoIndex").val()); 117 | } 118 | (chrome.storage.session ?? chrome.storage.local).set({ mediaControl: { tabid: _tabId, index: _index } }); 119 | getVideoState(true); 120 | }); 121 | let wheelPlaybackRateTimeout; 122 | $("#playbackRate").on("wheel", function (event) { 123 | $(this).blur(); 124 | let speed = parseFloat($(this).val()); 125 | speed = event.originalEvent.wheelDelta < 0 ? speed - 0.1 : speed + 0.1; 126 | speed = parseFloat(speed.toFixed(1)); 127 | if (speed < 0.1 || speed > 16) { return false; } 128 | $(this).val(speed); 129 | clearTimeout(wheelPlaybackRateTimeout); 130 | wheelPlaybackRateTimeout = setTimeout(() => { 131 | chrome.storage.sync.set({ playbackRate: speed }); 132 | chrome.tabs.sendMessage(_tabId, { Message: "speed", speed: speed, index: _index }); 133 | }, 200); 134 | return false; 135 | }); 136 | // 倍速播放 137 | $("#speed").click(function () { 138 | if (_index < 0 || _tabId < 0) { return; } 139 | if ($(this).data("switch") == "speed") { 140 | const speed = parseFloat($("#playbackRate").val()); 141 | chrome.tabs.sendMessage(_tabId, { Message: "speed", speed: speed, index: _index }); 142 | chrome.storage.sync.set({ playbackRate: speed }); 143 | return; 144 | } 145 | chrome.tabs.sendMessage(_tabId, { Message: "speed", speed: 1, index: _index }); 146 | }); 147 | // 画中画 148 | $("#pip").click(function () { 149 | if (_index < 0 || _tabId < 0) { return; } 150 | chrome.tabs.sendMessage(_tabId, { Message: "pip", index: _index }, function (state) { 151 | if (chrome.runtime.lastError) { return; } 152 | state.state ? $("#pip").html(i18n.exit) : $("#pip").html(i18n.pictureInPicture); 153 | }); 154 | }); 155 | // 全屏 156 | $("#fullScreen").click(function () { 157 | if (_index < 0 || _tabId < 0) { return; } 158 | chrome.tabs.get(_tabId, function (tab) { 159 | chrome.tabs.highlight({ 'tabs': tab.index }, function () { 160 | chrome.tabs.sendMessage(_tabId, { Message: "fullScreen", index: _index }, function (state) { 161 | close(); 162 | }); 163 | }); 164 | }); 165 | }); 166 | // 暂停 播放 167 | $("#control").click(function () { 168 | if (_index < 0 || _tabId < 0) { return; } 169 | const action = $(this).data("switch"); 170 | chrome.tabs.sendMessage(_tabId, { Message: action, index: _index }); 171 | }); 172 | // 循环 静音 173 | $("#loop, #muted").click(function () { 174 | if (_index < 0 || _tabId < 0) { return; } 175 | const action = $(this).prop("checked"); 176 | chrome.tabs.sendMessage(_tabId, { Message: this.id, action: action, index: _index }); 177 | }); 178 | // 调节音量和视频进度时 停止循环任务 179 | $("#volume, #time").mousedown(function () { 180 | if (_index < 0 || _tabId < 0) { return; } 181 | clearInterval(VideoStateTimer); 182 | }); 183 | // 调节音量 184 | $("#volume").mouseup(function () { 185 | if (_index < 0 || _tabId < 0) { return; } 186 | chrome.tabs.sendMessage(_tabId, { Message: "setVolume", volume: $(this).val(), index: _index }, function () { 187 | if (chrome.runtime.lastError) { return; } 188 | setVideoStateTimer(); 189 | }); 190 | }); 191 | // 调节视频进度 192 | $("#time").mouseup(function () { 193 | if (_index < 0 || _tabId < 0) { return; } 194 | chrome.tabs.sendMessage(_tabId, { Message: "setTime", time: $(this).val(), index: _index }, function () { 195 | if (chrome.runtime.lastError) { return; } 196 | setVideoStateTimer(); 197 | }); 198 | }); 199 | // 视频截图 200 | $("#screenshot").click(function () { 201 | if (_index < 0 || _tabId < 0) { return; } 202 | chrome.tabs.sendMessage(_tabId, { Message: "screenshot", index: _index }); 203 | }); 204 | })(); -------------------------------------------------------------------------------- /js/mpd.js: -------------------------------------------------------------------------------- 1 | // url 参数解析 2 | const params = new URL(location.href).searchParams; 3 | const _url = params.get("url"); 4 | // const _referer = params.get("referer"); 5 | const _requestHeaders = params.get("requestHeaders"); 6 | const _title = params.get("title"); 7 | 8 | // 修改当前标签下的所有xhr的requestHeaders 9 | let requestHeaders = JSONparse(_requestHeaders); 10 | setRequestHeaders(requestHeaders, () => { awaitG(init); }); 11 | 12 | var mpdJson = {}; // 解析器json结果 13 | var mpdXml = {}; // 解析器xml结果 14 | // var mpdContent; // mpd文件内容 15 | var m3u8Content = ""; //m3u8内容 16 | var mediaInfo = "" // 媒体文件信息 17 | 18 | chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) { 19 | if (message == "getM3u8") { 20 | sendResponse({ m3u8Content, mediaInfo }); 21 | } 22 | }); 23 | 24 | function init() { 25 | $(``).appendTo("head"); 26 | if (_url) { 27 | fetch(_url) 28 | .then(response => response.text()) 29 | .then(function (text) { 30 | // mpdContent = text; 31 | // parseMPD(mpdContent); 32 | parseMPD(text); 33 | $("#mpd_url").html(_url).attr("href", _url); 34 | }); 35 | } else { 36 | $("#loading").hide(); 37 | $("#mpdCustom").show(); 38 | $("#parse").click(function () { 39 | let url = $("#mpdUrl").val().trim();; 40 | url = "mpd.html?url=" + encodeURIComponent(url); 41 | let referer = $("#referer").val().trim();; 42 | if (referer) { url += "&requestHeaders=" + JSON.stringify({ referer: referer }); } 43 | chrome.tabs.update({ url: url }); 44 | }); 45 | } 46 | 47 | $("#mpdVideoLists, #mpdAudioLists").change(function () { 48 | let type = this.id == "mpdVideoLists" ? "video" : "audio"; 49 | showSegment(type, $(this).val()); 50 | }); 51 | $("#getVideo, #getAudio").click(function () { 52 | let type = "video"; 53 | let index = $("#mpdVideoLists").val(); 54 | if (this.id == "getAudio") { 55 | type = "audio"; 56 | index = $("#mpdAudioLists").val(); 57 | } 58 | showSegment(type, index); 59 | }); 60 | $("#videoToM3u8, #audioToM3u8").click(function () { 61 | let index = $("#mpdVideoLists").val(); 62 | let items = mpdJson.playlists[index]; 63 | let type = "video"; 64 | if (this.id == "audioToM3u8") { 65 | index = $("#mpdAudioLists").val(); 66 | let temp = index.split("$-bmmmd-$"); 67 | index = temp[0]; 68 | let index2 = temp[1]; 69 | items = mpdJson.mediaGroups.AUDIO.audio[index].playlists[index2]; 70 | type = "audio"; 71 | } 72 | mediaInfo = getInfo(type); 73 | m3u8Content = "#EXTM3U\n"; 74 | m3u8Content += "#EXT-X-VERSION:3\n"; 75 | m3u8Content += "#EXT-X-TARGETDURATION:" + items.targetDuration + "\n"; 76 | m3u8Content += "#EXT-X-MEDIA-SEQUENCE:0\n"; 77 | m3u8Content += "#EXT-X-PLAYLIST-TYPE:VOD\n"; 78 | m3u8Content += '#EXT-X-MAP:URI="' + items.segments[0].map.resolvedUri + '"\n'; 79 | for (let key in items.segments) { 80 | m3u8Content += "#EXTINF:" + items.segments[key].duration + ",\n" 81 | m3u8Content += items.segments[key].resolvedUri + "\n"; 82 | } 83 | m3u8Content += "#EXT-X-ENDLIST"; 84 | // $("#media_file").html(m3u8Content); return; 85 | chrome.tabs.getCurrent(function (tabs) { 86 | chrome.tabs.create({ url: "m3u8.html?getId=" + tabs.id }); 87 | }); 88 | }); 89 | } 90 | 91 | // 加密类型 92 | function getEncryptionType(schemeIdUri) { 93 | if (schemeIdUri.includes("edef8ba9-79d6-4ace-a3c8-27dcd51d21ed")) { 94 | return "Widevine"; 95 | } else if (schemeIdUri.includes("9a04f079-9840-4286-ab92-e65be0885f95")) { 96 | return "Microsoft PlayReady"; 97 | } else if (schemeIdUri.includes("94ce86fb-07ff-4f43-adb8-93d2fa968ca2")) { 98 | return "Apple FairPlay"; 99 | } else { 100 | return "Unknown"; 101 | } 102 | } 103 | // 判断DRM 104 | function isDRM(mpdContent) { 105 | const parser = new DOMParser(); 106 | const xmlDoc = parser.parseFromString(mpdContent, "application/xml"); 107 | let drmInfo = new Map(); 108 | const contentProtections = xmlDoc.getElementsByTagName("ContentProtection"); 109 | for (let i = 0; i < contentProtections.length; i++) { 110 | const protection = contentProtections[i]; 111 | const schemeIdUri = protection.getAttribute("schemeIdUri"); 112 | let pssh = protection.getElementsByTagName("cenc:pssh")[0]; 113 | if (!pssh) { 114 | pssh = protection.getElementsByTagName("mspr:pro")[0]; 115 | } 116 | 117 | if (schemeIdUri && pssh) { 118 | if (!drmInfo.has(schemeIdUri)) { 119 | drmInfo.set(schemeIdUri, pssh.textContent); 120 | } 121 | } 122 | } 123 | return Array.from(drmInfo.entries()).map(([schemeIdUri, pssh]) => ({ 124 | schemeIdUri, 125 | pssh, 126 | encryptionType: getEncryptionType(schemeIdUri) 127 | })); 128 | } 129 | function parseMPD(mpdContent) { 130 | $("#loading").hide(); $("#main").show(); 131 | mpdJson = mpdParser.parse(mpdContent, { manifestUri: _url }); 132 | 133 | const drmInfo = isDRM(mpdContent); 134 | if (drmInfo.length > 0) { 135 | $("#loading").show(); 136 | $("#loading .optionBox").html(`${i18n.DRMerror}

`); 137 | drmInfo.map(item => { 138 | $("#loading .optionBox").append(`${item.encryptionType}`); 139 | }); 140 | } 141 | 142 | for (let key in mpdJson.playlists) { 143 | $("#mpdVideoLists").append(``); 149 | } 150 | for (let key in mpdJson.mediaGroups.AUDIO.audio) { 151 | for (let index in mpdJson.mediaGroups.AUDIO.audio[key].playlists) { 152 | let item = mpdJson.mediaGroups.AUDIO.audio[key].playlists[index]; 153 | // console.log(item); 154 | $("#mpdAudioLists").append(``); 155 | } 156 | } 157 | $("#info").html(getInfo("video")); 158 | showSegment("video", 0); 159 | } 160 | 161 | function showSegment(type, index) { 162 | let textarea = ""; 163 | let items; 164 | if (type == "video") { 165 | items = mpdJson.playlists[index]; 166 | } else { 167 | let temp = index.split("$-bmmmd-$"); 168 | index = temp[0]; 169 | let index2 = temp[1]; 170 | items = mpdJson.mediaGroups.AUDIO.audio[index].playlists[index2]; 171 | } 172 | for (let key in items.segments) { 173 | textarea += items.segments[key].resolvedUri + "\n\n"; 174 | } 175 | $("#media_file").html(textarea); 176 | $("#count").html(i18n("m3u8Info", [items.segments.length, secToTime(mpdJson.duration)])); 177 | items.segments.length > 0 && $("#tips").html('initialization: '); 178 | $("#info").html(getInfo(type)); 179 | } 180 | 181 | function getInfo(type = "audio") { 182 | if (type == "audio") { 183 | return i18n.audio + ": " + $("#mpdAudioLists").find("option:selected").text(); 184 | } else { 185 | return i18n.video + ": " + $("#mpdVideoLists").find("option:selected").text(); 186 | } 187 | } -------------------------------------------------------------------------------- /js/pupup-utils.js: -------------------------------------------------------------------------------- 1 | // 复制选项 2 | function copyLink(data) { 3 | let text = data.url; 4 | if (data.parsing == "m3u8") { 5 | text = G.copyM3U8; 6 | } else if (data.parsing == "mpd") { 7 | text = G.copyMPD; 8 | } else { 9 | text = G.copyOther; 10 | } 11 | return templates(text, data); 12 | } 13 | function isM3U8(data) { 14 | return ( 15 | data.ext == "m3u8" || 16 | data.ext == "m3u" || 17 | data.type?.endsWith("/vnd.apple.mpegurl") || 18 | data.type?.endsWith("/x-mpegurl") || 19 | data.type?.endsWith("/mpegurl") || 20 | data.type?.endsWith("/octet-stream-m3u8") 21 | ) 22 | } 23 | function isMPD(data) { 24 | return (data.ext == "mpd" || 25 | data.type == "application/dash+xml" 26 | ) 27 | } 28 | function isJSON(data) { 29 | return (data.ext == "json" || 30 | data.type == "application/json" || 31 | data.type == "text/json" 32 | ) 33 | } 34 | function isPicture(data) { 35 | return (data.type?.startsWith("image/") || 36 | data.ext == "jpg" || 37 | data.ext == "png" || 38 | data.ext == "jpeg" || 39 | data.ext == "bmp" || 40 | data.ext == "gif" || 41 | data.ext == "webp" || 42 | data.ext == "svg" 43 | ) 44 | } 45 | function isMediaExt(ext) { 46 | return ['ogg', 'ogv', 'mp4', 'webm', 'mp3', 'wav', 'm4a', '3gp', 'mpeg', 'mov', 'm4s', 'aac'].includes(ext); 47 | } 48 | function isMedia(data) { 49 | return isMediaExt(data.ext) || data.type?.startsWith("video/") || data.type?.startsWith("audio/"); 50 | } 51 | /** 52 | * ari2a RPC发送一套资源 53 | * @param {object} data 资源对象 54 | * @param {Function} success 成功运行函数 55 | * @param {Function} error 失败运行函数 56 | */ 57 | function aria2AddUri(data, success, error) { 58 | const json = { 59 | "jsonrpc": "2.0", 60 | "id": "cat-catch-" + data.requestId, 61 | "method": "aria2.addUri", 62 | "params": [] 63 | }; 64 | if (G.aria2RpcToken) { 65 | json.params.push(`token:${G.aria2RpcToken}`); 66 | } 67 | const params = { out: data.downFileName }; 68 | if (G.enableAria2RpcReferer) { 69 | params.header = []; 70 | params.header.push(G.userAgent ? G.userAgent : navigator.userAgent); 71 | if (data.requestHeaders?.referer) { 72 | params.header.push("Referer: " + data.requestHeaders.referer); 73 | } 74 | if (data.cookie) { 75 | params.header.push("Cookie: " + data.cookie); 76 | } 77 | if (data.requestHeaders?.authorization) { 78 | params.header.push("Authorization: " + data.requestHeaders.authorization); 79 | } 80 | } 81 | json.params.push([data.url], params); 82 | fetch(G.aria2Rpc, { 83 | method: "POST", 84 | headers: { 85 | "Content-Type": "application/json; charset=utf-8" 86 | }, 87 | body: JSON.stringify(json) 88 | }).then(response => { 89 | return response.json(); 90 | }).then(data => { 91 | success && success(data); 92 | }).catch(errMsg => { 93 | error && error(errMsg); 94 | }); 95 | } -------------------------------------------------------------------------------- /json.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | titleJson 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |
21 |

22 |
23 | json 24 | 25 |
26 | json url 27 | 28 | 29 |
30 |
31 | 32 |
33 |

34 |
35 |
36 |

37 |         
38 | 39 |
40 |
41 |
42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /lib/StreamSaver.js: -------------------------------------------------------------------------------- 1 | /*! streamsaver. MIT License. Jimmy Wärting */ 2 | 3 | /* global chrome location ReadableStream define MessageChannel TransformStream */ 4 | 5 | ;((name, definition) => { 6 | typeof module !== 'undefined' 7 | ? module.exports = definition() 8 | : typeof define === 'function' && typeof define.amd === 'object' 9 | ? define(definition) 10 | : this[name] = definition() 11 | })('streamSaver', () => { 12 | 'use strict' 13 | 14 | const global = typeof window === 'object' ? window : this 15 | if (!global.HTMLElement) console.warn('streamsaver is meant to run on browsers main thread') 16 | 17 | let mitmTransporter = null 18 | let supportsTransferable = false 19 | const test = fn => { try { fn() } catch (e) {} } 20 | const ponyfill = global.WebStreamsPolyfill || {} 21 | const isSecureContext = global.isSecureContext 22 | // TODO: Must come up with a real detection test (#69) 23 | let useBlobFallback = /constructor/i.test(global.HTMLElement) || !!global.safari || !!global.WebKitPoint 24 | const downloadStrategy = isSecureContext || 'MozAppearance' in document.documentElement.style 25 | ? 'iframe' 26 | : 'navigate' 27 | 28 | const streamSaver = { 29 | createWriteStream, 30 | WritableStream: global.WritableStream || ponyfill.WritableStream, 31 | supported: true, 32 | version: { full: '2.0.5', major: 2, minor: 0, dot: 5 }, 33 | mitm: 'https://jimmywarting.github.io/StreamSaver.js/mitm.html?version=2.0.0' 34 | } 35 | 36 | /** 37 | * create a hidden iframe and append it to the DOM (body) 38 | * 39 | * @param {string} src page to load 40 | * @return {HTMLIFrameElement} page to load 41 | */ 42 | function makeIframe (src) { 43 | if (!src) throw new Error('meh') 44 | const iframe = document.createElement('iframe') 45 | iframe.hidden = true 46 | iframe.src = src 47 | iframe.loaded = false 48 | iframe.name = 'iframe' 49 | iframe.isIframe = true 50 | iframe.postMessage = (...args) => iframe.contentWindow.postMessage(...args) 51 | iframe.addEventListener('load', () => { 52 | iframe.loaded = true 53 | }, { once: true }) 54 | document.body.appendChild(iframe) 55 | return iframe 56 | } 57 | 58 | /** 59 | * create a popup that simulates the basic things 60 | * of what a iframe can do 61 | * 62 | * @param {string} src page to load 63 | * @return {object} iframe like object 64 | */ 65 | function makePopup (src) { 66 | const options = 'width=200,height=100' 67 | const delegate = document.createDocumentFragment() 68 | const popup = { 69 | frame: global.open(src, 'popup', options), 70 | loaded: false, 71 | isIframe: false, 72 | isPopup: true, 73 | remove () { popup.frame.close() }, 74 | addEventListener (...args) { delegate.addEventListener(...args) }, 75 | dispatchEvent (...args) { delegate.dispatchEvent(...args) }, 76 | removeEventListener (...args) { delegate.removeEventListener(...args) }, 77 | postMessage (...args) { popup.frame.postMessage(...args) } 78 | } 79 | 80 | const onReady = evt => { 81 | if (evt.source === popup.frame) { 82 | popup.loaded = true 83 | global.removeEventListener('message', onReady) 84 | popup.dispatchEvent(new Event('load')) 85 | } 86 | } 87 | 88 | global.addEventListener('message', onReady) 89 | 90 | return popup 91 | } 92 | 93 | try { 94 | // We can't look for service worker since it may still work on http 95 | new Response(new ReadableStream()) 96 | if (isSecureContext && !('serviceWorker' in navigator)) { 97 | useBlobFallback = true 98 | } 99 | } catch (err) { 100 | useBlobFallback = true 101 | } 102 | 103 | test(() => { 104 | // Transferable stream was first enabled in chrome v73 behind a flag 105 | const { readable } = new TransformStream() 106 | const mc = new MessageChannel() 107 | mc.port1.postMessage(readable, [readable]) 108 | mc.port1.close() 109 | mc.port2.close() 110 | supportsTransferable = true 111 | // Freeze TransformStream object (can only work with native) 112 | Object.defineProperty(streamSaver, 'TransformStream', { 113 | configurable: false, 114 | writable: false, 115 | value: TransformStream 116 | }) 117 | }) 118 | 119 | function loadTransporter () { 120 | if (!mitmTransporter) { 121 | mitmTransporter = isSecureContext 122 | ? makeIframe(streamSaver.mitm) 123 | : makePopup(streamSaver.mitm) 124 | } 125 | } 126 | 127 | /** 128 | * @param {string} filename filename that should be used 129 | * @param {object} options [description] 130 | * @param {number} size deprecated 131 | * @return {WritableStream} 132 | */ 133 | function createWriteStream (filename, options, size) { 134 | let opts = { 135 | size: null, 136 | pathname: null, 137 | writableStrategy: undefined, 138 | readableStrategy: undefined 139 | } 140 | 141 | let bytesWritten = 0 // by StreamSaver.js (not the service worker) 142 | let downloadUrl = null 143 | let channel = null 144 | let ts = null 145 | 146 | // normalize arguments 147 | if (Number.isFinite(options)) { 148 | [ size, options ] = [ options, size ] 149 | console.warn('[StreamSaver] Deprecated pass an object as 2nd argument when creating a write stream') 150 | opts.size = size 151 | opts.writableStrategy = options 152 | } else if (options && options.highWaterMark) { 153 | console.warn('[StreamSaver] Deprecated pass an object as 2nd argument when creating a write stream') 154 | opts.size = size 155 | opts.writableStrategy = options 156 | } else { 157 | opts = options || {} 158 | } 159 | if (!useBlobFallback) { 160 | loadTransporter() 161 | 162 | channel = new MessageChannel() 163 | 164 | // Make filename RFC5987 compatible 165 | filename = encodeURIComponent(filename.replace(/\//g, ':')) 166 | .replace(/['()]/g, escape) 167 | .replace(/\*/g, '%2A') 168 | 169 | const response = { 170 | transferringReadable: supportsTransferable, 171 | pathname: opts.pathname || Math.random().toString().slice(-6) + '/' + filename, 172 | headers: { 173 | 'Content-Type': 'application/octet-stream; charset=utf-8', 174 | 'Content-Disposition': "attachment; filename*=UTF-8''" + filename 175 | } 176 | } 177 | 178 | if (opts.size) { 179 | response.headers['Content-Length'] = opts.size 180 | } 181 | 182 | const args = [ response, '*', [ channel.port2 ] ] 183 | 184 | if (supportsTransferable) { 185 | const transformer = downloadStrategy === 'iframe' ? undefined : { 186 | // This transformer & flush method is only used by insecure context. 187 | transform (chunk, controller) { 188 | if (!(chunk instanceof Uint8Array)) { 189 | throw new TypeError('Can only write Uint8Arrays') 190 | } 191 | bytesWritten += chunk.length 192 | controller.enqueue(chunk) 193 | 194 | if (downloadUrl) { 195 | location.href = downloadUrl 196 | downloadUrl = null 197 | } 198 | }, 199 | flush () { 200 | if (downloadUrl) { 201 | location.href = downloadUrl 202 | } 203 | } 204 | } 205 | ts = new streamSaver.TransformStream( 206 | transformer, 207 | opts.writableStrategy, 208 | opts.readableStrategy 209 | ) 210 | const readableStream = ts.readable 211 | 212 | channel.port1.postMessage({ readableStream }, [ readableStream ]) 213 | } 214 | 215 | channel.port1.onmessage = evt => { 216 | // Service worker sent us a link that we should open. 217 | if (evt.data.download) { 218 | // Special treatment for popup... 219 | if (downloadStrategy === 'navigate') { 220 | mitmTransporter.remove() 221 | mitmTransporter = null 222 | if (bytesWritten) { 223 | location.href = evt.data.download 224 | } else { 225 | downloadUrl = evt.data.download 226 | } 227 | } else { 228 | if (mitmTransporter.isPopup) { 229 | mitmTransporter.remove() 230 | mitmTransporter = null 231 | // Special case for firefox, they can keep sw alive with fetch 232 | if (downloadStrategy === 'iframe') { 233 | makeIframe(streamSaver.mitm) 234 | } 235 | } 236 | 237 | // We never remove this iframes b/c it can interrupt saving 238 | makeIframe(evt.data.download) 239 | } 240 | } else if (evt.data.abort) { 241 | chunks = [] 242 | channel.port1.postMessage('abort') //send back so controller is aborted 243 | channel.port1.onmessage = null 244 | channel.port1.close() 245 | channel.port2.close() 246 | channel = null 247 | } 248 | } 249 | 250 | if (mitmTransporter.loaded) { 251 | mitmTransporter.postMessage(...args) 252 | } else { 253 | mitmTransporter.addEventListener('load', () => { 254 | mitmTransporter.postMessage(...args) 255 | }, { once: true }) 256 | } 257 | } 258 | 259 | let chunks = [] 260 | 261 | return (!useBlobFallback && ts && ts.writable) || new streamSaver.WritableStream({ 262 | write (chunk) { 263 | if (!(chunk instanceof Uint8Array)) { 264 | throw new TypeError('Can only write Uint8Arrays') 265 | } 266 | if (useBlobFallback) { 267 | // Safari... The new IE6 268 | // https://github.com/jimmywarting/StreamSaver.js/issues/69 269 | // 270 | // even though it has everything it fails to download anything 271 | // that comes from the service worker..! 272 | chunks.push(chunk) 273 | return 274 | } 275 | 276 | // is called when a new chunk of data is ready to be written 277 | // to the underlying sink. It can return a promise to signal 278 | // success or failure of the write operation. The stream 279 | // implementation guarantees that this method will be called 280 | // only after previous writes have succeeded, and never after 281 | // close or abort is called. 282 | 283 | // TODO: Kind of important that service worker respond back when 284 | // it has been written. Otherwise we can't handle backpressure 285 | // EDIT: Transferable streams solves this... 286 | channel.port1.postMessage(chunk) 287 | bytesWritten += chunk.length 288 | 289 | if (downloadUrl) { 290 | location.href = downloadUrl 291 | downloadUrl = null 292 | } 293 | }, 294 | close () { 295 | if (useBlobFallback) { 296 | const blob = new Blob(chunks, { type: 'application/octet-stream; charset=utf-8' }) 297 | const link = document.createElement('a') 298 | link.href = URL.createObjectURL(blob) 299 | link.download = filename 300 | link.click() 301 | } else { 302 | channel.port1.postMessage('end') 303 | } 304 | }, 305 | abort () { 306 | chunks = [] 307 | channel.port1.postMessage('abort') 308 | channel.port1.onmessage = null 309 | channel.port1.close() 310 | channel.port2.close() 311 | channel = null 312 | } 313 | }, opts.writableStrategy) 314 | } 315 | 316 | return streamSaver 317 | }) 318 | -------------------------------------------------------------------------------- /lib/base64.js: -------------------------------------------------------------------------------- 1 | class Base64 { 2 | /** 3 | * 将字符串编码为Base64(支持UTF-8) 4 | * @param {string} str - 需要编码的原始字符串 5 | * @returns {string} Base64编码结果 6 | */ 7 | static encode(str) { 8 | // 使用TextEncoder将字符串转换为UTF-8字节数组 9 | const encoder = new TextEncoder(); 10 | const data = encoder.encode(str); 11 | 12 | // 将字节数组转换为二进制字符串 13 | let binary = ''; 14 | data.forEach(byte => binary += String.fromCharCode(byte)); 15 | 16 | // 使用浏览器内置方法进行Base64编码 17 | return btoa(binary); 18 | } 19 | 20 | /** 21 | * 解码Base64字符串为原始字符串(支持UTF-8) 22 | * @param {string} base64Str - Base64编码字符串 23 | * @returns {string} 解码后的原始字符串 24 | */ 25 | static decode(base64Str) { 26 | // 解码Base64得到二进制字符串 27 | const binaryStr = atob(base64Str); 28 | 29 | // 将二进制字符串转换为字节数组 30 | const bytes = new Uint8Array(binaryStr.length); 31 | for (let i = 0; i < binaryStr.length; i++) { 32 | bytes[i] = binaryStr.charCodeAt(i); 33 | } 34 | 35 | // 使用TextDecoder将字节数组转换为UTF-8字符串 36 | const decoder = new TextDecoder(); 37 | return decoder.decode(bytes); 38 | } 39 | } 40 | window.Base64 = Base64; -------------------------------------------------------------------------------- /lib/jquery.json-viewer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery json-viewer 3 | * @author: Alexandre Bodelot 4 | * @link: https://github.com/abodelot/jquery.json-viewer 5 | */ 6 | (function($) { 7 | 8 | /** 9 | * Check if arg is either an array with at least 1 element, or a dict with at least 1 key 10 | * @return boolean 11 | */ 12 | function isCollapsable(arg) { 13 | return arg instanceof Object && Object.keys(arg).length > 0; 14 | } 15 | 16 | /** 17 | * Check if a string looks like a URL, based on protocol 18 | * This doesn't attempt to validate URLs, there's no use and syntax can be too complex 19 | * @return boolean 20 | */ 21 | function isUrl(string) { 22 | var protocols = ['http', 'https', 'ftp', 'ftps']; 23 | for (var i = 0; i < protocols.length; ++i) { 24 | if (string.startsWith(protocols[i] + '://')) { 25 | return true; 26 | } 27 | } 28 | return false; 29 | } 30 | 31 | /** 32 | * Return the input string html escaped 33 | * @return string 34 | */ 35 | function htmlEscape(s) { 36 | return s.replace(/&/g, '&') 37 | .replace(//g, '>') 39 | .replace(/'/g, ''') 40 | .replace(/"/g, '"'); 41 | } 42 | 43 | /** 44 | * Transform a json object into html representation 45 | * @return string 46 | */ 47 | function json2html(json, options) { 48 | var html = ''; 49 | if (typeof json === 'string') { 50 | // Escape tags and quotes 51 | json = htmlEscape(json); 52 | 53 | if (options.withLinks && isUrl(json)) { 54 | html += '' + json + ''; 55 | } else { 56 | // Escape double quotes in the rendered non-URL string. 57 | json = json.replace(/"/g, '\\"'); 58 | html += '"' + json + '"'; 59 | } 60 | } else if (typeof json === 'number' || typeof json === 'bigint') { 61 | html += '' + json + ''; 62 | } else if (typeof json === 'boolean') { 63 | html += '' + json + ''; 64 | } else if (json === null) { 65 | html += 'null'; 66 | } else if (json instanceof Array) { 67 | if (json.length > 0) { 68 | html += '[
    '; 69 | for (var i = 0; i < json.length; ++i) { 70 | html += '
  1. '; 71 | // Add toggle button if item is collapsable 72 | if (isCollapsable(json[i])) { 73 | html += ''; 74 | } 75 | html += json2html(json[i], options); 76 | // Add comma if item is not last 77 | if (i < json.length - 1) { 78 | html += ','; 79 | } 80 | html += '
  2. '; 81 | } 82 | html += '
]'; 83 | } else { 84 | html += '[]'; 85 | } 86 | } else if (typeof json === 'object') { 87 | // Optional support different libraries for big numbers 88 | // json.isLosslessNumber: package lossless-json 89 | // json.toExponential(): packages bignumber.js, big.js, decimal.js, decimal.js-light, others? 90 | if (options.bigNumbers && (typeof json.toExponential === 'function' || json.isLosslessNumber)) { 91 | html += '' + json.toString() + ''; 92 | } else { 93 | var keyCount = Object.keys(json).length; 94 | if (keyCount > 0) { 95 | html += '{
    '; 96 | for (var key in json) { 97 | if (Object.prototype.hasOwnProperty.call(json, key)) { 98 | key = htmlEscape(key); 99 | var keyRepr = options.withQuotes ? 100 | '"' + key + '"' : key; 101 | 102 | html += '
  • '; 103 | // Add toggle button if item is collapsable 104 | if (isCollapsable(json[key])) { 105 | html += '' + keyRepr + ''; 106 | } else { 107 | html += keyRepr; 108 | } 109 | html += ': ' + json2html(json[key], options); 110 | // Add comma if item is not last 111 | if (--keyCount > 0) { 112 | html += ','; 113 | } 114 | html += '
  • '; 115 | } 116 | } 117 | html += '
}'; 118 | } else { 119 | html += '{}'; 120 | } 121 | } 122 | } 123 | return html; 124 | } 125 | 126 | /** 127 | * jQuery plugin method 128 | * @param json: a javascript object 129 | * @param options: an optional options hash 130 | */ 131 | $.fn.jsonViewer = function(json, options) { 132 | // Merge user options with default options 133 | options = Object.assign({}, { 134 | collapsed: false, 135 | rootCollapsable: true, 136 | withQuotes: false, 137 | withLinks: true, 138 | bigNumbers: false 139 | }, options); 140 | 141 | // jQuery chaining 142 | return this.each(function() { 143 | 144 | // Transform to HTML 145 | var html = json2html(json, options); 146 | if (options.rootCollapsable && isCollapsable(json)) { 147 | html = '' + html; 148 | } 149 | 150 | // Insert HTML in target DOM element 151 | $(this).html(html); 152 | $(this).addClass('json-document'); 153 | 154 | // Bind click on toggle buttons 155 | $(this).off('click'); 156 | $(this).on('click', 'a.json-toggle', function() { 157 | var target = $(this).toggleClass('collapsed').siblings('ul.json-dict, ol.json-array'); 158 | target.toggle(); 159 | if (target.is(':visible')) { 160 | target.siblings('.json-placeholder').remove(); 161 | } else { 162 | var count = target.children('li').length; 163 | var placeholder = count + (count > 1 ? ' items' : ' item'); 164 | target.after('' + placeholder + ''); 165 | } 166 | return false; 167 | }); 168 | 169 | // Simulate click on toggle button when placeholder is clicked 170 | $(this).on('click', 'a.json-placeholder', function() { 171 | $(this).siblings('a.json-toggle').click(); 172 | return false; 173 | }); 174 | 175 | if (options.collapsed == true) { 176 | // Trigger click to collapse all nodes 177 | $(this).find('a.json-toggle').click(); 178 | } 179 | }); 180 | }; 181 | })(jQuery); 182 | -------------------------------------------------------------------------------- /lib/jquery.qrcode.min.js: -------------------------------------------------------------------------------- 1 | (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;da||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=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]= 5 | 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=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= 7 | 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- 8 | 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;0g;g++)if(null==this.modules[b][i-g]){var n=!1;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, 9 | c),b=new t,e=0;e8*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= 10 | 0,b=0,e=0,f=Array(c.length),i=Array(c.length),g=0;g>>=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:"+ 14 | a);}},getErrorCorrectPolynomial:function(a){for(var c=new q([1],0),d=0;dc)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:"+ 15 | a);}else throw Error("type:"+c);},getLostPoint:function(a){for(var c=a.getModuleCount(),d=0,b=0;b=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++;5a)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), 17 | LOG_TABLE:Array(256)},m=0;8>m;m++)l.EXP_TABLE[m]=1<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 18 | 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>>7-a%8&1)},put:function(a,c){for(var d=0;d>>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, 26 | 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").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").css("height",b+"px").appendTo(c);for(i=0;i").css("width", 28 | d+"px").css("background-color",a.isDark(e,i)?h.foreground:h.background).appendTo(f)}}a=c;jQuery(a).appendTo(this)})}})(jQuery); 29 | -------------------------------------------------------------------------------- /lib/m3u8-decrypt.js: -------------------------------------------------------------------------------- 1 | class AESDecryptor { 2 | constructor() { 3 | this.rcon = [0x0, 0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36]; 4 | this.subMix = [ 5 | new Uint32Array(256), 6 | new Uint32Array(256), 7 | new Uint32Array(256), 8 | new Uint32Array(256), 9 | ]; 10 | this.invSubMix = [ 11 | new Uint32Array(256), 12 | new Uint32Array(256), 13 | new Uint32Array(256), 14 | new Uint32Array(256), 15 | ]; 16 | this.sBox = new Uint32Array(256); 17 | this.invSBox = new Uint32Array(256); 18 | this.key = new Uint32Array(0); 19 | this.ksRows = 0; 20 | this.keySize = 0; 21 | this.initTable(); 22 | } 23 | removePadding(array) { 24 | const outputBytes = array.byteLength; 25 | const paddingBytes = 26 | outputBytes && new DataView(array).getUint8(outputBytes - 1); 27 | if (paddingBytes) { 28 | return array.slice(0, outputBytes - paddingBytes); 29 | } 30 | return array; 31 | } 32 | // Using view.getUint32() also swaps the byte order. 33 | uint8ArrayToUint32Array_(arrayBuffer) { 34 | const view = new DataView(arrayBuffer); 35 | const newArray = new Uint32Array(4); 36 | for (let i = 0; i < 4; i++) { 37 | newArray[i] = view.getUint32(i * 4); 38 | } 39 | return newArray; 40 | } 41 | initTable() { 42 | const sBox = this.sBox; 43 | const invSBox = this.invSBox; 44 | const subMix = this.subMix; 45 | const subMix0 = subMix[0]; 46 | const subMix1 = subMix[1]; 47 | const subMix2 = subMix[2]; 48 | const subMix3 = subMix[3]; 49 | const invSubMix = this.invSubMix; 50 | const invSubMix0 = invSubMix[0]; 51 | const invSubMix1 = invSubMix[1]; 52 | const invSubMix2 = invSubMix[2]; 53 | const invSubMix3 = invSubMix[3]; 54 | const d = new Uint32Array(256); 55 | let x = 0; 56 | let xi = 0; 57 | let i = 0; 58 | for (i = 0; i < 256; i++) { 59 | if (i < 128) { 60 | d[i] = i << 1; 61 | } else { 62 | d[i] = (i << 1) ^ 0x11b; 63 | } 64 | } 65 | for (i = 0; i < 256; i++) { 66 | let sx = xi ^ (xi << 1) ^ (xi << 2) ^ (xi << 3) ^ (xi << 4); 67 | sx = (sx >>> 8) ^ (sx & 0xff) ^ 0x63; 68 | sBox[x] = sx; 69 | invSBox[sx] = x; 70 | // Compute multiplication 71 | const x2 = d[x]; 72 | const x4 = d[x2]; 73 | const x8 = d[x4]; 74 | // Compute sub/invSub bytes, mix columns tables 75 | let t = (d[sx] * 0x101) ^ (sx * 0x1010100); 76 | subMix0[x] = (t << 24) | (t >>> 8); 77 | subMix1[x] = (t << 16) | (t >>> 16); 78 | subMix2[x] = (t << 8) | (t >>> 24); 79 | subMix3[x] = t; 80 | // Compute inv sub bytes, inv mix columns tables 81 | t = (x8 * 0x1010101) ^ (x4 * 0x10001) ^ (x2 * 0x101) ^ (x * 0x1010100); 82 | invSubMix0[sx] = (t << 24) | (t >>> 8); 83 | invSubMix1[sx] = (t << 16) | (t >>> 16); 84 | invSubMix2[sx] = (t << 8) | (t >>> 24); 85 | invSubMix3[sx] = t; 86 | // Compute next counter 87 | if (!x) { 88 | x = xi = 1; 89 | } else { 90 | x = x2 ^ d[d[d[x8 ^ x2]]]; 91 | xi ^= d[d[xi]]; 92 | } 93 | } 94 | } 95 | expandKey(keyBuffer) { 96 | // convert keyBuffer to Uint32Array 97 | const key = this.uint8ArrayToUint32Array_(keyBuffer); 98 | let sameKey = true; 99 | let offset = 0; 100 | while (offset < key.length && sameKey) { 101 | sameKey = key[offset] === this.key[offset]; 102 | offset++; 103 | } 104 | if (sameKey) { 105 | return; 106 | } 107 | this.key = key; 108 | const keySize = (this.keySize = key.length); 109 | if (keySize !== 4 && keySize !== 6 && keySize !== 8) { 110 | throw new Error("Invalid aes key size=" + keySize); 111 | } 112 | const ksRows = (this.ksRows = (keySize + 6 + 1) * 4); 113 | let ksRow; 114 | let invKsRow; 115 | const keySchedule = (this.keySchedule = new Uint32Array(ksRows)); 116 | const invKeySchedule = (this.invKeySchedule = new Uint32Array(ksRows)); 117 | const sbox = this.sBox; 118 | const rcon = this.rcon; 119 | const invSubMix = this.invSubMix; 120 | const invSubMix0 = invSubMix[0]; 121 | const invSubMix1 = invSubMix[1]; 122 | const invSubMix2 = invSubMix[2]; 123 | const invSubMix3 = invSubMix[3]; 124 | let prev; 125 | let t; 126 | for (ksRow = 0; ksRow < ksRows; ksRow++) { 127 | if (ksRow < keySize) { 128 | prev = keySchedule[ksRow] = key[ksRow]; 129 | continue; 130 | } 131 | t = prev; 132 | if (ksRow % keySize === 0) { 133 | // Rot word 134 | t = (t << 8) | (t >>> 24); 135 | // Sub word 136 | t = 137 | (sbox[t >>> 24] << 24) | 138 | (sbox[(t >>> 16) & 0xff] << 16) | 139 | (sbox[(t >>> 8) & 0xff] << 8) | 140 | sbox[t & 0xff]; 141 | // Mix Rcon 142 | t ^= rcon[(ksRow / keySize) | 0] << 24; 143 | } else if (keySize > 6 && ksRow % keySize === 4) { 144 | // Sub word 145 | t = 146 | (sbox[t >>> 24] << 24) | 147 | (sbox[(t >>> 16) & 0xff] << 16) | 148 | (sbox[(t >>> 8) & 0xff] << 8) | 149 | sbox[t & 0xff]; 150 | } 151 | keySchedule[ksRow] = prev = (keySchedule[ksRow - keySize] ^ t) >>> 0; 152 | } 153 | for (invKsRow = 0; invKsRow < ksRows; invKsRow++) { 154 | ksRow = ksRows - invKsRow; 155 | if (invKsRow & 3) { 156 | t = keySchedule[ksRow]; 157 | } else { 158 | t = keySchedule[ksRow - 4]; 159 | } 160 | if (invKsRow < 4 || ksRow <= 4) { 161 | invKeySchedule[invKsRow] = t; 162 | } else { 163 | invKeySchedule[invKsRow] = 164 | invSubMix0[sbox[t >>> 24]] ^ 165 | invSubMix1[sbox[(t >>> 16) & 0xff]] ^ 166 | invSubMix2[sbox[(t >>> 8) & 0xff]] ^ 167 | invSubMix3[sbox[t & 0xff]]; 168 | } 169 | invKeySchedule[invKsRow] = invKeySchedule[invKsRow] >>> 0; 170 | } 171 | } 172 | // Adding this as a method greatly improves performance. 173 | networkToHostOrderSwap(word) { 174 | return ( 175 | (word << 24) | 176 | ((word & 0xff00) << 8) | 177 | ((word & 0xff0000) >> 8) | 178 | (word >>> 24) 179 | ); 180 | } 181 | decrypt(inputArrayBuffer, offset, aesIV, removePKCS7Padding) { 182 | const nRounds = this.keySize + 6; 183 | const invKeySchedule = this.invKeySchedule; 184 | const invSBOX = this.invSBox; 185 | const invSubMix = this.invSubMix; 186 | const invSubMix0 = invSubMix[0]; 187 | const invSubMix1 = invSubMix[1]; 188 | const invSubMix2 = invSubMix[2]; 189 | const invSubMix3 = invSubMix[3]; 190 | const initVector = this.uint8ArrayToUint32Array_(aesIV); 191 | let initVector0 = initVector[0]; 192 | let initVector1 = initVector[1]; 193 | let initVector2 = initVector[2]; 194 | let initVector3 = initVector[3]; 195 | const inputInt32 = new Int32Array(inputArrayBuffer); 196 | const outputInt32 = new Int32Array(inputInt32.length); 197 | let t0, t1, t2, t3; 198 | let s0, s1, s2, s3; 199 | let inputWords0, inputWords1, inputWords2, inputWords3; 200 | let ksRow, i; 201 | const swapWord = this.networkToHostOrderSwap; 202 | while (offset < inputInt32.length) { 203 | inputWords0 = swapWord(inputInt32[offset]); 204 | inputWords1 = swapWord(inputInt32[offset + 1]); 205 | inputWords2 = swapWord(inputInt32[offset + 2]); 206 | inputWords3 = swapWord(inputInt32[offset + 3]); 207 | s0 = inputWords0 ^ invKeySchedule[0]; 208 | s1 = inputWords3 ^ invKeySchedule[1]; 209 | s2 = inputWords2 ^ invKeySchedule[2]; 210 | s3 = inputWords1 ^ invKeySchedule[3]; 211 | ksRow = 4; 212 | // Iterate through the rounds of decryption 213 | for (i = 1; i < nRounds; i++) { 214 | t0 = 215 | invSubMix0[s0 >>> 24] ^ 216 | invSubMix1[(s1 >> 16) & 0xff] ^ 217 | invSubMix2[(s2 >> 8) & 0xff] ^ 218 | invSubMix3[s3 & 0xff] ^ 219 | invKeySchedule[ksRow]; 220 | t1 = 221 | invSubMix0[s1 >>> 24] ^ 222 | invSubMix1[(s2 >> 16) & 0xff] ^ 223 | invSubMix2[(s3 >> 8) & 0xff] ^ 224 | invSubMix3[s0 & 0xff] ^ 225 | invKeySchedule[ksRow + 1]; 226 | t2 = 227 | invSubMix0[s2 >>> 24] ^ 228 | invSubMix1[(s3 >> 16) & 0xff] ^ 229 | invSubMix2[(s0 >> 8) & 0xff] ^ 230 | invSubMix3[s1 & 0xff] ^ 231 | invKeySchedule[ksRow + 2]; 232 | t3 = 233 | invSubMix0[s3 >>> 24] ^ 234 | invSubMix1[(s0 >> 16) & 0xff] ^ 235 | invSubMix2[(s1 >> 8) & 0xff] ^ 236 | invSubMix3[s2 & 0xff] ^ 237 | invKeySchedule[ksRow + 3]; 238 | // Update state 239 | s0 = t0; 240 | s1 = t1; 241 | s2 = t2; 242 | s3 = t3; 243 | ksRow = ksRow + 4; 244 | } 245 | // Shift rows, sub bytes, add round key 246 | t0 = 247 | (invSBOX[s0 >>> 24] << 24) ^ 248 | (invSBOX[(s1 >> 16) & 0xff] << 16) ^ 249 | (invSBOX[(s2 >> 8) & 0xff] << 8) ^ 250 | invSBOX[s3 & 0xff] ^ 251 | invKeySchedule[ksRow]; 252 | t1 = 253 | (invSBOX[s1 >>> 24] << 24) ^ 254 | (invSBOX[(s2 >> 16) & 0xff] << 16) ^ 255 | (invSBOX[(s3 >> 8) & 0xff] << 8) ^ 256 | invSBOX[s0 & 0xff] ^ 257 | invKeySchedule[ksRow + 1]; 258 | t2 = 259 | (invSBOX[s2 >>> 24] << 24) ^ 260 | (invSBOX[(s3 >> 16) & 0xff] << 16) ^ 261 | (invSBOX[(s0 >> 8) & 0xff] << 8) ^ 262 | invSBOX[s1 & 0xff] ^ 263 | invKeySchedule[ksRow + 2]; 264 | t3 = 265 | (invSBOX[s3 >>> 24] << 24) ^ 266 | (invSBOX[(s0 >> 16) & 0xff] << 16) ^ 267 | (invSBOX[(s1 >> 8) & 0xff] << 8) ^ 268 | invSBOX[s2 & 0xff] ^ 269 | invKeySchedule[ksRow + 3]; 270 | // Write 271 | outputInt32[offset] = swapWord(t0 ^ initVector0); 272 | outputInt32[offset + 1] = swapWord(t3 ^ initVector1); 273 | outputInt32[offset + 2] = swapWord(t2 ^ initVector2); 274 | outputInt32[offset + 3] = swapWord(t1 ^ initVector3); 275 | // reset initVector to last 4 unsigned int 276 | initVector0 = inputWords0; 277 | initVector1 = inputWords1; 278 | initVector2 = inputWords2; 279 | initVector3 = inputWords3; 280 | offset = offset + 4; 281 | } 282 | return removePKCS7Padding 283 | ? this.removePadding(outputInt32.buffer) 284 | : outputInt32.buffer; 285 | } 286 | destroy() { 287 | this.key = undefined; 288 | this.keySize = undefined; 289 | this.ksRows = undefined; 290 | this.sBox = undefined; 291 | this.invSBox = undefined; 292 | this.subMix = undefined; 293 | this.invSubMix = undefined; 294 | this.keySchedule = undefined; 295 | this.invKeySchedule = undefined; 296 | this.rcon = undefined; 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /m3u8.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | titleM3U8 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 |
20 |
21 |
22 | 23 |
24 |

25 |
26 | 28 | 29 |
30 | 31 |
32 | 33 |
34 | 35 | 36 |
37 |
38 | 39 |
40 |

41 | 42 |
43 |
44 |
45 |
46 |
47 |

48 | 49 |
50 |
51 |
52 |
53 |
54 |

55 | 56 |
57 |
58 |
59 |
60 | 61 |
62 |
63 | 64 |
65 |
66 | 67 |
68 |

69 | 70 |
71 |
72 |

73 |

74 |
75 | 76 |
77 |
78 | 81 | 82 |
83 | 84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | 92 | 94 |
95 |
96 | 97 | 98 | 99 | 100 | 101 |
102 |
103 |
104 | 105 |
106 |
107 | 108 | 109 | 110 | 111 | 112 | 113 | 116 | 117 | 118 | 119 |
120 | 121 |
122 |
123 | 125 | 127 | 128 | 129 | 130 |
131 |
132 |
133 |

135 |
136 | 139 | 142 | 145 | 148 | 151 | 154 | 157 |
158 |

159 | 162 |
163 | 164 | 165 |
166 |
167 | 169 |
170 |

(test) 172 |
173 | 174 | 175 | 176 | 177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 | 186 |
187 |
188 |
189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | -------------------------------------------------------------------------------- /manifest.firefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": { 3 | "scripts": [ 4 | "js/firefox.js", 5 | "js/background.js" 6 | ] 7 | }, 8 | "action": { 9 | "default_icon": "img/icon.png", 10 | "default_title": "__MSG_catCatch__", 11 | "default_popup": "popup.html" 12 | }, 13 | "description": "__MSG_description__", 14 | "icons": { 15 | "64": "img/icon.png", 16 | "128": "img/icon128.png" 17 | }, 18 | "manifest_version": 3, 19 | "name": "__MSG_catCatch__", 20 | "homepage_url": "https://github.com/xifangczy/cat-catch", 21 | "options_ui": { 22 | "page": "options.html", 23 | "open_in_tab": true 24 | }, 25 | "permissions": [ 26 | "tabs", 27 | "webRequest", 28 | "downloads", 29 | "storage", 30 | "webNavigation", 31 | "alarms", 32 | "scripting", 33 | "declarativeNetRequest" 34 | ], 35 | "commands": { 36 | "_execute_browser_action": {}, 37 | "enable": { 38 | "description": "__MSG_pause__ / __MSG_enable__" 39 | }, 40 | "auto_down": { 41 | "description": "__MSG_autoDownload__" 42 | }, 43 | "catch": { 44 | "description": "__MSG_cacheCapture__" 45 | }, 46 | "m3u8": { 47 | "description": "__MSG_m3u8Parser__" 48 | }, 49 | "clear": { 50 | "description": "__MSG_clear__" 51 | } 52 | }, 53 | "browser_specific_settings": { 54 | "gecko": { 55 | "id": "xifangczy@gmail.com", 56 | "strict_min_version": "113.0" 57 | } 58 | }, 59 | "host_permissions": [ 60 | "*://*/*", 61 | "" 62 | ], 63 | "content_scripts": [ 64 | { 65 | "matches": [ 66 | "https://*/*", 67 | "http://*/*" 68 | ], 69 | "js": [ 70 | "js/content-script.js" 71 | ], 72 | "all_frames": true, 73 | "run_at": "document_start" 74 | } 75 | ], 76 | "web_accessible_resources": [ 77 | { 78 | "resources": [ 79 | "catch-script/*" 80 | ], 81 | "matches": [ 82 | "" 83 | ] 84 | } 85 | ], 86 | "default_locale": "en", 87 | "version": "2.6.3" 88 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": { 3 | "service_worker": "js/background.js" 4 | }, 5 | "action": { 6 | "default_icon": "img/icon.png", 7 | "default_title": "__MSG_catCatch__", 8 | "default_popup": "popup.html" 9 | }, 10 | "description": "__MSG_description__", 11 | "icons": { 12 | "64": "img/icon.png", 13 | "128": "img/icon128.png" 14 | }, 15 | "manifest_version": 3, 16 | "minimum_chrome_version": "93", 17 | "name": "__MSG_catCatch__", 18 | "homepage_url": "https://github.com/xifangczy/cat-catch", 19 | "options_ui": { 20 | "page": "options.html", 21 | "open_in_tab": true 22 | }, 23 | "permissions": [ 24 | "tabs", 25 | "webRequest", 26 | "downloads", 27 | "storage", 28 | "webNavigation", 29 | "alarms", 30 | "declarativeNetRequest", 31 | "scripting", 32 | "sidePanel" 33 | ], 34 | "commands": { 35 | "_execute_action": {}, 36 | "enable": { 37 | "description": "__MSG_pause__ / __MSG_enable__" 38 | }, 39 | "auto_down": { 40 | "description": "__MSG_autoDownload__" 41 | }, 42 | "catch": { 43 | "description": "__MSG_cacheCapture__" 44 | }, 45 | "m3u8": { 46 | "description": "__MSG_m3u8Parser__" 47 | }, 48 | "clear": { 49 | "description": "__MSG_clear__" 50 | } 51 | }, 52 | "host_permissions": [ 53 | "*://*/*", 54 | "" 55 | ], 56 | "content_scripts": [ 57 | { 58 | "matches": [ 59 | "https://*/*", 60 | "http://*/*" 61 | ], 62 | "js": [ 63 | "js/content-script.js" 64 | ], 65 | "run_at": "document_start", 66 | "all_frames": true 67 | } 68 | ], 69 | "web_accessible_resources": [ 70 | { 71 | "resources": [ 72 | "catch-script/*" 73 | ], 74 | "matches": [ 75 | "" 76 | ] 77 | } 78 | ], 79 | "default_locale": "en", 80 | "version": "2.6.3", 81 | "incognito": "split" 82 | } -------------------------------------------------------------------------------- /mpd.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | titledash 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 |
22 | 23 |
24 |

25 |
26 | 27 | 28 | 29 |
30 |
31 | 32 |
33 |

34 |
35 |
36 | mpd url 37 |

38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | 46 |
47 |
48 |
49 |

: 50 |
51 | 52 | 53 |
54 |
55 |
56 |
57 |

: 58 |
59 | 60 | 61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | catCatch 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 |
19 |
20 |
21 | 22 |
23 |
24 |
25 | / 26 |
27 |
28 | 29 |
30 |
31 |
32 |
33 |
~
34 |
35 |
36 |
37 |
38 |
39 | 40 | 43 |
44 |
45 | 46 | 49 |
50 |
51 | 53 | 54 | 55 | 56 | 57 | 58 | 61 | 64 |
65 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | 76 | 77 | 78 | 79 | 80 |
81 |
82 |
83 |
84 | 85 | 87 | 88 |
89 |
90 |
91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 |
103 |
104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 |
118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /preview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | filter 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 | 26 |
27 |
28 | 29 |
30 | 31 |
32 |
33 | 34 |
35 | 36 |
37 |
38 | 41 | 44 | 47 |
48 |
49 | 52 | 55 |
56 |
57 |
58 | 59 |
60 | 61 | 64 |
65 | 66 |
67 | 68 | 69 |
70 | 71 |
72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 89 |
90 |
91 | 92 | 93 | 94 |
95 |
96 | 97 | 98 | 103 |
104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | --------------------------------------------------------------------------------