m3u8-cli 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. {m3u8_cli-0.2.0 → m3u8_cli-0.2.1}/PKG-INFO +13 -3
  2. {m3u8_cli-0.2.0 → m3u8_cli-0.2.1}/README.md +12 -2
  3. {m3u8_cli-0.2.0 → m3u8_cli-0.2.1}/chrome-extension/README.md +5 -4
  4. {m3u8_cli-0.2.0 → m3u8_cli-0.2.1}/chrome-extension/background.js +7 -11
  5. {m3u8_cli-0.2.0 → m3u8_cli-0.2.1}/chrome-extension/manifest.json +1 -1
  6. {m3u8_cli-0.2.0 → m3u8_cli-0.2.1}/chrome-extension/popup.css +177 -25
  7. m3u8_cli-0.2.1/chrome-extension/popup.html +293 -0
  8. {m3u8_cli-0.2.0 → m3u8_cli-0.2.1}/chrome-extension/popup.js +111 -5
  9. {m3u8_cli-0.2.0 → m3u8_cli-0.2.1}/pyproject.toml +1 -1
  10. {m3u8_cli-0.2.0 → m3u8_cli-0.2.1}/pyproject.toml.orig +1 -1
  11. {m3u8_cli-0.2.0 → m3u8_cli-0.2.1}/src/m3u8_cli/__init__.py +1 -1
  12. {m3u8_cli-0.2.0 → m3u8_cli-0.2.1}/src/m3u8_cli/serve.py +38 -7
  13. m3u8_cli-0.2.0/chrome-extension/popup.html +0 -112
  14. {m3u8_cli-0.2.0 → m3u8_cli-0.2.1}/LICENSE +0 -0
  15. {m3u8_cli-0.2.0 → m3u8_cli-0.2.1}/chrome-extension/content.js +0 -0
  16. {m3u8_cli-0.2.0 → m3u8_cli-0.2.1}/chrome-extension/lib.js +0 -0
  17. {m3u8_cli-0.2.0 → m3u8_cli-0.2.1}/chrome-extension/lib.test.js +0 -0
  18. {m3u8_cli-0.2.0 → m3u8_cli-0.2.1}/scripts/build_extension.py +0 -0
  19. {m3u8_cli-0.2.0 → m3u8_cli-0.2.1}/src/m3u8_cli/__main__.py +0 -0
  20. {m3u8_cli-0.2.0 → m3u8_cli-0.2.1}/src/m3u8_cli/cli.py +0 -0
  21. {m3u8_cli-0.2.0 → m3u8_cli-0.2.1}/src/m3u8_cli/convert.py +0 -0
  22. {m3u8_cli-0.2.0 → m3u8_cli-0.2.1}/src/m3u8_cli/download.py +0 -0
  23. {m3u8_cli-0.2.0 → m3u8_cli-0.2.1}/src/m3u8_cli/downloader.py +0 -0
  24. {m3u8_cli-0.2.0 → m3u8_cli-0.2.1}/src/m3u8_cli/errors.py +0 -0
  25. {m3u8_cli-0.2.0 → m3u8_cli-0.2.1}/src/m3u8_cli/hls_export.py +0 -0
  26. {m3u8_cli-0.2.0 → m3u8_cli-0.2.1}/src/m3u8_cli/merge.py +0 -0
  27. {m3u8_cli-0.2.0 → m3u8_cli-0.2.1}/src/m3u8_cli/paths.py +0 -0
  28. {m3u8_cli-0.2.0 → m3u8_cli-0.2.1}/src/m3u8_cli/playlist.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: m3u8-cli
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: HLS 视频下载与格式转换工具
5
5
  Author: r1a
6
6
  Author-email: r1a <m@zcq100.com>
@@ -200,6 +200,11 @@ m3u8-cli serve --host 0.0.0.0 --port 8787 --output-dir ./downloads
200
200
 
201
201
  启动日志会打印监听地址、输出目录与 API token;省略 --token 时会自动生成。
202
202
 
203
+ 来源为回环地址(`127.0.0.0/8`、`::1`,含 IPv4-mapped IPv6)的请求**免 token**,方便本机扩展/脚本直接使用;其他来源仍需 `Authorization: Bearer <token>`。注意:
204
+
205
+ - 这意味着同一台机器上的其他用户或进程也能免 token 访问;如果通过监听在本机的反向代理转发请求,所有流量看起来都来自回环地址。
206
+ - API 的 CORS 为 `*`,免 token 也意味着浏览器里的任意网页都能调用本机 serve(读取任务、提交下载)。如果在意这一点,请保留 Token(网页无法获取 Token),或自行收紧 CORS。
207
+
203
208
  ```bash
204
209
  # 创建任务
205
210
  curl -X POST http://127.0.0.1:8787/api/tasks \
@@ -217,6 +222,9 @@ curl -X POST http://127.0.0.1:8787/api/tasks \
217
222
  curl http://127.0.0.1:8787/api/tasks -H "Authorization: Bearer <token>"
218
223
  curl http://127.0.0.1:8787/api/tasks/<id> -H "Authorization: Bearer <token>"
219
224
 
225
+ # 服务版本
226
+ curl http://127.0.0.1:8787/api/version -H "Authorization: Bearer <token>"
227
+
220
228
  # 取消任务
221
229
  curl -X DELETE http://127.0.0.1:8787/api/tasks/<id> \
222
230
  -H "Authorization: Bearer <token>"
@@ -235,7 +243,8 @@ curl -X DELETE http://127.0.0.1:8787/api/tasks/<id> \
235
243
  - 监听页面网络请求并结合 DOM 扫描,列出当前标签页的 m3u8 地址;
236
244
  - 采集页面标题(`og:title`、Twitter Card、JSON-LD、`document.title`)作为默认下载文件名,也可在设置中改用 URL 短 hash;
237
245
  - 复制链接,或生成带 `Referer`/`Cookie`/`User-Agent` 的 `m3u8-cli download` 命令;
238
- - 一键提交到本地 `m3u8-cli serve` 下载,并在弹窗中查看进度、取消任务。
246
+ - 一键提交到本地 `m3u8-cli serve` 下载,并在弹窗中查看进度、取消任务;
247
+ - 设置页显示扩展版本与后端 `serve` 版本(依赖 `GET /api/version`)。
239
248
 
240
249
  安装步骤:打开 `chrome://extensions`,开启“开发者模式”,点击“加载已解压的扩展程序”,选择仓库中的 `chrome-extension/` 目录。
241
250
 
@@ -316,7 +325,8 @@ movie/
316
325
  - HLS AES-128 加密不是 DRM;能访问 `key.bin` 的客户端即可解密。
317
326
  - FFmpeg 的 HLS muxer 会写入固定 `IV=0`,所有分片共用同一密钥与 IV。
318
327
  - 不支持自定义密钥、密钥 URI 或 IV。
319
- - serve 不做 SSRF 内网地址屏蔽,依赖 token 与局域网部署。
328
+ - serve 不做 SSRF 内网地址屏蔽;回环地址请求免 token,非本机来源仍依赖 token 与局域网部署。
329
+ - 回环免 token 对同一台机器上的所有用户生效;多用户主机或本机反向代理场景请谨慎,必要时只监听 `127.0.0.1`。
320
330
  - Windows 默认 `MAX_PATH=260`:serve 会按输出目录长度截短过长的文件名(保留扩展名),任务响应中返回实际名字;CLI 的 `-o` 不做改写。
321
331
  - serve 不提供下载产物接口,文件留在服务器。
322
332
  - serve 重启后任务标记为 `interrupted`,不会自动恢复。
@@ -187,6 +187,11 @@ m3u8-cli serve --host 0.0.0.0 --port 8787 --output-dir ./downloads
187
187
 
188
188
  启动日志会打印监听地址、输出目录与 API token;省略 --token 时会自动生成。
189
189
 
190
+ 来源为回环地址(`127.0.0.0/8`、`::1`,含 IPv4-mapped IPv6)的请求**免 token**,方便本机扩展/脚本直接使用;其他来源仍需 `Authorization: Bearer <token>`。注意:
191
+
192
+ - 这意味着同一台机器上的其他用户或进程也能免 token 访问;如果通过监听在本机的反向代理转发请求,所有流量看起来都来自回环地址。
193
+ - API 的 CORS 为 `*`,免 token 也意味着浏览器里的任意网页都能调用本机 serve(读取任务、提交下载)。如果在意这一点,请保留 Token(网页无法获取 Token),或自行收紧 CORS。
194
+
190
195
  ```bash
191
196
  # 创建任务
192
197
  curl -X POST http://127.0.0.1:8787/api/tasks \
@@ -204,6 +209,9 @@ curl -X POST http://127.0.0.1:8787/api/tasks \
204
209
  curl http://127.0.0.1:8787/api/tasks -H "Authorization: Bearer <token>"
205
210
  curl http://127.0.0.1:8787/api/tasks/<id> -H "Authorization: Bearer <token>"
206
211
 
212
+ # 服务版本
213
+ curl http://127.0.0.1:8787/api/version -H "Authorization: Bearer <token>"
214
+
207
215
  # 取消任务
208
216
  curl -X DELETE http://127.0.0.1:8787/api/tasks/<id> \
209
217
  -H "Authorization: Bearer <token>"
@@ -222,7 +230,8 @@ curl -X DELETE http://127.0.0.1:8787/api/tasks/<id> \
222
230
  - 监听页面网络请求并结合 DOM 扫描,列出当前标签页的 m3u8 地址;
223
231
  - 采集页面标题(`og:title`、Twitter Card、JSON-LD、`document.title`)作为默认下载文件名,也可在设置中改用 URL 短 hash;
224
232
  - 复制链接,或生成带 `Referer`/`Cookie`/`User-Agent` 的 `m3u8-cli download` 命令;
225
- - 一键提交到本地 `m3u8-cli serve` 下载,并在弹窗中查看进度、取消任务。
233
+ - 一键提交到本地 `m3u8-cli serve` 下载,并在弹窗中查看进度、取消任务;
234
+ - 设置页显示扩展版本与后端 `serve` 版本(依赖 `GET /api/version`)。
226
235
 
227
236
  安装步骤:打开 `chrome://extensions`,开启“开发者模式”,点击“加载已解压的扩展程序”,选择仓库中的 `chrome-extension/` 目录。
228
237
 
@@ -303,7 +312,8 @@ movie/
303
312
  - HLS AES-128 加密不是 DRM;能访问 `key.bin` 的客户端即可解密。
304
313
  - FFmpeg 的 HLS muxer 会写入固定 `IV=0`,所有分片共用同一密钥与 IV。
305
314
  - 不支持自定义密钥、密钥 URI 或 IV。
306
- - serve 不做 SSRF 内网地址屏蔽,依赖 token 与局域网部署。
315
+ - serve 不做 SSRF 内网地址屏蔽;回环地址请求免 token,非本机来源仍依赖 token 与局域网部署。
316
+ - 回环免 token 对同一台机器上的所有用户生效;多用户主机或本机反向代理场景请谨慎,必要时只监听 `127.0.0.1`。
307
317
  - Windows 默认 `MAX_PATH=260`:serve 会按输出目录长度截短过长的文件名(保留扩展名),任务响应中返回实际名字;CLI 的 `-o` 不做改写。
308
318
  - serve 不提供下载产物接口,文件留在服务器。
309
319
  - serve 重启后任务标记为 `interrupted`,不会自动恢复。
@@ -48,7 +48,7 @@ uv run m3u8-cli serve --host 127.0.0.1 --port 8787 --token mytoken --output-dir
48
48
  | 设置项 | 说明 |
49
49
  | --- | --- |
50
50
  | 服务地址 | 默认 `http://127.0.0.1:8787`,需与 `--host/--port` 一致 |
51
- | Token | 与 `--token` 一致;点击“测试连接”确认 |
51
+ | Token | 与 `--token` 一致;本机 serve(回环地址)可留空,点击“测试连接”确认 |
52
52
  | 下载格式 | `mp4`(合并封装)或 `hls`(保留分片) |
53
53
  | 文件名 | `页面标题`(默认)或 `URL 短 hash`(12 位十六进制;URL 带临时 token 时每次可能不同) |
54
54
  | 覆盖已存在的文件 | 对应 serve 的 `overwrite` 字段 |
@@ -60,7 +60,7 @@ uv run m3u8-cli serve --host 127.0.0.1 --port 8787 --token mytoken --output-dir
60
60
 
61
61
  1. 打开包含视频的页面并开始播放,或等待页面加载完成。
62
62
  2. 扩展图标上的 badge 显示当前标签页识别到的 m3u8 数量。
63
- 3. 点击扩展图标,候选条目会显示页面标题和 m3u8 URL:
63
+ 3. 点击扩展图标,弹窗分为「链接 / 任务 / 设置 / 关于」四个标签页;「关于」页分为「项目描述 / 使用帮助 / 项目信息」:使用帮助包含 `pip install m3u8-cli`、可折行展示的 serve 启动命令与扩展配置提示,项目信息包含 PyPI 主页、作者/邮箱与 MIT 许可证链接;「链接」中候选条目会显示页面标题和 m3u8 URL:
64
64
  - **复制**:复制 m3u8 URL。
65
65
  - **复制 CLI**:生成 `m3u8-cli download ...` 命令,按设置决定是否包含 `--referer`、`--cookie`、`--user-agent`;`-o` 默认使用页面标题。
66
66
  - **下载**:提交到本地 serve 服务,默认文件名同样来自页面标题;若提示文件名冲突,可点击“换名重试”自动加时间戳。
@@ -101,6 +101,7 @@ uv run m3u8-cli serve --host 127.0.0.1 --port 8787 --token mytoken --output-dir
101
101
  - 服务地址不是本机(`127.0.0.1`/`localhost`/`::1`)且开启 Cookie 透传时,设置区会显示警告。
102
102
  - Token 保存在 `storage.local`,请勿在不信任的共享 Chrome 配置中使用。
103
103
  - serve 端请求头只保留在内存,不落盘、不在任务响应中返回;详见仓库 README。
104
+ - serve 对回环地址请求免 token:Token 留空时扩展默认只适合连接本机 serve;同一台机器上的其他用户/进程也能免 token 访问该服务。API 的 CORS 为 `*`,因此任意网页也能调用本机 serve;若在意请在扩展里保留 Token(网页拿不到它)。
104
105
  - 扩展不会自动提交下载,必须由用户点击,避免页面借扩展触发 serve 请求。
105
106
  - 「跳过 TLS 证书校验」会关闭 serve 到源站的证书与主机名校验,可能暴露下载内容给中间人;仅在确认源站可信(如自签名内网服务)时开启。
106
107
  - CORS `*` 不是安全边界,真正的访问控制是 Bearer token。
@@ -113,7 +114,7 @@ uv run m3u8-cli serve --host 127.0.0.1 --port 8787 --token mytoken --output-dir
113
114
  | 提示“未配置 Token” | 在扩展设置中填写 serve 的 `--token` |
114
115
  | “无法连接 http://...(Failed to fetch)” | serve 未启动、地址/端口错误,或协议不是 `http/https` |
115
116
  | 提示“连接 ... 超时(8 秒)” | 服务可达性差或进程卡住;确认 serve 正常运行并检查网络/防火墙 |
116
- | 401 / “未授权” | Token 与服务端不一致;serve 未指定 `--token` 时每次启动都会生成新 token |
117
+ | 401 / “未授权” | 非本机请求需要 Token;本机回环请求免 token,若仍 401 检查是否经过代理或填了错误 Token |
117
118
  | 409 / 目标文件已存在 | 点击“换名重试”,或在设置中开启“覆盖已存在的文件” |
118
119
  | 下载失败,提示 403 | 源站可能校验 `Referer`/`Cookie`/`User-Agent`;确认候选来自“网络”且页面仍处于登录状态 |
119
120
  | 失败原因是 `CERTIFICATE_VERIFY_FAILED` / Hostname mismatch | 源站证书不匹配(自签名或内网域名);确认可信后开启「跳过 TLS 证书校验」 |
@@ -153,7 +154,7 @@ uv run python scripts/build_extension.py
153
154
  - `storage.session["hdr:<candidateId>"]`:`{cookie, referer, userAgent}`,仅 background 读取。
154
155
  - `storage.local["settings"]`:服务地址、token、格式、`naming`(`title`/`hash`)、`insecure`(跳过源站 TLS 校验)等设置。
155
156
  - `storage.local["tasks"]`:最近的任务摘要(最多 20 条,优先保留活动任务),不含请求头。
156
- - popup 通过 `chrome.runtime.sendMessage` 发送 `download:submit`、`task:cancel`、`tasks:refresh`、`settings:test`、`cli:build`、`tab:clear`、`dom:rescan`。
157
+ - popup 通过 `chrome.runtime.sendMessage` 发送 `download:submit`、`task:cancel`、`tasks:refresh`、`settings:test`、`server:version`、`cli:build`、`tab:clear`、`dom:rescan`。`server:version` 请求后端的 `GET /api/version`;设置页底部显示「插件 vX · 后端 vY」,并附 PyPI 链接;无法连接/需要 Token/旧版 serve(无该接口)分别显示对应状态。
157
158
  - content script 通过 `dom:candidates` 上报 URL 与 `title`(附 `frameTop` 标记是否顶层 frame),用 `sender.tab.id` 归属标签页。
158
159
 
159
160
  ## 已知限制
@@ -418,7 +418,9 @@ async function apiFetch(path, options) {
418
418
  headers: { ...((options && options.headers) || {}) },
419
419
  signal: AbortSignal.timeout(8000),
420
420
  };
421
- init.headers.Authorization = `Bearer ${settings.token}`;
421
+ if (settings.token) {
422
+ init.headers.Authorization = `Bearer ${settings.token}`;
423
+ }
422
424
 
423
425
  let response;
424
426
  try {
@@ -468,12 +470,6 @@ async function submitDownload(message) {
468
470
  return { ok: false, error: "无效的标签页" };
469
471
  }
470
472
  const settings = await getSettings();
471
- if (!settings.token) {
472
- return {
473
- ok: false,
474
- error: "未配置 Token:请在扩展设置中填写 serve 的 token",
475
- };
476
- }
477
473
  const state = await readTabState(tabId);
478
474
  const candidate = state.items.find((item) => item.id === message.candidateId);
479
475
  if (!candidate) {
@@ -523,10 +519,6 @@ async function submitDownload(message) {
523
519
  }
524
520
 
525
521
  async function testConnection() {
526
- const settings = await getSettings();
527
- if (!settings.token) {
528
- return { ok: false, error: "请先填写 Token" };
529
- }
530
522
  try {
531
523
  const result = await apiFetch("/api/tasks?limit=1");
532
524
  return { ok: true, total: Number(result.total || 0) };
@@ -722,6 +714,10 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
722
714
  return { ok: true, tasks };
723
715
  },
724
716
  "settings:test": () => testConnection(),
717
+ "server:version": async () => {
718
+ const result = await apiFetch("/api/version");
719
+ return { ok: true, version: String(result.version || "") };
720
+ },
725
721
  "cli:build": () => buildCliMessage(message),
726
722
  "tab:clear": () => clearTabAndRescan(message.tabId),
727
723
  "dom:rescan": () => rescanTab(message.tabId),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "m3u8-cli 链接检测器",
4
- "version": "1.3.0",
4
+ "version": "1.8.0",
5
5
  "description": "识别页面中的 m3u8 播放列表,复制链接或发送到本地 m3u8-cli 服务下载。",
6
6
  "minimum_chrome_version": "116",
7
7
  "permissions": ["webRequest", "storage", "alarms"],
@@ -97,10 +97,9 @@ button:disabled {
97
97
  top: 0;
98
98
  z-index: 2;
99
99
  display: flex;
100
- align-items: center;
101
- justify-content: space-between;
102
- gap: 8px;
103
- padding: 10px 14px;
100
+ flex-direction: column;
101
+ gap: 6px;
102
+ padding: 10px 14px 0;
104
103
  background: var(--panel);
105
104
  border-bottom: 1px solid var(--border);
106
105
  box-shadow: var(--shadow);
@@ -133,26 +132,74 @@ h1 {
133
132
  white-space: nowrap;
134
133
  }
135
134
 
136
- .count {
137
- min-width: 20px;
138
- padding: 0 6px;
135
+ .tabs {
136
+ display: flex;
137
+ gap: 2px;
138
+ margin: 0 -14px;
139
+ padding: 0 14px;
140
+ }
141
+
142
+ .tab {
143
+ display: inline-flex;
144
+ align-items: center;
145
+ gap: 5px;
146
+ margin-bottom: -1px;
147
+ padding: 7px 10px 6px;
148
+ border: none;
149
+ border-bottom: 2px solid transparent;
150
+ border-radius: 0;
151
+ background: transparent;
152
+ color: var(--muted);
153
+ font-size: 12.5px;
154
+ font-weight: 600;
155
+ }
156
+
157
+ .tab:hover:not(:disabled) {
158
+ background: transparent;
159
+ color: var(--accent-strong);
160
+ }
161
+
162
+ .tab.active {
163
+ color: var(--accent-strong);
164
+ border-bottom-color: var(--accent);
165
+ }
166
+
167
+ .tab:focus-visible {
168
+ outline: 2px solid var(--accent-soft);
169
+ outline-offset: -2px;
170
+ }
171
+
172
+ .tab-count {
173
+ min-width: 18px;
174
+ padding: 0 5px;
139
175
  border-radius: 999px;
140
176
  background: var(--accent-soft);
141
177
  color: var(--accent-strong);
142
- font-size: 11px;
178
+ font-size: 10px;
143
179
  font-weight: 600;
180
+ line-height: 16px;
144
181
  text-align: center;
145
182
  }
146
183
 
147
- .header-actions {
148
- display: flex;
149
- gap: 6px;
184
+ .tab.active .tab-count {
185
+ background: var(--accent);
186
+ color: #fff;
150
187
  }
151
188
 
152
189
  main {
153
190
  padding: 10px 14px 16px;
154
191
  }
155
192
 
193
+ .section-actions {
194
+ display: flex;
195
+ gap: 6px;
196
+ }
197
+
198
+ .hint {
199
+ color: var(--muted);
200
+ font-size: 11px;
201
+ }
202
+
156
203
  .section + .section {
157
204
  margin-top: 14px;
158
205
  }
@@ -372,24 +419,11 @@ h2 {
372
419
  word-break: break-word;
373
420
  }
374
421
 
375
- .settings {
376
- margin-top: 16px;
377
- border-top: 1px solid var(--border);
378
- padding-top: 10px;
379
- }
380
-
381
- .settings summary {
382
- cursor: pointer;
383
- color: var(--muted);
384
- font-size: 12px;
385
- font-weight: 600;
386
- }
387
-
388
422
  .settings-grid {
389
423
  display: flex;
390
424
  flex-direction: column;
391
425
  gap: 9px;
392
- padding-top: 10px;
426
+ padding-top: 4px;
393
427
  }
394
428
 
395
429
  .settings-grid > label {
@@ -463,6 +497,124 @@ h2 {
463
497
  color: var(--danger);
464
498
  }
465
499
 
500
+ .version-info {
501
+ display: flex;
502
+ align-items: baseline;
503
+ justify-content: space-between;
504
+ gap: 8px;
505
+ margin-top: 12px;
506
+ padding-top: 8px;
507
+ border-top: 1px solid var(--border);
508
+ color: var(--muted);
509
+ font-size: 11px;
510
+ }
511
+
512
+ .version-info.connected {
513
+ color: var(--success);
514
+ }
515
+
516
+ .version-info a {
517
+ flex: none;
518
+ color: var(--accent-strong);
519
+ text-decoration: none;
520
+ }
521
+
522
+ .version-info a:hover {
523
+ text-decoration: underline;
524
+ }
525
+
526
+ .about-section {
527
+ margin-top: 14px;
528
+ }
529
+
530
+ .section-title-row + .about-section {
531
+ margin-top: 8px;
532
+ }
533
+
534
+ .about-section h3 {
535
+ margin: 0 0 6px;
536
+ font-size: 12px;
537
+ font-weight: 600;
538
+ color: var(--muted);
539
+ letter-spacing: 0.02em;
540
+ }
541
+
542
+ .about-desc {
543
+ margin: 0 0 6px;
544
+ color: var(--muted);
545
+ font-size: 12px;
546
+ }
547
+
548
+ .about-list {
549
+ margin: 0;
550
+ padding-left: 18px;
551
+ color: var(--muted);
552
+ font-size: 12px;
553
+ display: flex;
554
+ flex-direction: column;
555
+ gap: 3px;
556
+ }
557
+
558
+ .about-step + .about-step {
559
+ margin-top: 10px;
560
+ }
561
+
562
+ .about-step h4 {
563
+ margin: 0 0 5px;
564
+ font-size: 12px;
565
+ font-weight: 600;
566
+ }
567
+
568
+ .about-note {
569
+ margin: 5px 0 0;
570
+ color: var(--muted);
571
+ font-size: 11.5px;
572
+ }
573
+
574
+ .about-section pre {
575
+ margin: 0;
576
+ padding: 8px 10px;
577
+ border: 1px solid var(--border);
578
+ border-radius: 6px;
579
+ background: var(--accent-soft);
580
+ white-space: pre-wrap;
581
+ overflow-wrap: anywhere;
582
+ font-size: 11.5px;
583
+ line-height: 1.5;
584
+ }
585
+
586
+ .about-section code {
587
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
588
+ font-size: 11.5px;
589
+ }
590
+
591
+ .about-section a {
592
+ color: var(--accent-strong);
593
+ text-decoration: none;
594
+ }
595
+
596
+ .about-section a:hover {
597
+ text-decoration: underline;
598
+ }
599
+
600
+ .about-info {
601
+ display: grid;
602
+ grid-template-columns: auto 1fr;
603
+ gap: 4px 10px;
604
+ margin: 0;
605
+ font-size: 12px;
606
+ }
607
+
608
+ .about-info dt {
609
+ color: var(--muted);
610
+ }
611
+
612
+ .about-info dd {
613
+ margin: 0;
614
+ min-width: 0;
615
+ overflow-wrap: anywhere;
616
+ }
617
+
466
618
  .toast {
467
619
  position: fixed;
468
620
  left: 50%;
@@ -0,0 +1,293 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>m3u8-cli 链接检测器</title>
7
+ <link rel="stylesheet" href="popup.css" />
8
+ </head>
9
+ <body>
10
+ <header class="app-header">
11
+ <div class="title-row">
12
+ <span class="logo" aria-hidden="true">▶</span>
13
+ <h1>m3u8 检测器</h1>
14
+ </div>
15
+ <nav id="tab-list" class="tabs" role="tablist" aria-label="视图">
16
+ <button
17
+ id="tab-candidates"
18
+ type="button"
19
+ class="tab active"
20
+ role="tab"
21
+ data-tab="candidates"
22
+ aria-controls="panel-candidates"
23
+ aria-selected="true"
24
+ >
25
+ 链接<span id="candidate-count" class="tab-count">0</span>
26
+ </button>
27
+ <button
28
+ id="tab-tasks"
29
+ type="button"
30
+ class="tab"
31
+ role="tab"
32
+ data-tab="tasks"
33
+ aria-controls="panel-tasks"
34
+ aria-selected="false"
35
+ tabindex="-1"
36
+ >
37
+ 任务<span id="task-count" class="tab-count" hidden>0</span>
38
+ </button>
39
+ <button
40
+ id="tab-settings"
41
+ type="button"
42
+ class="tab"
43
+ role="tab"
44
+ data-tab="settings"
45
+ aria-controls="panel-settings"
46
+ aria-selected="false"
47
+ tabindex="-1"
48
+ >
49
+ 设置
50
+ </button>
51
+ <button
52
+ id="tab-about"
53
+ type="button"
54
+ class="tab"
55
+ role="tab"
56
+ data-tab="about"
57
+ aria-controls="panel-about"
58
+ aria-selected="false"
59
+ tabindex="-1"
60
+ >
61
+ 关于
62
+ </button>
63
+ </nav>
64
+ </header>
65
+
66
+ <main>
67
+ <section
68
+ id="panel-candidates"
69
+ class="panel"
70
+ role="tabpanel"
71
+ aria-labelledby="tab-candidates"
72
+ >
73
+ <div class="section-title-row">
74
+ <h2>检测到的链接</h2>
75
+ <div class="section-actions">
76
+ <button id="rescan-button" type="button" class="ghost small">
77
+ 重新扫描
78
+ </button>
79
+ <button id="clear-button" type="button" class="ghost small">
80
+ 清空
81
+ </button>
82
+ </div>
83
+ </div>
84
+ <div id="empty-state" class="empty">
85
+ 未检测到 m3u8 链接。<br />
86
+ 播放视频或刷新页面后再试。
87
+ </div>
88
+ <ul id="candidate-list" class="candidate-list"></ul>
89
+ </section>
90
+
91
+ <section
92
+ id="panel-tasks"
93
+ class="panel"
94
+ role="tabpanel"
95
+ aria-labelledby="tab-tasks"
96
+ hidden
97
+ >
98
+ <div class="section-title-row">
99
+ <h2>下载任务</h2>
100
+ <button id="refresh-tasks" type="button" class="ghost small">
101
+ 刷新
102
+ </button>
103
+ </div>
104
+ <div id="task-empty" class="empty small-empty">暂无任务</div>
105
+ <ul id="task-list" class="task-list"></ul>
106
+ </section>
107
+
108
+ <section
109
+ id="panel-settings"
110
+ class="panel"
111
+ role="tabpanel"
112
+ aria-labelledby="tab-settings"
113
+ hidden
114
+ >
115
+ <div class="section-title-row">
116
+ <h2>设置</h2>
117
+ <span class="hint">修改后自动保存</span>
118
+ </div>
119
+ <div class="settings-grid">
120
+ <label>
121
+ <span>服务地址</span>
122
+ <input
123
+ id="server-url"
124
+ type="text"
125
+ spellcheck="false"
126
+ placeholder="http://127.0.0.1:8787"
127
+ />
128
+ </label>
129
+ <label>
130
+ <span>Token</span>
131
+ <input
132
+ id="token"
133
+ type="password"
134
+ autocomplete="off"
135
+ spellcheck="false"
136
+ placeholder="本机 serve 可留空"
137
+ />
138
+ </label>
139
+ <label>
140
+ <span>下载格式</span>
141
+ <select id="format">
142
+ <option value="mp4">MP4(合并封装)</option>
143
+ <option value="hls">HLS(保留分片)</option>
144
+ </select>
145
+ </label>
146
+ <label>
147
+ <span>文件名</span>
148
+ <select id="naming">
149
+ <option value="title">页面标题</option>
150
+ <option value="hash">URL 短 hash</option>
151
+ </select>
152
+ </label>
153
+ <label class="checkbox">
154
+ <input id="overwrite" type="checkbox" />
155
+ <span>覆盖已存在的文件</span>
156
+ </label>
157
+ <label class="checkbox">
158
+ <input id="send-cookie" type="checkbox" />
159
+ <span>发送捕获到的 Cookie(用于下载)</span>
160
+ </label>
161
+ <label class="checkbox">
162
+ <input id="copy-cookie" type="checkbox" />
163
+ <span>复制 CLI 命令时包含 Cookie(敏感)</span>
164
+ </label>
165
+ <label class="checkbox">
166
+ <input id="insecure" type="checkbox" />
167
+ <span>跳过 TLS 证书校验(不安全,仅用于自签名/内网服务)</span>
168
+ </label>
169
+ <div id="remote-warning" class="warning" hidden>
170
+ 服务地址不是本机,Cookie 会被发送到该地址,请确认其可信。
171
+ </div>
172
+ <div class="settings-actions">
173
+ <button id="test-connection" type="button" class="primary">
174
+ 测试连接
175
+ </button>
176
+ <span id="settings-status" class="status"></span>
177
+ </div>
178
+ </div>
179
+ <div id="version-info" class="version-info">
180
+ <span id="version-text">插件版本读取中…</span>
181
+ <a
182
+ id="version-pypi"
183
+ href="https://pypi.org/project/m3u8-cli/"
184
+ target="_blank"
185
+ rel="noopener noreferrer"
186
+ >PyPI</a
187
+ >
188
+ </div>
189
+ </section>
190
+
191
+ <section
192
+ id="panel-about"
193
+ class="panel"
194
+ role="tabpanel"
195
+ aria-labelledby="tab-about"
196
+ hidden
197
+ >
198
+ <div class="section-title-row">
199
+ <h2>关于</h2>
200
+ <span class="hint">m3u8-cli 链接检测器</span>
201
+ </div>
202
+
203
+ <div class="about-section">
204
+ <h3>项目描述</h3>
205
+ <p class="about-desc">
206
+ 本扩展用于发现网页中的 m3u8 播放列表,并与本地
207
+ <code>m3u8-cli</code> 下载服务配合使用:
208
+ </p>
209
+ <ul class="about-list">
210
+ <li>
211
+ 监听网络请求并扫描页面,识别 m3u8 地址,支持 iframe
212
+ 中的播放器
213
+ </li>
214
+ <li>
215
+ 复制链接,或生成带 <code>Referer</code>/<code>Cookie</code>
216
+ 的 <code>m3u8-cli download</code> 命令
217
+ </li>
218
+ <li>
219
+ 一键提交到 <code>m3u8-cli serve</code>
220
+ 下载,并在弹窗中查看进度、取消任务
221
+ </li>
222
+ </ul>
223
+ </div>
224
+
225
+ <div class="about-section">
226
+ <h3>使用帮助</h3>
227
+
228
+ <div class="about-step">
229
+ <h4>1. 安装 CLI</h4>
230
+ <pre><code>pip install m3u8-cli</code></pre>
231
+ </div>
232
+
233
+ <div class="about-step">
234
+ <h4>2. 启动下载服务</h4>
235
+ <pre><code>m3u8-cli serve \
236
+ --host 127.0.0.1 \
237
+ --port 8787 \
238
+ --token &lt;token&gt; \
239
+ --output-dir ./downloads</code></pre>
240
+ <p class="about-note">
241
+ 本机(回环地址)请求免 Token;其他来源需使用与
242
+ <code>--token</code> 一致的 Token。
243
+ </p>
244
+ </div>
245
+
246
+ <div class="about-step">
247
+ <h4>3. 配置扩展</h4>
248
+ <p class="about-note">
249
+ 在「设置」中确认服务地址(默认
250
+ <code>http://127.0.0.1:8787</code>),本机可留空
251
+ Token。
252
+ </p>
253
+ </div>
254
+ </div>
255
+
256
+ <div class="about-section">
257
+ <h3>项目信息</h3>
258
+ <dl class="about-info">
259
+ <dt>项目主页</dt>
260
+ <dd>
261
+ <a
262
+ href="https://pypi.org/project/m3u8-cli/"
263
+ target="_blank"
264
+ rel="noopener noreferrer"
265
+ >pypi.org/project/m3u8-cli</a
266
+ >
267
+ </dd>
268
+ <dt>作者</dt>
269
+ <dd>r1a</dd>
270
+ <dt>邮箱</dt>
271
+ <dd>
272
+ <a href="mailto:m@zcq100.com">m@zcq100.com</a>
273
+ </dd>
274
+ <dt>许可证</dt>
275
+ <dd>
276
+ <a
277
+ href="https://opensource.org/license/mit"
278
+ target="_blank"
279
+ rel="noopener noreferrer"
280
+ >MIT License</a
281
+ >
282
+ </dd>
283
+ </dl>
284
+ </div>
285
+ </section>
286
+ </main>
287
+
288
+ <div id="toast" class="toast" role="status" hidden></div>
289
+
290
+ <script src="lib.js"></script>
291
+ <script src="popup.js"></script>
292
+ </body>
293
+ </html>
@@ -23,12 +23,14 @@
23
23
  lost: "已丢失",
24
24
  };
25
25
  const ACTIVE_STATES = new Set(["queued", "running"]);
26
+ const TAB_NAMES = ["candidates", "tasks", "settings", "about"];
26
27
  const REFRESH_INTERVAL_MS = 2000;
27
28
  const SETTINGS_SAVE_DELAY_MS = 300;
28
29
  // 服务端会按输出目录长度截短文件名;重试名保持较短,避免时间戳被截掉。
29
30
  const RETRY_NAME_LIMIT = 100;
30
31
 
31
32
  const elements = {};
33
+ let activeTab = "candidates";
32
34
  let currentTabId = null;
33
35
  let currentPageUrl = "";
34
36
  let currentPageTitle = "";
@@ -44,7 +46,11 @@
44
46
  }
45
47
 
46
48
  function cacheElements() {
49
+ elements.tabList = byId("tab-list");
50
+ elements.tabs = Array.from(document.querySelectorAll(".tab"));
51
+ elements.panels = Array.from(document.querySelectorAll(".panel"));
47
52
  elements.candidateCount = byId("candidate-count");
53
+ elements.taskCount = byId("task-count");
48
54
  elements.candidateList = byId("candidate-list");
49
55
  elements.emptyState = byId("empty-state");
50
56
  elements.rescanButton = byId("rescan-button");
@@ -63,6 +69,8 @@
63
69
  elements.remoteWarning = byId("remote-warning");
64
70
  elements.testConnection = byId("test-connection");
65
71
  elements.settingsStatus = byId("settings-status");
72
+ elements.versionInfo = byId("version-info");
73
+ elements.versionText = byId("version-text");
66
74
  elements.toast = byId("toast");
67
75
  }
68
76
 
@@ -121,6 +129,56 @@
121
129
  }
122
130
  }
123
131
 
132
+ /* ------------------------------------------------------------------ */
133
+ /* 标签页切换 */
134
+ /* ------------------------------------------------------------------ */
135
+
136
+ function activateTab(name, moveFocus) {
137
+ activeTab = TAB_NAMES.includes(name) ? name : TAB_NAMES[0];
138
+ for (const tab of elements.tabs) {
139
+ const selected = tab.dataset.tab === activeTab;
140
+ tab.classList.toggle("active", selected);
141
+ tab.setAttribute("aria-selected", selected ? "true" : "false");
142
+ tab.tabIndex = selected ? 0 : -1;
143
+ if (selected && moveFocus) {
144
+ tab.focus();
145
+ }
146
+ }
147
+ for (const panel of elements.panels) {
148
+ panel.hidden = panel.id !== `panel-${activeTab}`;
149
+ }
150
+ }
151
+
152
+ function selectTab(name, moveFocus) {
153
+ activateTab(name, moveFocus);
154
+ if (activeTab === "candidates") {
155
+ loadCandidates().catch(() => {});
156
+ } else if (activeTab === "tasks") {
157
+ refreshTasks().catch(() => {});
158
+ } else if (activeTab === "settings") {
159
+ refreshVersionInfo().catch(() => {});
160
+ }
161
+ }
162
+
163
+ function handleTabKeydown(event) {
164
+ const index = TAB_NAMES.indexOf(activeTab);
165
+ let next = null;
166
+ if (event.key === "ArrowRight") {
167
+ next = (index + 1) % TAB_NAMES.length;
168
+ } else if (event.key === "ArrowLeft") {
169
+ next = (index - 1 + TAB_NAMES.length) % TAB_NAMES.length;
170
+ } else if (event.key === "Home") {
171
+ next = 0;
172
+ } else if (event.key === "End") {
173
+ next = TAB_NAMES.length - 1;
174
+ }
175
+ if (next === null) {
176
+ return;
177
+ }
178
+ event.preventDefault();
179
+ selectTab(TAB_NAMES[next], true);
180
+ }
181
+
124
182
  /* ------------------------------------------------------------------ */
125
183
  /* 候选列表 */
126
184
  /* ------------------------------------------------------------------ */
@@ -279,10 +337,7 @@
279
337
  const settingsButton = createButton(
280
338
  "打开设置",
281
339
  () => {
282
- const panel = document.querySelector(".settings");
283
- if (panel) {
284
- panel.open = true;
285
- }
340
+ selectTab("settings");
286
341
  elements.token.focus();
287
342
  },
288
343
  "ghost small",
@@ -380,6 +435,11 @@
380
435
  }
381
436
 
382
437
  function renderTasks() {
438
+ const activeCount = tasks.filter(
439
+ (task) => ACTIVE_STATES.has(task.state),
440
+ ).length;
441
+ elements.taskCount.textContent = String(activeCount);
442
+ elements.taskCount.hidden = activeCount === 0;
383
443
  elements.taskEmpty.hidden = tasks.length > 0;
384
444
  elements.taskList.replaceChildren();
385
445
  for (const task of tasks) {
@@ -465,9 +525,46 @@
465
525
  }
466
526
 
467
527
  /* ------------------------------------------------------------------ */
468
- /* 设置 */
528
+ /* 设置与版本信息 */
469
529
  /* ------------------------------------------------------------------ */
470
530
 
531
+ async function refreshVersionInfo() {
532
+ let extensionVersion = "?";
533
+ try {
534
+ const manifest = chrome.runtime.getManifest
535
+ ? chrome.runtime.getManifest()
536
+ : null;
537
+ extensionVersion = (manifest && manifest.version) || "?";
538
+ } catch (_) {
539
+ // 极少数环境拿不到 manifest,保留占位符。
540
+ }
541
+
542
+ let backend = "未连接";
543
+ let detail = "";
544
+ try {
545
+ const response = await sendMessage({ type: "server:version" });
546
+ if (response && response.ok) {
547
+ backend = response.version ? `v${response.version}` : "未知";
548
+ elements.versionInfo.classList.add("connected");
549
+ } else {
550
+ const status = Number((response && response.status) || 0);
551
+ backend = status === 404
552
+ ? "旧版"
553
+ : status === 401
554
+ ? "需 Token"
555
+ : "未连接";
556
+ detail = (response && response.error) || "";
557
+ elements.versionInfo.classList.remove("connected");
558
+ }
559
+ } catch (error) {
560
+ detail = error.message || String(error);
561
+ elements.versionInfo.classList.remove("connected");
562
+ }
563
+ elements.versionText.textContent =
564
+ `插件 v${extensionVersion} · 后端 ${backend}`;
565
+ elements.versionText.title = detail;
566
+ }
567
+
471
568
  async function loadSettings() {
472
569
  const data = await chrome.storage.local.get("settings");
473
570
  settings = { ...DEFAULT_SETTINGS, ...(data.settings || {}) };
@@ -561,6 +658,7 @@
561
658
  } finally {
562
659
  elements.testConnection.disabled = false;
563
660
  }
661
+ refreshVersionInfo().catch(() => {});
564
662
  }
565
663
 
566
664
  /* ------------------------------------------------------------------ */
@@ -568,6 +666,12 @@
568
666
  /* ------------------------------------------------------------------ */
569
667
 
570
668
  function bindEvents() {
669
+ for (const tab of elements.tabs) {
670
+ tab.addEventListener("click", () => {
671
+ selectTab(tab.dataset.tab);
672
+ });
673
+ }
674
+ elements.tabList.addEventListener("keydown", handleTabKeydown);
571
675
  elements.rescanButton.addEventListener("click", requestRescan);
572
676
  elements.clearButton.addEventListener("click", clearCurrentTab);
573
677
  elements.refreshTasks.addEventListener("click", () => {
@@ -631,6 +735,7 @@
631
735
  async function init() {
632
736
  cacheElements();
633
737
  bindEvents();
738
+ activateTab(activeTab);
634
739
 
635
740
  const [tab] = await chrome.tabs.query({
636
741
  active: true,
@@ -643,6 +748,7 @@
643
748
  }
644
749
 
645
750
  await Promise.all([loadSettings(), loadCandidates(), loadTasks()]);
751
+ refreshVersionInfo().catch(() => {});
646
752
  startTaskRefresh();
647
753
  await refreshTasks();
648
754
  }
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "m3u8-cli"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "HLS 视频下载与格式转换工具"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "m3u8-cli"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "HLS 视频下载与格式转换工具"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1,3 +1,3 @@
1
1
  """HLS 视频下载与格式转换工具。"""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.2.1"
@@ -1,6 +1,7 @@
1
1
  """RESTful 下载服务。"""
2
2
 
3
3
  import hmac
4
+ import ipaddress
4
5
  import json
5
6
  import secrets
6
7
  import threading
@@ -11,6 +12,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
11
12
  from pathlib import Path
12
13
  from urllib.parse import parse_qs, urlsplit
13
14
 
15
+ from . import __version__
14
16
  from .download import DownloadOptions, download_hls
15
17
  from .errors import DownloadCancelled, HLSException
16
18
  from .paths import fit_output_name
@@ -470,8 +472,22 @@ def parse_task_request(payload, default_jobs=8):
470
472
  }
471
473
 
472
474
 
473
- def make_handler(manager, token):
474
- """创建绑定到指定 TaskManager 与 token 的请求处理器。"""
475
+ def is_loopback_address(address):
476
+ """判断连接来源是否为本机回环地址(兼容 IPv4-mapped IPv6)。"""
477
+ try:
478
+ ip = ipaddress.ip_address(str(address).split("%", 1)[0])
479
+ except ValueError:
480
+ return False
481
+ if isinstance(ip, ipaddress.IPv6Address) and ip.ipv4_mapped:
482
+ ip = ip.ipv4_mapped
483
+ return ip.is_loopback
484
+
485
+
486
+ def make_handler(manager, token, allow_local=True):
487
+ """创建绑定到指定 TaskManager 与 token 的请求处理器。
488
+
489
+ allow_local 为 True 时,来源为回环地址的请求跳过 token 校验。
490
+ """
475
491
  if not token:
476
492
  raise ValueError("token 不能为空")
477
493
  valid_states = {
@@ -525,11 +541,15 @@ def make_handler(manager, token):
525
541
  return False
526
542
 
527
543
  def _require_auth(self):
528
- if not self._authorized():
529
- self.close_connection = True
530
- self._send_json(401, {"error": "未授权"})
531
- return False
532
- return True
544
+ if allow_local and is_loopback_address(
545
+ self.client_address[0]
546
+ ):
547
+ return True
548
+ if self._authorized():
549
+ return True
550
+ self.close_connection = True
551
+ self._send_json(401, {"error": "未授权"})
552
+ return False
533
553
 
534
554
  def _path_parts(self):
535
555
  return urlsplit(self.path).path.rstrip("/").split("/")
@@ -565,6 +585,13 @@ def make_handler(manager, token):
565
585
  def _handle_get(self):
566
586
  parts = self._path_parts()
567
587
 
588
+ if parts == ["", "api", "version"]:
589
+ self._send_json(200, {
590
+ "name": "m3u8-cli",
591
+ "version": __version__,
592
+ })
593
+ return
594
+
568
595
  if parts == ["", "api", "tasks"]:
569
596
  query = parse_qs(urlsplit(self.path).query)
570
597
  state = query.get("state", [None])[0]
@@ -725,6 +752,10 @@ def run_serve(args):
725
752
  )
726
753
  print(f"输出目录:{output_dir}", flush=True)
727
754
  print(f"API token:{token}", flush=True)
755
+ print(
756
+ "本机(回环地址)请求免 token;其他来源需要 Bearer token",
757
+ flush=True,
758
+ )
728
759
 
729
760
  try:
730
761
  server.serve_forever()
@@ -1,112 +0,0 @@
1
- <!doctype html>
2
- <html lang="zh-CN">
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>m3u8-cli 链接检测器</title>
7
- <link rel="stylesheet" href="popup.css" />
8
- </head>
9
- <body>
10
- <header class="app-header">
11
- <div class="title-row">
12
- <span class="logo" aria-hidden="true">▶</span>
13
- <h1>m3u8 检测器</h1>
14
- <span id="candidate-count" class="count">0</span>
15
- </div>
16
- <div class="header-actions">
17
- <button id="rescan-button" type="button" class="ghost">重新扫描</button>
18
- <button id="clear-button" type="button" class="ghost">清空</button>
19
- </div>
20
- </header>
21
-
22
- <main>
23
- <section class="section">
24
- <div id="empty-state" class="empty">
25
- 未检测到 m3u8 链接。<br />
26
- 播放视频或刷新页面后再试。
27
- </div>
28
- <ul id="candidate-list" class="candidate-list"></ul>
29
- </section>
30
-
31
- <section class="section">
32
- <div class="section-title-row">
33
- <h2>下载任务</h2>
34
- <button id="refresh-tasks" type="button" class="ghost small">
35
- 刷新
36
- </button>
37
- </div>
38
- <div id="task-empty" class="empty small-empty">暂无任务</div>
39
- <ul id="task-list" class="task-list"></ul>
40
- </section>
41
-
42
- <details class="settings">
43
- <summary>设置</summary>
44
- <div class="settings-grid">
45
- <label>
46
- <span>服务地址</span>
47
- <input
48
- id="server-url"
49
- type="text"
50
- spellcheck="false"
51
- placeholder="http://127.0.0.1:8787"
52
- />
53
- </label>
54
- <label>
55
- <span>Token</span>
56
- <input
57
- id="token"
58
- type="password"
59
- autocomplete="off"
60
- spellcheck="false"
61
- placeholder="m3u8-cli serve --token ..."
62
- />
63
- </label>
64
- <label>
65
- <span>下载格式</span>
66
- <select id="format">
67
- <option value="mp4">MP4(合并封装)</option>
68
- <option value="hls">HLS(保留分片)</option>
69
- </select>
70
- </label>
71
- <label>
72
- <span>文件名</span>
73
- <select id="naming">
74
- <option value="title">页面标题</option>
75
- <option value="hash">URL 短 hash</option>
76
- </select>
77
- </label>
78
- <label class="checkbox">
79
- <input id="overwrite" type="checkbox" />
80
- <span>覆盖已存在的文件</span>
81
- </label>
82
- <label class="checkbox">
83
- <input id="send-cookie" type="checkbox" />
84
- <span>发送捕获到的 Cookie(用于下载)</span>
85
- </label>
86
- <label class="checkbox">
87
- <input id="copy-cookie" type="checkbox" />
88
- <span>复制 CLI 命令时包含 Cookie(敏感)</span>
89
- </label>
90
- <label class="checkbox">
91
- <input id="insecure" type="checkbox" />
92
- <span>跳过 TLS 证书校验(不安全,仅用于自签名/内网服务)</span>
93
- </label>
94
- <div id="remote-warning" class="warning" hidden>
95
- 服务地址不是本机,Cookie 会被发送到该地址,请确认其可信。
96
- </div>
97
- <div class="settings-actions">
98
- <button id="test-connection" type="button" class="primary">
99
- 测试连接
100
- </button>
101
- <span id="settings-status" class="status"></span>
102
- </div>
103
- </div>
104
- </details>
105
- </main>
106
-
107
- <div id="toast" class="toast" role="status" hidden></div>
108
-
109
- <script src="lib.js"></script>
110
- <script src="popup.js"></script>
111
- </body>
112
- </html>
File without changes
File without changes
File without changes
File without changes