thunder-subtitle-srt 1.0.0__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 (47) hide show
  1. thunder_subtitle_srt-1.0.0/PKG-INFO +182 -0
  2. thunder_subtitle_srt-1.0.0/README.md +169 -0
  3. thunder_subtitle_srt-1.0.0/cli.py +156 -0
  4. thunder_subtitle_srt-1.0.0/commands/__init__.py +1 -0
  5. thunder_subtitle_srt-1.0.0/commands/config.py +33 -0
  6. thunder_subtitle_srt-1.0.0/commands/download.py +34 -0
  7. thunder_subtitle_srt-1.0.0/commands/dump.py +102 -0
  8. thunder_subtitle_srt-1.0.0/commands/review.py +42 -0
  9. thunder_subtitle_srt-1.0.0/commands/scan.py +41 -0
  10. thunder_subtitle_srt-1.0.0/commands/search.py +153 -0
  11. thunder_subtitle_srt-1.0.0/pyproject.toml +43 -0
  12. thunder_subtitle_srt-1.0.0/setup.cfg +4 -0
  13. thunder_subtitle_srt-1.0.0/src/__init__.py +0 -0
  14. thunder_subtitle_srt-1.0.0/src/api.py +129 -0
  15. thunder_subtitle_srt-1.0.0/src/config.py +72 -0
  16. thunder_subtitle_srt-1.0.0/src/download.py +217 -0
  17. thunder_subtitle_srt-1.0.0/src/exceptions.py +33 -0
  18. thunder_subtitle_srt-1.0.0/src/models.py +74 -0
  19. thunder_subtitle_srt-1.0.0/src/py.typed +0 -0
  20. thunder_subtitle_srt-1.0.0/src/reviewer/__init__.py +143 -0
  21. thunder_subtitle_srt-1.0.0/src/reviewer/_encoding.py +32 -0
  22. thunder_subtitle_srt-1.0.0/src/reviewer/_marker.py +111 -0
  23. thunder_subtitle_srt-1.0.0/src/reviewer/_output.py +99 -0
  24. thunder_subtitle_srt-1.0.0/src/reviewer/_review.py +139 -0
  25. thunder_subtitle_srt-1.0.0/src/reviewer/_srt.py +109 -0
  26. thunder_subtitle_srt-1.0.0/src/scanner/__init__.py +7 -0
  27. thunder_subtitle_srt-1.0.0/src/scanner/_dir.py +44 -0
  28. thunder_subtitle_srt-1.0.0/src/scanner/_io.py +96 -0
  29. thunder_subtitle_srt-1.0.0/src/scanner/_parallel.py +205 -0
  30. thunder_subtitle_srt-1.0.0/src/scanner/_processor.py +259 -0
  31. thunder_subtitle_srt-1.0.0/src/scanner/_skip.py +193 -0
  32. thunder_subtitle_srt-1.0.0/src/ui.py +70 -0
  33. thunder_subtitle_srt-1.0.0/src/utils.py +181 -0
  34. thunder_subtitle_srt-1.0.0/tests/test_api.py +275 -0
  35. thunder_subtitle_srt-1.0.0/tests/test_cli.py +154 -0
  36. thunder_subtitle_srt-1.0.0/tests/test_config.py +107 -0
  37. thunder_subtitle_srt-1.0.0/tests/test_download.py +241 -0
  38. thunder_subtitle_srt-1.0.0/tests/test_integration.py +114 -0
  39. thunder_subtitle_srt-1.0.0/tests/test_reviewer.py +185 -0
  40. thunder_subtitle_srt-1.0.0/tests/test_scanner_helpers.py +436 -0
  41. thunder_subtitle_srt-1.0.0/tests/test_utils.py +169 -0
  42. thunder_subtitle_srt-1.0.0/thunder_subtitle_srt.egg-info/PKG-INFO +182 -0
  43. thunder_subtitle_srt-1.0.0/thunder_subtitle_srt.egg-info/SOURCES.txt +45 -0
  44. thunder_subtitle_srt-1.0.0/thunder_subtitle_srt.egg-info/dependency_links.txt +1 -0
  45. thunder_subtitle_srt-1.0.0/thunder_subtitle_srt.egg-info/entry_points.txt +2 -0
  46. thunder_subtitle_srt-1.0.0/thunder_subtitle_srt.egg-info/requires.txt +7 -0
  47. thunder_subtitle_srt-1.0.0/thunder_subtitle_srt.egg-info/top_level.txt +3 -0
@@ -0,0 +1,182 @@
1
+ Metadata-Version: 2.4
2
+ Name: thunder-subtitle-srt
3
+ Version: 1.0.0
4
+ Summary: CLI tool for searching and downloading Chinese subtitles via Xunlei API
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: requests>=2.31.0
8
+ Provides-Extra: dev
9
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
10
+ Requires-Dist: mypy>=1.0; extra == "dev"
11
+ Requires-Dist: types-requests>=2.31; extra == "dev"
12
+ Requires-Dist: ruff; extra == "dev"
13
+
14
+ # Thunder Subtitle
15
+
16
+ 迅雷字幕 CLI 工具 — 搜索、下载中文字幕,支持 Jellyfin 媒体库自动扫描。
17
+
18
+ ## 安装
19
+
20
+ ```bash
21
+ pip install thunder-subtitle
22
+ ```
23
+
24
+ 或开发模式安装:
25
+
26
+ ```bash
27
+ git clone <repo>
28
+ cd thunder-subtitle-py
29
+ pip install -e ".[dev]"
30
+ ```
31
+
32
+ ## 使用方法
33
+
34
+ ### 搜索字幕
35
+
36
+ ```bash
37
+ # 基本搜索
38
+ thunder-subtitle search "Movie Name"
39
+
40
+ # 仅中文字幕
41
+ thunder-subtitle search "Movie Name" --chinese-only
42
+
43
+ # 按视频时长筛选 (过滤时长不匹配的字幕)
44
+ thunder-subtitle search "Movie Name" --max-duration 1h30m
45
+
46
+ # 下载全部结果
47
+ thunder-subtitle search "Movie Name" --all -o ./subs/
48
+
49
+ # 按序号下载
50
+ thunder-subtitle search "Movie Name" --index 1,3,5
51
+ ```
52
+
53
+ ### 全量下载(dump)
54
+
55
+ 下载所有匹配的中文字幕,自动跳过之前已拒绝的字幕(基于 gcid 记录在 `.rejected` 文件中)。
56
+
57
+ ```bash
58
+ # 直接搜索下载
59
+ thunder-subtitle dump "Movie Name"
60
+
61
+ # 从目录读取 movie.nfo 自动获取时长
62
+ thunder-subtitle dump --dir /path/to/movie
63
+
64
+ # 清空 .rejected 文件以重新下载所有字幕
65
+ rm /path/to/movie/.rejected
66
+ ```
67
+
68
+ > 注意:已拒绝的字幕(gcid 或 url hash 记录在 `.rejected`)在后续下载中自动跳过。手动删除 `.rejected` 文件可清空拒绝列表。scan 模式下的 `--reset-fail` 会自动清空 `.rejected` 和审查失败标记,实现完全暴力刷新。
69
+
70
+ ### Jellyfin 目录扫描
71
+
72
+ 扫描演员/电影目录结构,自动搜索并下载缺失的字幕。支持断点续扫、并行下载。
73
+
74
+ ```bash
75
+ # 基础扫描
76
+ thunder-subtitle scan /path/to/jellyfin/media
77
+
78
+ # 预览模式(不实际下载)
79
+ thunder-subtitle scan /path/to/jellyfin/media --dry-run
80
+
81
+ # 并行处理(4线程)
82
+ thunder-subtitle scan /path/to/jellyfin/media --parallel 4
83
+
84
+ # 过滤特定电影
85
+ thunder-subtitle scan /path/to/jellyfin/media --filter "电影名"
86
+
87
+ # 断点续扫
88
+ thunder-subtitle scan /path/to/jellyfin/media --resume
89
+
90
+ # 仅处理 N 天前发布的电影
91
+ thunder-subtitle scan /path/to/jellyfin/media --min-age 7
92
+
93
+ # 全量下载模式(每部电影下载所有匹配字幕)
94
+ thunder-subtitle scan /path/to/jellyfin/media --dump
95
+
96
+ # 强制重试标记失败的电影(但跳过已拒绝的字幕)
97
+ thunder-subtitle scan /path/to/jellyfin/media --dump --force
98
+
99
+ # 暴力刷新:清除标记失败状态和所有拒绝记录,重新下载
100
+ thunder-subtitle scan /path/to/jellyfin/media --dump --force --reset-fail
101
+ ```
102
+
103
+ ### 字幕审查
104
+
105
+ 审查已下载字幕质量,给出百分制评分。
106
+
107
+ ```bash
108
+ # 审查目录
109
+ thunder-subtitle review /path/to/jellyfin/media
110
+
111
+ # 标记审查状态
112
+ thunder-subtitle review --mark "电影名" # 标记通过
113
+ thunder-subtitle review --mark-fail "电影名" # 标记失败(不再尝试下载)
114
+ thunder-subtitle review --unmark "电影名" # 取消标记
115
+ thunder-subtitle review --mark-all # 全部标记
116
+ ```
117
+
118
+ ### 配置管理
119
+
120
+ ```bash
121
+ # 查看配置
122
+ thunder-subtitle config
123
+
124
+ # 设置配置项
125
+ thunder-subtitle config --set media_paths /path1,/path2
126
+ thunder-subtitle config --set rate_limit 5
127
+ thunder-subtitle config --set preferred_groups "KitaujiSub,DMG"
128
+
129
+ # 重置为默认
130
+ thunder-subtitle config --reset
131
+ ```
132
+
133
+ 配置项:
134
+
135
+ | 键 | 类型 | 默认值 | 说明 |
136
+ |---|---|---|---|
137
+ | `output_dir` | str | `""` | 默认下载目录 |
138
+ | `timeout` | int | `30` | API 超时(秒) |
139
+ | `rate_limit` | int | `3` | 扫描模式查询间隔(秒) |
140
+ | `retry_count` | int | `3` | 下载失败重试次数 |
141
+ | `retry_delay` | int | `2` | 重试基础间隔(秒,指数退避) |
142
+ | `preferred_groups` | str | `""` | 偏好字幕组(逗号分隔) |
143
+ | `media_paths` | str | `""` | 默认媒体库路径(逗号分隔) |
144
+
145
+ ### 直接下载
146
+
147
+ ```bash
148
+ thunder-subtitle download "https://..." "filename.srt"
149
+ ```
150
+
151
+ ## Jellyfin 目录结构
152
+
153
+ ```
154
+ 媒体库/
155
+ ├── 演员A/
156
+ │ ├── 电影1/
157
+ │ │ ├── movie.nfo
158
+ │ │ └── 视频文件
159
+ │ └── 电影2/
160
+ │ └── movie.nfo
161
+ └── 演员B/
162
+ └── 电影3/
163
+ └── movie.nfo
164
+ ```
165
+
166
+ ## 开发
167
+
168
+ ```bash
169
+ pip install -e ".[dev]" # 安装开发依赖
170
+ pytest tests/ -v # 运行测试(196 用例)
171
+ ruff check . # 代码检查
172
+ ```
173
+
174
+ ## CI/CD
175
+
176
+ - **CI**:push/PR 时自动运行 ruff lint + 编译检查 + pytest
177
+ - **发布**:推送 `v*` tag 自动构建 wheel 并发布到 PyPI
178
+
179
+ ## 要求
180
+
181
+ - Python >= 3.10
182
+ - `requests`
@@ -0,0 +1,169 @@
1
+ # Thunder Subtitle
2
+
3
+ 迅雷字幕 CLI 工具 — 搜索、下载中文字幕,支持 Jellyfin 媒体库自动扫描。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pip install thunder-subtitle
9
+ ```
10
+
11
+ 或开发模式安装:
12
+
13
+ ```bash
14
+ git clone <repo>
15
+ cd thunder-subtitle-py
16
+ pip install -e ".[dev]"
17
+ ```
18
+
19
+ ## 使用方法
20
+
21
+ ### 搜索字幕
22
+
23
+ ```bash
24
+ # 基本搜索
25
+ thunder-subtitle search "Movie Name"
26
+
27
+ # 仅中文字幕
28
+ thunder-subtitle search "Movie Name" --chinese-only
29
+
30
+ # 按视频时长筛选 (过滤时长不匹配的字幕)
31
+ thunder-subtitle search "Movie Name" --max-duration 1h30m
32
+
33
+ # 下载全部结果
34
+ thunder-subtitle search "Movie Name" --all -o ./subs/
35
+
36
+ # 按序号下载
37
+ thunder-subtitle search "Movie Name" --index 1,3,5
38
+ ```
39
+
40
+ ### 全量下载(dump)
41
+
42
+ 下载所有匹配的中文字幕,自动跳过之前已拒绝的字幕(基于 gcid 记录在 `.rejected` 文件中)。
43
+
44
+ ```bash
45
+ # 直接搜索下载
46
+ thunder-subtitle dump "Movie Name"
47
+
48
+ # 从目录读取 movie.nfo 自动获取时长
49
+ thunder-subtitle dump --dir /path/to/movie
50
+
51
+ # 清空 .rejected 文件以重新下载所有字幕
52
+ rm /path/to/movie/.rejected
53
+ ```
54
+
55
+ > 注意:已拒绝的字幕(gcid 或 url hash 记录在 `.rejected`)在后续下载中自动跳过。手动删除 `.rejected` 文件可清空拒绝列表。scan 模式下的 `--reset-fail` 会自动清空 `.rejected` 和审查失败标记,实现完全暴力刷新。
56
+
57
+ ### Jellyfin 目录扫描
58
+
59
+ 扫描演员/电影目录结构,自动搜索并下载缺失的字幕。支持断点续扫、并行下载。
60
+
61
+ ```bash
62
+ # 基础扫描
63
+ thunder-subtitle scan /path/to/jellyfin/media
64
+
65
+ # 预览模式(不实际下载)
66
+ thunder-subtitle scan /path/to/jellyfin/media --dry-run
67
+
68
+ # 并行处理(4线程)
69
+ thunder-subtitle scan /path/to/jellyfin/media --parallel 4
70
+
71
+ # 过滤特定电影
72
+ thunder-subtitle scan /path/to/jellyfin/media --filter "电影名"
73
+
74
+ # 断点续扫
75
+ thunder-subtitle scan /path/to/jellyfin/media --resume
76
+
77
+ # 仅处理 N 天前发布的电影
78
+ thunder-subtitle scan /path/to/jellyfin/media --min-age 7
79
+
80
+ # 全量下载模式(每部电影下载所有匹配字幕)
81
+ thunder-subtitle scan /path/to/jellyfin/media --dump
82
+
83
+ # 强制重试标记失败的电影(但跳过已拒绝的字幕)
84
+ thunder-subtitle scan /path/to/jellyfin/media --dump --force
85
+
86
+ # 暴力刷新:清除标记失败状态和所有拒绝记录,重新下载
87
+ thunder-subtitle scan /path/to/jellyfin/media --dump --force --reset-fail
88
+ ```
89
+
90
+ ### 字幕审查
91
+
92
+ 审查已下载字幕质量,给出百分制评分。
93
+
94
+ ```bash
95
+ # 审查目录
96
+ thunder-subtitle review /path/to/jellyfin/media
97
+
98
+ # 标记审查状态
99
+ thunder-subtitle review --mark "电影名" # 标记通过
100
+ thunder-subtitle review --mark-fail "电影名" # 标记失败(不再尝试下载)
101
+ thunder-subtitle review --unmark "电影名" # 取消标记
102
+ thunder-subtitle review --mark-all # 全部标记
103
+ ```
104
+
105
+ ### 配置管理
106
+
107
+ ```bash
108
+ # 查看配置
109
+ thunder-subtitle config
110
+
111
+ # 设置配置项
112
+ thunder-subtitle config --set media_paths /path1,/path2
113
+ thunder-subtitle config --set rate_limit 5
114
+ thunder-subtitle config --set preferred_groups "KitaujiSub,DMG"
115
+
116
+ # 重置为默认
117
+ thunder-subtitle config --reset
118
+ ```
119
+
120
+ 配置项:
121
+
122
+ | 键 | 类型 | 默认值 | 说明 |
123
+ |---|---|---|---|
124
+ | `output_dir` | str | `""` | 默认下载目录 |
125
+ | `timeout` | int | `30` | API 超时(秒) |
126
+ | `rate_limit` | int | `3` | 扫描模式查询间隔(秒) |
127
+ | `retry_count` | int | `3` | 下载失败重试次数 |
128
+ | `retry_delay` | int | `2` | 重试基础间隔(秒,指数退避) |
129
+ | `preferred_groups` | str | `""` | 偏好字幕组(逗号分隔) |
130
+ | `media_paths` | str | `""` | 默认媒体库路径(逗号分隔) |
131
+
132
+ ### 直接下载
133
+
134
+ ```bash
135
+ thunder-subtitle download "https://..." "filename.srt"
136
+ ```
137
+
138
+ ## Jellyfin 目录结构
139
+
140
+ ```
141
+ 媒体库/
142
+ ├── 演员A/
143
+ │ ├── 电影1/
144
+ │ │ ├── movie.nfo
145
+ │ │ └── 视频文件
146
+ │ └── 电影2/
147
+ │ └── movie.nfo
148
+ └── 演员B/
149
+ └── 电影3/
150
+ └── movie.nfo
151
+ ```
152
+
153
+ ## 开发
154
+
155
+ ```bash
156
+ pip install -e ".[dev]" # 安装开发依赖
157
+ pytest tests/ -v # 运行测试(196 用例)
158
+ ruff check . # 代码检查
159
+ ```
160
+
161
+ ## CI/CD
162
+
163
+ - **CI**:push/PR 时自动运行 ruff lint + 编译检查 + pytest
164
+ - **发布**:推送 `v*` tag 自动构建 wheel 并发布到 PyPI
165
+
166
+ ## 要求
167
+
168
+ - Python >= 3.10
169
+ - `requests`
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Thunder Subtitle Python CLI - Entry Point
4
+
5
+ 字幕搜索下载工具,纯命令行参数方式
6
+ """
7
+
8
+ import argparse
9
+ from typing import Callable
10
+
11
+ from commands.search import cmd_search
12
+ from commands.download import cmd_download
13
+ from commands.config import cmd_config
14
+ from commands.dump import cmd_dump
15
+ from commands.review import cmd_review
16
+ from commands.scan import cmd_scan
17
+
18
+
19
+ # 命令注册表:dispatch 时直接查表,无需 if/elif 链
20
+ _COMMANDS: dict[str, Callable] = {
21
+ "search": cmd_search,
22
+ "download": cmd_download,
23
+ "scan": cmd_scan,
24
+ "review": cmd_review,
25
+ "dump": cmd_dump,
26
+ "config": cmd_config,
27
+ }
28
+
29
+
30
+ def _get_version() -> str:
31
+ """从已安装包的 metadata 读取版本号"""
32
+ from importlib.metadata import PackageNotFoundError, version
33
+ try:
34
+ return version("thunder-subtitle")
35
+ except PackageNotFoundError:
36
+ return "dev"
37
+
38
+
39
+ def _build_parser() -> argparse.ArgumentParser:
40
+ """构建命令行参数解析器"""
41
+ parser = argparse.ArgumentParser(
42
+ prog="thunder-subtitle",
43
+ description="CLI tool for searching and downloading Chinese subtitles via Xunlei API",
44
+ )
45
+ parser.add_argument("--version", action="version", version=f"thunder-subtitle {_get_version()}")
46
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
47
+
48
+ # ===== search 命令 =====
49
+ p = subparsers.add_parser("search", help="Search for subtitles by name")
50
+ p.add_argument("name", help="Search keyword for subtitles")
51
+ p.add_argument("-c", "--chinese-only", action="store_true", default=False,
52
+ help="Filter to Chinese subtitles only")
53
+ p.add_argument("-d", "--max-duration", type=str, default=None,
54
+ help="Filter by max video duration (e.g., 1h30m, 90m, 45s)")
55
+ p.add_argument("-f", "--chinese-first", action="store_true", default=False,
56
+ help="Prioritize Chinese subtitles, fallback to others if none found")
57
+ p.add_argument("-o", "--output", type=str, default=None,
58
+ help="Output directory for downloads")
59
+ p.add_argument("-i", "--index", type=str, default=None,
60
+ help="Download specific subtitle(s) by index (e.g., 1 or 1,3,5 or 1-3)")
61
+ p.add_argument("-a", "--all", action="store_true", default=False,
62
+ help="Download all search results")
63
+ p.add_argument("--limit", type=int, default=None,
64
+ help="Limit the number of results shown")
65
+
66
+ # ===== download 命令 =====
67
+ p = subparsers.add_parser("download", help="Download subtitle by URL")
68
+ p.add_argument("url", help="Subtitle download URL")
69
+ p.add_argument("filename", nargs="?", default=None, help="Output filename")
70
+ p.add_argument("-o", "--output", type=str, default=None,
71
+ help="Output directory for downloads")
72
+
73
+ # ===== config 命令 =====
74
+ p = subparsers.add_parser("config", help="View or update configuration")
75
+ p.add_argument("--set", nargs=2, metavar=("KEY", "VALUE"), dest="set_pair",
76
+ help="Set a config value (e.g., --set rate_limit 5)")
77
+ p.add_argument("--reset", action="store_true", default=False,
78
+ help="Reset config to defaults")
79
+
80
+ # ===== dump 命令 =====
81
+ p = subparsers.add_parser("dump", help="Download ALL subtitles for a movie, numbered 1.srt, 2.srt, ...")
82
+ p.add_argument("name", nargs="?", default=None, help="Movie name to search")
83
+ p.add_argument("--dir", type=str, default=None,
84
+ help="Movie directory (auto-reads name from basename, duration from movie.nfo)")
85
+ p.add_argument("-o", "--output", type=str, default=None,
86
+ help="Output directory (default: current dir, or movie dir if --dir)")
87
+ p.add_argument("-d", "--max-duration", type=str, default=None,
88
+ help="Filter by max video duration (e.g., 1h30m)")
89
+ p.add_argument("-c", "--chinese-only", action="store_true", default=False,
90
+ help="Only Chinese subtitles")
91
+ p.add_argument("-f", "--chinese-first", action="store_true", default=False,
92
+ help="Prioritize Chinese subtitles first")
93
+
94
+ # ===== review 命令 =====
95
+ p = subparsers.add_parser("review", help="Review downloaded subtitle files for quality issues")
96
+ p.add_argument("directory", nargs="?", default=None,
97
+ help="Base directory to review (uses media_paths if omitted)")
98
+ p.add_argument("--filter", type=str, action="append", default=None, dest="filters",
99
+ help="Only review movies matching this keyword (can repeat)")
100
+ p.add_argument("--log", action="store_true", default=False,
101
+ help="Save review report to the scan directory")
102
+ p.add_argument("--mark", type=str, default=None,
103
+ help="Mark matching movies as reviewed")
104
+ p.add_argument("--unmark", type=str, default=None,
105
+ help="Remove review mark from matching movies")
106
+ p.add_argument("--mark-all", action="store_true", default=False,
107
+ help="Mark all movies as reviewed")
108
+ p.add_argument("--mark-path", type=str, default=None,
109
+ help="Mark a specific movie directory as reviewed")
110
+ p.add_argument("--unmark-path", type=str, default=None,
111
+ help="Remove review mark from a specific movie directory")
112
+ p.add_argument("--mark-fail", type=str, default=None,
113
+ help="Mark matching movies as review FAILED (all subs unusable)")
114
+ p.add_argument("--mark-fail-path", type=str, default=None,
115
+ help="Mark specific movie dir as review FAILED (relative/absolute)")
116
+
117
+ # ===== scan 命令 =====
118
+ p = subparsers.add_parser("scan", help="Scan Jellyfin movie directories and auto-download subtitles")
119
+ p.add_argument("directory", nargs="?", default=None,
120
+ help="Base directory to scan (演员/电影 structure, uses media_paths if omitted)")
121
+ p.add_argument("--dry-run", action="store_true", default=False,
122
+ help="Show what would be done without downloading")
123
+ p.add_argument("--filter", type=str, action="append", default=None, dest="filters",
124
+ help="Only process movies matching this keyword (can repeat)")
125
+ p.add_argument("--resume", action="store_true", default=False,
126
+ help="Resume from last interruption, skip already-processed movies")
127
+ p.add_argument("--log", action="store_true", default=False,
128
+ help="Save scan log to the scan directory")
129
+ p.add_argument("--min-age", type=int, default=0,
130
+ help="Only process movies released N+ days ago (default 0 = immediate)")
131
+ p.add_argument("--dump", action="store_true", default=False, dest="dump",
132
+ help="Brute-force: download ALL subtitles per movie (1.srt, 2.srt...)")
133
+ p.add_argument("--force", action="store_true", default=False,
134
+ help="Force re-download even for mark-fail movies (keeps fail state)")
135
+ p.add_argument("--reset-fail", action="store_true", default=False,
136
+ help="Clear mark-fail status, reset to need-review")
137
+ p.add_argument("-p", "--parallel", type=int, default=1,
138
+ help="Number of parallel workers (default 1 = serial)")
139
+
140
+ return parser
141
+
142
+
143
+ def main() -> None:
144
+ """CLI 入口:构建 parser → 解析参数 → 分发命令"""
145
+ parser = _build_parser()
146
+ args = parser.parse_args()
147
+
148
+ handler = _COMMANDS.get(args.command)
149
+ if handler:
150
+ handler(args)
151
+ else:
152
+ parser.print_help()
153
+
154
+
155
+ if __name__ == "__main__":
156
+ main()
@@ -0,0 +1 @@
1
+ """commands/__init__.py"""
@@ -0,0 +1,33 @@
1
+ """config 命令:配置管理"""
2
+
3
+ from src.config import Config
4
+ from src.ui import DIM, GREEN, RED, RESET
5
+
6
+
7
+ def cmd_config(args) -> None:
8
+ """配置管理"""
9
+ config = Config.load()
10
+
11
+ if args.reset:
12
+ config = Config()
13
+ config.save()
14
+ print(f"{GREEN}\n ✓ Config reset to defaults{RESET}\n")
15
+ return
16
+
17
+ if args.set_pair:
18
+ key, value = args.set_pair[0], args.set_pair[1]
19
+ if not hasattr(config, key):
20
+ valid = ", ".join(Config.__dataclass_fields__.keys())
21
+ print(f"{RED}\n ✗ Unknown key: {key}{RESET}")
22
+ print(f"{DIM} Valid keys: {valid}{RESET}\n")
23
+ return
24
+ current = getattr(config, key)
25
+ if isinstance(current, int):
26
+ setattr(config, key, int(value))
27
+ else:
28
+ setattr(config, key, value)
29
+ config.save()
30
+ print(f"{GREEN}\n ✓ {key} = {getattr(config, key)}{RESET}\n")
31
+ return
32
+
33
+ config.show()
@@ -0,0 +1,34 @@
1
+ """download 命令:通过 URL 下载字幕"""
2
+
3
+ from src.exceptions import CLIExit
4
+ from src.models import Subtitle
5
+ from src.ui import display_error, display_success
6
+ from src.download import download_subtitle, get_default_download_dir
7
+
8
+
9
+ def cmd_download(args) -> None:
10
+ """执行下载命令"""
11
+ output_dir = args.output or get_default_download_dir()
12
+
13
+ # 构造一个简单的 Subtitle 对象
14
+ subtitle = Subtitle(
15
+ gcid="",
16
+ cid="",
17
+ url=args.url,
18
+ ext=args.filename.split(".")[-1] if args.filename and "." in args.filename else "srt",
19
+ name=args.filename or "subtitle",
20
+ duration=0,
21
+ languages=[],
22
+ source=0,
23
+ score=0.0,
24
+ fingerprintf_score=0.0,
25
+ extra_name="",
26
+ mt=0,
27
+ )
28
+
29
+ result = download_subtitle(subtitle, output_dir, custom_filename=args.filename)
30
+ if result.success:
31
+ display_success(f"Downloaded: {result.filename}")
32
+ else:
33
+ display_error(f"Download failed: {result.error}")
34
+ raise CLIExit()
@@ -0,0 +1,102 @@
1
+ """dump 命令:全量下载字幕"""
2
+
3
+ import logging
4
+ import os
5
+ import xml.etree.ElementTree as ET
6
+
7
+ from src.api import SubtitleApiClient
8
+ from src.config import Config
9
+ from src.exceptions import CLIExit, ThunderSubtitleError
10
+ from src.ui import BOLD, DIM, GREEN, RED, RESET, display_error
11
+ from src.download import dump_subtitles, get_default_download_dir
12
+ from src.utils import parse_duration, parse_nfo, seconds_to_duration_str, filter_by_duration, load_gcid_file, clear_file
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def cmd_dump(args) -> None:
18
+ """全量下载字幕:搜索后下载全部匹配结果,按 1.{ext}, 2.{ext} 命名"""
19
+ client = SubtitleApiClient()
20
+
21
+ # --dir 模式:从目录读取电影名和时长
22
+ if args.dir:
23
+ if not os.path.isdir(args.dir):
24
+ display_error(f"Directory not found: {args.dir}")
25
+ raise CLIExit()
26
+ movie_name = os.path.basename(args.dir.rstrip("/"))
27
+ output_dir = args.output if args.output is not None else args.dir
28
+ # 读取 movie.nfo 获取时长
29
+ nfo_path = os.path.join(args.dir, "movie.nfo")
30
+ try:
31
+ nfo = parse_nfo(nfo_path)
32
+ max_duration_ms = nfo.duration_seconds * 1000 if nfo.duration_seconds > 0 else None
33
+ duration_str = seconds_to_duration_str(nfo.duration_seconds)
34
+ except (ET.ParseError, OSError):
35
+ max_duration_ms = None
36
+ duration_str = "unknown"
37
+ else:
38
+ if not args.name:
39
+ display_error("Either movie name or --dir is required")
40
+ raise CLIExit()
41
+ movie_name = args.name
42
+ output_dir = args.output if args.output is not None else "."
43
+ duration_str = args.max_duration or ""
44
+ max_duration_ms = None
45
+ if args.max_duration:
46
+ try:
47
+ max_duration_ms = parse_duration(args.max_duration)
48
+ except ValueError as e:
49
+ display_error(str(e))
50
+ raise CLIExit()
51
+
52
+ print(f"{BOLD}\n Dumping all subtitles for: \"{movie_name}\"{RESET}")
53
+ if max_duration_ms:
54
+ print(f"{DIM} Max video duration: {duration_str} (from NFO){RESET}")
55
+ if max_duration_ms and duration_str:
56
+ print(f"{DIM} Filtering: Max video duration {duration_str}{RESET}")
57
+ print(f"{DIM} Output: {os.path.abspath(output_dir)}{RESET}\n")
58
+
59
+ try:
60
+ result = client.search_subtitles(movie_name)
61
+ if result.total == 0:
62
+ display_error("No subtitles found.")
63
+ raise CLIExit()
64
+
65
+ subtitles = result.subtitles
66
+
67
+ # 中文筛选
68
+ if args.chinese_only:
69
+ subtitles = client.filter_chinese_subtitles(subtitles)
70
+ elif args.chinese_first:
71
+ subtitles.sort(key=lambda s: 0 if client.is_chinese_subtitle(s) else 1)
72
+
73
+ # 时长筛选
74
+ if max_duration_ms is not None:
75
+ subtitles = filter_by_duration(subtitles, max_duration_ms, client.filter_by_max_duration)
76
+
77
+ if not subtitles:
78
+ display_error("No subtitles match the filters.")
79
+ raise CLIExit()
80
+
81
+ print(f"{GREEN} Found {len(subtitles)} subtitle(s){RESET}\n")
82
+
83
+ # 加载已拒绝 gcid
84
+ rejected = load_gcid_file(os.path.join(output_dir, ".rejected"))
85
+
86
+ os.makedirs(output_dir, exist_ok=True)
87
+ dumped_path = os.path.join(output_dir, ".dumped")
88
+ clear_file(dumped_path) # 清空旧 .dumped
89
+ r = dump_subtitles(subtitles, output_dir, rejected, dumped_path=dumped_path)
90
+
91
+ total = len(subtitles)
92
+ parts = []
93
+ if r.dupes > 0:
94
+ parts.append(f"{r.dupes} dupes")
95
+ if r.skipped > 0:
96
+ parts.append(f"{r.skipped} rejected")
97
+ dup_msg = f" ({', '.join(parts)})" if parts else ""
98
+ print(f"\n{GREEN} ✓ Downloaded {r.downloaded}/{total}{dup_msg}{RESET}\n")
99
+
100
+ except ThunderSubtitleError as e:
101
+ display_error(str(e))
102
+ raise CLIExit()