moviefinder-cli 0.1.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.
- moviefinder_cli-0.1.0/.github/workflows/publish.yml +27 -0
- moviefinder_cli-0.1.0/.gitignore +8 -0
- moviefinder_cli-0.1.0/CHANGELOG.md +9 -0
- moviefinder_cli-0.1.0/LICENSE +21 -0
- moviefinder_cli-0.1.0/PKG-INFO +211 -0
- moviefinder_cli-0.1.0/README.md +195 -0
- moviefinder_cli-0.1.0/pyproject.toml +35 -0
- moviefinder_cli-0.1.0/requirements.txt +3 -0
- moviefinder_cli-0.1.0/src/moviefinder_cli/__init__.py +5 -0
- moviefinder_cli-0.1.0/src/moviefinder_cli/__main__.py +5 -0
- moviefinder_cli-0.1.0/src/moviefinder_cli/cache.py +125 -0
- moviefinder_cli-0.1.0/src/moviefinder_cli/cli.py +82 -0
- moviefinder_cli-0.1.0/src/moviefinder_cli/markdown.py +129 -0
- moviefinder_cli-0.1.0/src/moviefinder_cli/mcp/__init__.py +1 -0
- moviefinder_cli-0.1.0/src/moviefinder_cli/mcp/server.py +143 -0
- moviefinder_cli-0.1.0/src/moviefinder_cli/models.py +53 -0
- moviefinder_cli-0.1.0/src/moviefinder_cli/rrdynb.py +329 -0
- moviefinder_cli-0.1.0/src/moviefinder_cli/server.py +78 -0
- moviefinder_cli-0.1.0/src/moviefinder_cli/service.py +182 -0
- moviefinder_cli-0.1.0/tests/__init__.py +1 -0
- moviefinder_cli-0.1.0/tests/test_cache.py +40 -0
- moviefinder_cli-0.1.0/tests/test_markdown.py +64 -0
- moviefinder_cli-0.1.0/tests/test_rrdynb_parser.py +127 -0
- moviefinder_cli-0.1.0/uv.lock +1920 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
pypi-publish:
|
|
9
|
+
name: Build & Publish Release to PyPI
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
id-token: write
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout target repository
|
|
18
|
+
uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Install the latest version of uv
|
|
21
|
+
uses: astral-sh/setup-uv@v5
|
|
22
|
+
|
|
23
|
+
- name: Build package
|
|
24
|
+
run: uv build
|
|
25
|
+
|
|
26
|
+
- name: Publish package distributions to PyPI
|
|
27
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 - 2026-05-30
|
|
4
|
+
|
|
5
|
+
- Initial PyPI-ready release.
|
|
6
|
+
- Add `moviefinder` CLI with Markdown and JSON output.
|
|
7
|
+
- Add SQLite search/detail caching with stale-cache fallback.
|
|
8
|
+
- Add `moviefinder-mcp` entrypoint for Agent/MCP clients.
|
|
9
|
+
- Parse rrdynb search results, movie synopsis, Douban score, IMDB score, and cloud-drive resource links.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Han Zhang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: moviefinder-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MovieFinder CLI and MCP server for searching cached rrdynb movie resources.
|
|
5
|
+
Author: Han Zhang
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: cli,markdown,mcp,movie,movie-search,rrdynb
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Requires-Dist: beautifulsoup4>=4.12
|
|
11
|
+
Requires-Dist: fastmcp>=0.1.0
|
|
12
|
+
Requires-Dist: requests>=2.31
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# moviefinder-cli
|
|
18
|
+
|
|
19
|
+
MovieFinder 会搜索 `https://www.rrdynb.com`,抓取搜索结果页和影片详情页,把结果缓存到本地 SQLite,并以 Markdown/JSON 返回给 CLI 或 Agent。
|
|
20
|
+
|
|
21
|
+
返回字段包括:
|
|
22
|
+
|
|
23
|
+
- 影片标题、详情页地址、分类、发布日期、海报
|
|
24
|
+
- 简介、豆瓣评分、IMDB 评分、IMDb ID
|
|
25
|
+
- 详情页中识别到的所有网盘地址,包括夸克、阿里、迅雷、百度等
|
|
26
|
+
- 提取码和 URL 中的 `pwd` 参数
|
|
27
|
+
|
|
28
|
+
## Agent / MCP 使用
|
|
29
|
+
|
|
30
|
+
发布到 PyPI 后,可以在 Claude Desktop 等 MCP 客户端中直接通过 `uvx` 启动:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"mcpServers": {
|
|
35
|
+
"moviefinder": {
|
|
36
|
+
"command": "uvx",
|
|
37
|
+
"args": [
|
|
38
|
+
"--refresh",
|
|
39
|
+
"--from",
|
|
40
|
+
"moviefinder-cli",
|
|
41
|
+
"moviefinder-mcp"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
可用 MCP Tools:
|
|
49
|
+
|
|
50
|
+
- `search_movies(query, limit=5, refresh=false, output_format="markdown")`
|
|
51
|
+
- `movie_cache_stats(output_format="markdown")`
|
|
52
|
+
|
|
53
|
+
## 安装
|
|
54
|
+
|
|
55
|
+
需要 Python 3.10+。
|
|
56
|
+
|
|
57
|
+
### 使用 uv 安装(推荐)
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
uv tool install moviefinder-cli
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
安装后即可使用:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
moviefinder search 一一 --limit 3
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 使用 pip 安装
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install moviefinder-cli
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 源码安装(本地开发)
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
python3 -m pip install -e .
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 命令行搜索
|
|
82
|
+
|
|
83
|
+
默认输出 Markdown:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
moviefinder search 一一 --limit 3
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
也可以不安装,直接通过模块方式运行:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
python3 -m moviefinder_cli search 一一 --limit 3
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
强制刷新缓存:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
moviefinder search 一一 --limit 3 --refresh
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
保留 JSON 输出:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
moviefinder search 一一 --limit 3 --format json
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
查看缓存统计:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
moviefinder cache-stats
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## HTTP API
|
|
114
|
+
|
|
115
|
+
启动服务:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
python3 -m moviefinder_cli serve --host 127.0.0.1 --port 8000
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
搜索:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
curl 'http://127.0.0.1:8000/search?q=一一&limit=3'
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
强制刷新:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
curl 'http://127.0.0.1:8000/search?q=一一&limit=3&refresh=1'
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
健康检查:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
curl 'http://127.0.0.1:8000/health'
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
缓存统计:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
curl 'http://127.0.0.1:8000/cache/stats'
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## 缓存
|
|
146
|
+
|
|
147
|
+
默认缓存文件是 `data/moviefinder.sqlite3`,可以通过环境变量覆盖:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
MOVIEFINDER_DB_PATH=/tmp/moviefinder.sqlite3 moviefinder search 一一
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
缓存策略:
|
|
154
|
+
|
|
155
|
+
- 搜索结果缓存 1 小时
|
|
156
|
+
- 影片详情缓存 7 天
|
|
157
|
+
- 搜索结果会复用影片详情缓存,避免同一详情页被重复抓取
|
|
158
|
+
- 如果远端临时返回 403/网络错误,CLI 会优先用同关键词缓存兜底,并在 Markdown/JSON 中标注 `warning`
|
|
159
|
+
|
|
160
|
+
## 返回示例
|
|
161
|
+
|
|
162
|
+
```markdown
|
|
163
|
+
# 搜索结果:一一
|
|
164
|
+
|
|
165
|
+
- 来源:rrdynb
|
|
166
|
+
- 搜索页:[打开](https://www.rrdynb.com/plus/search.php?q=...)
|
|
167
|
+
- 结果数:1
|
|
168
|
+
- 缓存:命中
|
|
169
|
+
|
|
170
|
+
## 1. 《一一》百度云网盘下载.阿里云盘.国语中字.(2000)
|
|
171
|
+
|
|
172
|
+
| 字段 | 值 |
|
|
173
|
+
| --- | --- |
|
|
174
|
+
| 分类 | movie |
|
|
175
|
+
| 发布日期 | 2026-05-29 |
|
|
176
|
+
| 豆瓣评分 | 9.0 |
|
|
177
|
+
| IMDB 评分 | 8.1 |
|
|
178
|
+
| IMDb ID | tt0244316 |
|
|
179
|
+
| 详情页 | [打开](https://www.rrdynb.com/movie/2019/0216/3035.html) |
|
|
180
|
+
|
|
181
|
+
### 简介
|
|
182
|
+
|
|
183
|
+
电影《一一》讲述了...
|
|
184
|
+
|
|
185
|
+
### 网盘资源
|
|
186
|
+
|
|
187
|
+
| 平台 | 地址 | 提取码 | URL 密码 |
|
|
188
|
+
| --- | --- | --- | --- |
|
|
189
|
+
| 夸克网盘 | [打开](https://pan.quark.cn/s/...) | - | - |
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## 测试
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
PYTHONPATH=src python3 -m unittest discover -s tests
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## 发布
|
|
199
|
+
|
|
200
|
+
项目使用与 `getnotes-cli` 类似的发布模式:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
uv build
|
|
204
|
+
uv publish
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
如果使用 GitHub Release 发布,则 `.github/workflows/publish.yml` 会通过 PyPI Trusted Publisher 自动发布。
|
|
208
|
+
|
|
209
|
+
## 注意
|
|
210
|
+
|
|
211
|
+
这个项目只读取公开页面并返回元数据和页面公开展示的网盘链接,不做登录绕过、验证码绕过或资源批量下载。
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# moviefinder-cli
|
|
2
|
+
|
|
3
|
+
MovieFinder 会搜索 `https://www.rrdynb.com`,抓取搜索结果页和影片详情页,把结果缓存到本地 SQLite,并以 Markdown/JSON 返回给 CLI 或 Agent。
|
|
4
|
+
|
|
5
|
+
返回字段包括:
|
|
6
|
+
|
|
7
|
+
- 影片标题、详情页地址、分类、发布日期、海报
|
|
8
|
+
- 简介、豆瓣评分、IMDB 评分、IMDb ID
|
|
9
|
+
- 详情页中识别到的所有网盘地址,包括夸克、阿里、迅雷、百度等
|
|
10
|
+
- 提取码和 URL 中的 `pwd` 参数
|
|
11
|
+
|
|
12
|
+
## Agent / MCP 使用
|
|
13
|
+
|
|
14
|
+
发布到 PyPI 后,可以在 Claude Desktop 等 MCP 客户端中直接通过 `uvx` 启动:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"mcpServers": {
|
|
19
|
+
"moviefinder": {
|
|
20
|
+
"command": "uvx",
|
|
21
|
+
"args": [
|
|
22
|
+
"--refresh",
|
|
23
|
+
"--from",
|
|
24
|
+
"moviefinder-cli",
|
|
25
|
+
"moviefinder-mcp"
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
可用 MCP Tools:
|
|
33
|
+
|
|
34
|
+
- `search_movies(query, limit=5, refresh=false, output_format="markdown")`
|
|
35
|
+
- `movie_cache_stats(output_format="markdown")`
|
|
36
|
+
|
|
37
|
+
## 安装
|
|
38
|
+
|
|
39
|
+
需要 Python 3.10+。
|
|
40
|
+
|
|
41
|
+
### 使用 uv 安装(推荐)
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uv tool install moviefinder-cli
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
安装后即可使用:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
moviefinder search 一一 --limit 3
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 使用 pip 安装
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install moviefinder-cli
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 源码安装(本地开发)
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
python3 -m pip install -e .
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 命令行搜索
|
|
66
|
+
|
|
67
|
+
默认输出 Markdown:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
moviefinder search 一一 --limit 3
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
也可以不安装,直接通过模块方式运行:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
python3 -m moviefinder_cli search 一一 --limit 3
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
强制刷新缓存:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
moviefinder search 一一 --limit 3 --refresh
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
保留 JSON 输出:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
moviefinder search 一一 --limit 3 --format json
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
查看缓存统计:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
moviefinder cache-stats
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## HTTP API
|
|
98
|
+
|
|
99
|
+
启动服务:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
python3 -m moviefinder_cli serve --host 127.0.0.1 --port 8000
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
搜索:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
curl 'http://127.0.0.1:8000/search?q=一一&limit=3'
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
强制刷新:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
curl 'http://127.0.0.1:8000/search?q=一一&limit=3&refresh=1'
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
健康检查:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
curl 'http://127.0.0.1:8000/health'
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
缓存统计:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
curl 'http://127.0.0.1:8000/cache/stats'
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## 缓存
|
|
130
|
+
|
|
131
|
+
默认缓存文件是 `data/moviefinder.sqlite3`,可以通过环境变量覆盖:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
MOVIEFINDER_DB_PATH=/tmp/moviefinder.sqlite3 moviefinder search 一一
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
缓存策略:
|
|
138
|
+
|
|
139
|
+
- 搜索结果缓存 1 小时
|
|
140
|
+
- 影片详情缓存 7 天
|
|
141
|
+
- 搜索结果会复用影片详情缓存,避免同一详情页被重复抓取
|
|
142
|
+
- 如果远端临时返回 403/网络错误,CLI 会优先用同关键词缓存兜底,并在 Markdown/JSON 中标注 `warning`
|
|
143
|
+
|
|
144
|
+
## 返回示例
|
|
145
|
+
|
|
146
|
+
```markdown
|
|
147
|
+
# 搜索结果:一一
|
|
148
|
+
|
|
149
|
+
- 来源:rrdynb
|
|
150
|
+
- 搜索页:[打开](https://www.rrdynb.com/plus/search.php?q=...)
|
|
151
|
+
- 结果数:1
|
|
152
|
+
- 缓存:命中
|
|
153
|
+
|
|
154
|
+
## 1. 《一一》百度云网盘下载.阿里云盘.国语中字.(2000)
|
|
155
|
+
|
|
156
|
+
| 字段 | 值 |
|
|
157
|
+
| --- | --- |
|
|
158
|
+
| 分类 | movie |
|
|
159
|
+
| 发布日期 | 2026-05-29 |
|
|
160
|
+
| 豆瓣评分 | 9.0 |
|
|
161
|
+
| IMDB 评分 | 8.1 |
|
|
162
|
+
| IMDb ID | tt0244316 |
|
|
163
|
+
| 详情页 | [打开](https://www.rrdynb.com/movie/2019/0216/3035.html) |
|
|
164
|
+
|
|
165
|
+
### 简介
|
|
166
|
+
|
|
167
|
+
电影《一一》讲述了...
|
|
168
|
+
|
|
169
|
+
### 网盘资源
|
|
170
|
+
|
|
171
|
+
| 平台 | 地址 | 提取码 | URL 密码 |
|
|
172
|
+
| --- | --- | --- | --- |
|
|
173
|
+
| 夸克网盘 | [打开](https://pan.quark.cn/s/...) | - | - |
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## 测试
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
PYTHONPATH=src python3 -m unittest discover -s tests
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## 发布
|
|
183
|
+
|
|
184
|
+
项目使用与 `getnotes-cli` 类似的发布模式:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
uv build
|
|
188
|
+
uv publish
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
如果使用 GitHub Release 发布,则 `.github/workflows/publish.yml` 会通过 PyPI Trusted Publisher 自动发布。
|
|
192
|
+
|
|
193
|
+
## 注意
|
|
194
|
+
|
|
195
|
+
这个项目只读取公开页面并返回元数据和页面公开展示的网盘链接,不做登录绕过、验证码绕过或资源批量下载。
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "moviefinder-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MovieFinder CLI and MCP server for searching cached rrdynb movie resources."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Han Zhang" }
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.10"
|
|
15
|
+
keywords = ["movie", "movie-search", "rrdynb", "cli", "markdown", "mcp"]
|
|
16
|
+
dependencies = [
|
|
17
|
+
"beautifulsoup4>=4.12",
|
|
18
|
+
"requests>=2.31",
|
|
19
|
+
"fastmcp>=0.1.0",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
moviefinder = "moviefinder_cli.cli:main"
|
|
24
|
+
moviefinder-mcp = "moviefinder_cli.mcp.server:main"
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
dev = [
|
|
28
|
+
"pytest>=7.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.wheel]
|
|
32
|
+
packages = ["src/moviefinder_cli"]
|
|
33
|
+
|
|
34
|
+
[tool.pytest.ini_options]
|
|
35
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sqlite3
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SqliteCache:
|
|
9
|
+
def __init__(self, db_path: str = "data/moviefinder.sqlite3") -> None:
|
|
10
|
+
self.db_path = Path(db_path)
|
|
11
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
12
|
+
self._ensure_schema()
|
|
13
|
+
|
|
14
|
+
def _connect(self) -> sqlite3.Connection:
|
|
15
|
+
connection = sqlite3.connect(str(self.db_path))
|
|
16
|
+
connection.row_factory = sqlite3.Row
|
|
17
|
+
return connection
|
|
18
|
+
|
|
19
|
+
def _ensure_schema(self) -> None:
|
|
20
|
+
with self._connect() as connection:
|
|
21
|
+
connection.execute(
|
|
22
|
+
"""
|
|
23
|
+
CREATE TABLE IF NOT EXISTS cache_entries (
|
|
24
|
+
namespace TEXT NOT NULL,
|
|
25
|
+
cache_key TEXT NOT NULL,
|
|
26
|
+
payload TEXT NOT NULL,
|
|
27
|
+
created_at REAL NOT NULL,
|
|
28
|
+
expires_at REAL NOT NULL,
|
|
29
|
+
PRIMARY KEY (namespace, cache_key)
|
|
30
|
+
)
|
|
31
|
+
"""
|
|
32
|
+
)
|
|
33
|
+
connection.execute(
|
|
34
|
+
"""
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_cache_entries_expires_at
|
|
36
|
+
ON cache_entries(expires_at)
|
|
37
|
+
"""
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def get_json(self, namespace: str, cache_key: str) -> Tuple[Optional[Any], bool]:
|
|
41
|
+
payload, hit, _expired = self.get_json_with_meta(namespace, cache_key)
|
|
42
|
+
return payload, hit
|
|
43
|
+
|
|
44
|
+
def get_json_with_meta(
|
|
45
|
+
self, namespace: str, cache_key: str, allow_expired: bool = False
|
|
46
|
+
) -> Tuple[Optional[Any], bool, bool]:
|
|
47
|
+
now = time.time()
|
|
48
|
+
with self._connect() as connection:
|
|
49
|
+
row = connection.execute(
|
|
50
|
+
"""
|
|
51
|
+
SELECT payload, expires_at
|
|
52
|
+
FROM cache_entries
|
|
53
|
+
WHERE namespace = ? AND cache_key = ?
|
|
54
|
+
""",
|
|
55
|
+
(namespace, cache_key),
|
|
56
|
+
).fetchone()
|
|
57
|
+
if row is None:
|
|
58
|
+
return None, False, False
|
|
59
|
+
expired = float(row["expires_at"]) < now
|
|
60
|
+
if expired and not allow_expired:
|
|
61
|
+
return None, False, True
|
|
62
|
+
return json.loads(row["payload"]), True, expired
|
|
63
|
+
|
|
64
|
+
def set_json(
|
|
65
|
+
self, namespace: str, cache_key: str, value: Any, ttl_seconds: int
|
|
66
|
+
) -> None:
|
|
67
|
+
now = time.time()
|
|
68
|
+
with self._connect() as connection:
|
|
69
|
+
connection.execute(
|
|
70
|
+
"""
|
|
71
|
+
INSERT INTO cache_entries
|
|
72
|
+
(namespace, cache_key, payload, created_at, expires_at)
|
|
73
|
+
VALUES (?, ?, ?, ?, ?)
|
|
74
|
+
ON CONFLICT(namespace, cache_key) DO UPDATE SET
|
|
75
|
+
payload = excluded.payload,
|
|
76
|
+
created_at = excluded.created_at,
|
|
77
|
+
expires_at = excluded.expires_at
|
|
78
|
+
""",
|
|
79
|
+
(
|
|
80
|
+
namespace,
|
|
81
|
+
cache_key,
|
|
82
|
+
json.dumps(value, ensure_ascii=False),
|
|
83
|
+
now,
|
|
84
|
+
now + ttl_seconds,
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def iter_json(
|
|
89
|
+
self, namespace: str, allow_expired: bool = False
|
|
90
|
+
) -> List[Tuple[str, Any, bool]]:
|
|
91
|
+
now = time.time()
|
|
92
|
+
with self._connect() as connection:
|
|
93
|
+
rows = connection.execute(
|
|
94
|
+
"""
|
|
95
|
+
SELECT cache_key, payload, expires_at
|
|
96
|
+
FROM cache_entries
|
|
97
|
+
WHERE namespace = ?
|
|
98
|
+
ORDER BY created_at DESC
|
|
99
|
+
""",
|
|
100
|
+
(namespace,),
|
|
101
|
+
).fetchall()
|
|
102
|
+
|
|
103
|
+
entries: List[Tuple[str, Any, bool]] = []
|
|
104
|
+
for row in rows:
|
|
105
|
+
expired = float(row["expires_at"]) < now
|
|
106
|
+
if expired and not allow_expired:
|
|
107
|
+
continue
|
|
108
|
+
entries.append((str(row["cache_key"]), json.loads(row["payload"]), expired))
|
|
109
|
+
return entries
|
|
110
|
+
|
|
111
|
+
def clear(self) -> None:
|
|
112
|
+
with self._connect() as connection:
|
|
113
|
+
connection.execute("DELETE FROM cache_entries")
|
|
114
|
+
|
|
115
|
+
def stats(self) -> Dict[str, int]:
|
|
116
|
+
with self._connect() as connection:
|
|
117
|
+
rows = connection.execute(
|
|
118
|
+
"""
|
|
119
|
+
SELECT namespace, COUNT(*) AS count
|
|
120
|
+
FROM cache_entries
|
|
121
|
+
GROUP BY namespace
|
|
122
|
+
ORDER BY namespace
|
|
123
|
+
"""
|
|
124
|
+
).fetchall()
|
|
125
|
+
return {str(row["namespace"]): int(row["count"]) for row in rows}
|