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.
@@ -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,8 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ data/
5
+ .pytest_cache/
6
+ *.egg-info/
7
+ build/
8
+ dist/
@@ -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,3 @@
1
+ beautifulsoup4>=4.12
2
+ requests>=2.31
3
+ fastmcp>=0.1.0
@@ -0,0 +1,5 @@
1
+ """MovieFinder package."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -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}