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.
- thunder_subtitle_srt-1.0.0/PKG-INFO +182 -0
- thunder_subtitle_srt-1.0.0/README.md +169 -0
- thunder_subtitle_srt-1.0.0/cli.py +156 -0
- thunder_subtitle_srt-1.0.0/commands/__init__.py +1 -0
- thunder_subtitle_srt-1.0.0/commands/config.py +33 -0
- thunder_subtitle_srt-1.0.0/commands/download.py +34 -0
- thunder_subtitle_srt-1.0.0/commands/dump.py +102 -0
- thunder_subtitle_srt-1.0.0/commands/review.py +42 -0
- thunder_subtitle_srt-1.0.0/commands/scan.py +41 -0
- thunder_subtitle_srt-1.0.0/commands/search.py +153 -0
- thunder_subtitle_srt-1.0.0/pyproject.toml +43 -0
- thunder_subtitle_srt-1.0.0/setup.cfg +4 -0
- thunder_subtitle_srt-1.0.0/src/__init__.py +0 -0
- thunder_subtitle_srt-1.0.0/src/api.py +129 -0
- thunder_subtitle_srt-1.0.0/src/config.py +72 -0
- thunder_subtitle_srt-1.0.0/src/download.py +217 -0
- thunder_subtitle_srt-1.0.0/src/exceptions.py +33 -0
- thunder_subtitle_srt-1.0.0/src/models.py +74 -0
- thunder_subtitle_srt-1.0.0/src/py.typed +0 -0
- thunder_subtitle_srt-1.0.0/src/reviewer/__init__.py +143 -0
- thunder_subtitle_srt-1.0.0/src/reviewer/_encoding.py +32 -0
- thunder_subtitle_srt-1.0.0/src/reviewer/_marker.py +111 -0
- thunder_subtitle_srt-1.0.0/src/reviewer/_output.py +99 -0
- thunder_subtitle_srt-1.0.0/src/reviewer/_review.py +139 -0
- thunder_subtitle_srt-1.0.0/src/reviewer/_srt.py +109 -0
- thunder_subtitle_srt-1.0.0/src/scanner/__init__.py +7 -0
- thunder_subtitle_srt-1.0.0/src/scanner/_dir.py +44 -0
- thunder_subtitle_srt-1.0.0/src/scanner/_io.py +96 -0
- thunder_subtitle_srt-1.0.0/src/scanner/_parallel.py +205 -0
- thunder_subtitle_srt-1.0.0/src/scanner/_processor.py +259 -0
- thunder_subtitle_srt-1.0.0/src/scanner/_skip.py +193 -0
- thunder_subtitle_srt-1.0.0/src/ui.py +70 -0
- thunder_subtitle_srt-1.0.0/src/utils.py +181 -0
- thunder_subtitle_srt-1.0.0/tests/test_api.py +275 -0
- thunder_subtitle_srt-1.0.0/tests/test_cli.py +154 -0
- thunder_subtitle_srt-1.0.0/tests/test_config.py +107 -0
- thunder_subtitle_srt-1.0.0/tests/test_download.py +241 -0
- thunder_subtitle_srt-1.0.0/tests/test_integration.py +114 -0
- thunder_subtitle_srt-1.0.0/tests/test_reviewer.py +185 -0
- thunder_subtitle_srt-1.0.0/tests/test_scanner_helpers.py +436 -0
- thunder_subtitle_srt-1.0.0/tests/test_utils.py +169 -0
- thunder_subtitle_srt-1.0.0/thunder_subtitle_srt.egg-info/PKG-INFO +182 -0
- thunder_subtitle_srt-1.0.0/thunder_subtitle_srt.egg-info/SOURCES.txt +45 -0
- thunder_subtitle_srt-1.0.0/thunder_subtitle_srt.egg-info/dependency_links.txt +1 -0
- thunder_subtitle_srt-1.0.0/thunder_subtitle_srt.egg-info/entry_points.txt +2 -0
- thunder_subtitle_srt-1.0.0/thunder_subtitle_srt.egg-info/requires.txt +7 -0
- 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()
|