pexels-mcp 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.
- pexels_mcp-1.0.0/LICENSE +21 -0
- pexels_mcp-1.0.0/PKG-INFO +100 -0
- pexels_mcp-1.0.0/README.md +75 -0
- pexels_mcp-1.0.0/pyproject.toml +47 -0
- pexels_mcp-1.0.0/src/pexels_mcp/__init__.py +15 -0
- pexels_mcp-1.0.0/src/pexels_mcp/__main__.py +3 -0
- pexels_mcp-1.0.0/src/pexels_mcp/cli.py +201 -0
- pexels_mcp-1.0.0/src/pexels_mcp/core.py +583 -0
- pexels_mcp-1.0.0/src/pexels_mcp/server.py +331 -0
pexels_mcp-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 aqiu
|
|
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,100 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pexels-mcp
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: MCP + CLI toolkit to search and download free Pexels stock images/videos. No API key, no quota. Built-in relevance gate kills Pexels's junk hits; picks the right resolution tier for vertical shorts.
|
|
5
|
+
Project-URL: Homepage, https://modelscope.cn/mcp/servers
|
|
6
|
+
Author: aqiu
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: images,mcp,model-context-protocol,pexels,shorts,stock,video-assembly,videos
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
21
|
+
Classifier: Topic :: Multimedia :: Video
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# pexels-mcp
|
|
27
|
+
|
|
28
|
+
MCP + CLI toolkit to **search and download free Pexels stock images/videos**.
|
|
29
|
+
|
|
30
|
+
- **Zero third-party dependencies** — pure Python standard library (urllib). Install, run, done.
|
|
31
|
+
- **No API key, no quota** — talks to Pexels' own web front-end API v3 (the public `secret-key` constant Pexels hardcodes in its JS).
|
|
32
|
+
- **Built-in relevance gate** — Pexels returns *something* for any query (even `zzzqqqxxx_not_exist_9999`). This tool validates every candidate against title/tags and drops 0-hit junk, so you never get unrelated footage.
|
|
33
|
+
- **Right resolution tier, automatically** — picks the video_files tier closest to your target (default 1080×1920 portrait) and refuses to upscale postage-stamp clips.
|
|
34
|
+
- **Chinese query rejected** — the English site can't search Chinese meaningfully. Translate to concrete English words (`office meeting` > `work`).
|
|
35
|
+
|
|
36
|
+
Works as an [MCP](https://modelcontextprotocol.io) server (5 tools) or a plain CLI.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install pexels-mcp # or: uvx pexels-mcp@latest
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### As an MCP server
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"pexels": {
|
|
52
|
+
"command": "uvx",
|
|
53
|
+
"args": ["pexels-mcp@latest"],
|
|
54
|
+
"env": { "PEXELS_OUT_DIR": "/tmp/pexels_download" }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Any MCP client (Claude Desktop, CowAgent, …) sees 5 tools:
|
|
61
|
+
|
|
62
|
+
| tool | what it does |
|
|
63
|
+
|-----------------|--------------|
|
|
64
|
+
| `pexels_search` | search only, returns direct links + size/duration metadata; no junk hits |
|
|
65
|
+
| `pexels_get` | search **and download** N items to a local dir, returns paths + byte counts |
|
|
66
|
+
| `pexels_download` | download a single direct link you already have |
|
|
67
|
+
| `pexels_health` | channel self-check; includes a reverse probe (a nonsense query must return 0 hits) |
|
|
68
|
+
| `pexels_info` | capability boundary, no network |
|
|
69
|
+
|
|
70
|
+
`PEXELS_OUT_DIR` sets the download folder (default: OS temp dir).
|
|
71
|
+
|
|
72
|
+
### As a CLI
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pexels-cli search "elderly woman face" --kind video --n 5
|
|
76
|
+
pexels-cli search "office meeting" --json
|
|
77
|
+
pexels-cli get "hands holding cash" -o ./out --n 2
|
|
78
|
+
pexels-cli health
|
|
79
|
+
pexels-cli info
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
All subcommands support `--json` for machine-readable output.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Why it's reliable
|
|
87
|
+
|
|
88
|
+
- **No fake hits**: Pexels fabricates total_results even for nonsense queries. We split your query into content tokens (with a stop-word list — `not` must not match `do not lean`), and require ≥1 token to actually appear in the item's title/tags/slug. A test with `zzzqqqxxx_not_exist_9999` returns 0 hits.
|
|
89
|
+
- **No decompression trap**: we request `Accept-Encoding: identity` (avoid brotli unless you have it) and fall back to gunzip.
|
|
90
|
+
- **Throttle-safe**: measured 100 sequential + 8-way parallel direct downloads at 100% success with no Cloudflare intervention.
|
|
91
|
+
- **Bomb fuse**: downloads stream with size cap, and a `text/html` response is rejected as a Cloudflare block page masquerading as HTTP 200.
|
|
92
|
+
|
|
93
|
+
## Notes
|
|
94
|
+
|
|
95
|
+
- This package is just "give me a keyword → get good direct-link stock media". It does **not** assemble videos — pair it with an assembler MCP (e.g. `video-assembler-mcp`) for the full pipeline.
|
|
96
|
+
- The endpoint is Pexels' own public web API and could change at any time. It's a third-party behavior, out of our control; `pexels_health` exists precisely to catch that the day it happens.
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# pexels-mcp
|
|
2
|
+
|
|
3
|
+
MCP + CLI toolkit to **search and download free Pexels stock images/videos**.
|
|
4
|
+
|
|
5
|
+
- **Zero third-party dependencies** — pure Python standard library (urllib). Install, run, done.
|
|
6
|
+
- **No API key, no quota** — talks to Pexels' own web front-end API v3 (the public `secret-key` constant Pexels hardcodes in its JS).
|
|
7
|
+
- **Built-in relevance gate** — Pexels returns *something* for any query (even `zzzqqqxxx_not_exist_9999`). This tool validates every candidate against title/tags and drops 0-hit junk, so you never get unrelated footage.
|
|
8
|
+
- **Right resolution tier, automatically** — picks the video_files tier closest to your target (default 1080×1920 portrait) and refuses to upscale postage-stamp clips.
|
|
9
|
+
- **Chinese query rejected** — the English site can't search Chinese meaningfully. Translate to concrete English words (`office meeting` > `work`).
|
|
10
|
+
|
|
11
|
+
Works as an [MCP](https://modelcontextprotocol.io) server (5 tools) or a plain CLI.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install pexels-mcp # or: uvx pexels-mcp@latest
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### As an MCP server
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"mcpServers": {
|
|
26
|
+
"pexels": {
|
|
27
|
+
"command": "uvx",
|
|
28
|
+
"args": ["pexels-mcp@latest"],
|
|
29
|
+
"env": { "PEXELS_OUT_DIR": "/tmp/pexels_download" }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Any MCP client (Claude Desktop, CowAgent, …) sees 5 tools:
|
|
36
|
+
|
|
37
|
+
| tool | what it does |
|
|
38
|
+
|-----------------|--------------|
|
|
39
|
+
| `pexels_search` | search only, returns direct links + size/duration metadata; no junk hits |
|
|
40
|
+
| `pexels_get` | search **and download** N items to a local dir, returns paths + byte counts |
|
|
41
|
+
| `pexels_download` | download a single direct link you already have |
|
|
42
|
+
| `pexels_health` | channel self-check; includes a reverse probe (a nonsense query must return 0 hits) |
|
|
43
|
+
| `pexels_info` | capability boundary, no network |
|
|
44
|
+
|
|
45
|
+
`PEXELS_OUT_DIR` sets the download folder (default: OS temp dir).
|
|
46
|
+
|
|
47
|
+
### As a CLI
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pexels-cli search "elderly woman face" --kind video --n 5
|
|
51
|
+
pexels-cli search "office meeting" --json
|
|
52
|
+
pexels-cli get "hands holding cash" -o ./out --n 2
|
|
53
|
+
pexels-cli health
|
|
54
|
+
pexels-cli info
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
All subcommands support `--json` for machine-readable output.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Why it's reliable
|
|
62
|
+
|
|
63
|
+
- **No fake hits**: Pexels fabricates total_results even for nonsense queries. We split your query into content tokens (with a stop-word list — `not` must not match `do not lean`), and require ≥1 token to actually appear in the item's title/tags/slug. A test with `zzzqqqxxx_not_exist_9999` returns 0 hits.
|
|
64
|
+
- **No decompression trap**: we request `Accept-Encoding: identity` (avoid brotli unless you have it) and fall back to gunzip.
|
|
65
|
+
- **Throttle-safe**: measured 100 sequential + 8-way parallel direct downloads at 100% success with no Cloudflare intervention.
|
|
66
|
+
- **Bomb fuse**: downloads stream with size cap, and a `text/html` response is rejected as a Cloudflare block page masquerading as HTTP 200.
|
|
67
|
+
|
|
68
|
+
## Notes
|
|
69
|
+
|
|
70
|
+
- This package is just "give me a keyword → get good direct-link stock media". It does **not** assemble videos — pair it with an assembler MCP (e.g. `video-assembler-mcp`) for the full pipeline.
|
|
71
|
+
- The endpoint is Pexels' own public web API and could change at any time. It's a third-party behavior, out of our control; `pexels_health` exists precisely to catch that the day it happens.
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
MIT
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.24"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pexels-mcp"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "MCP + CLI toolkit to search and download free Pexels stock images/videos. No API key, no quota. Built-in relevance gate kills Pexels's junk hits; picks the right resolution tier for vertical shorts."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "aqiu" }]
|
|
13
|
+
keywords = ["mcp", "pexels", "stock", "videos", "images", "shorts", "video-assembly", "model-context-protocol"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Multimedia :: Video",
|
|
26
|
+
"Topic :: Multimedia :: Graphics",
|
|
27
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
dependencies = []
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
pexels-mcp = "pexels_mcp.server:main"
|
|
34
|
+
pexels-cli = "pexels_mcp.cli:main"
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://modelscope.cn/mcp/servers"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/pexels_mcp"]
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.sdist]
|
|
43
|
+
include = [
|
|
44
|
+
"/src",
|
|
45
|
+
"/README.md",
|
|
46
|
+
"/LICENSE",
|
|
47
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""pexels-mcp —— Pexels 素材检索与下载的 MCP 工具集(零第三方依赖)。
|
|
2
|
+
|
|
3
|
+
两个入口:
|
|
4
|
+
pexels-mcp MCP server(stdio JSON-RPC)
|
|
5
|
+
pexels-cli 命令行:search / get / health / info
|
|
6
|
+
|
|
7
|
+
模块:
|
|
8
|
+
core 检索/选档/下载/自检(纯标准库)
|
|
9
|
+
server MCP server,5 个工具
|
|
10
|
+
cli 命令行入口
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
__version__ = "1.0.0"
|
|
15
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""pexels-cli —— Pexels 图片/视频检索与下载(零依赖)
|
|
4
|
+
|
|
5
|
+
python pexels_cli.py search "elderly woman face" --kind video --n 5
|
|
6
|
+
python pexels_cli.py search "office meeting" --json
|
|
7
|
+
python pexels_cli.py get "hands holding cash" -o ./out --n 2
|
|
8
|
+
python pexels_cli.py health
|
|
9
|
+
|
|
10
|
+
所有子命令都支持 --json,输出机器可读结构(便于被 Agent / 脚本消费)。
|
|
11
|
+
中文 query 会被拒绝并给出提示——英文站不支持中文检索。
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from . import core
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# --------------------------------------------------------------------- 输出
|
|
24
|
+
|
|
25
|
+
def _fmt_size(n: int) -> str:
|
|
26
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
27
|
+
if n < 1024 or unit == "GB":
|
|
28
|
+
return f"{n:.1f}{unit}" if unit != "B" else f"{n}B"
|
|
29
|
+
n /= 1024.0
|
|
30
|
+
return f"{n:.1f}GB"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def print_clips(clips: list[dict], query: str) -> None:
|
|
34
|
+
if not clips:
|
|
35
|
+
print(f" 0 条命中 (query={query!r})")
|
|
36
|
+
print(" 提示:Pexels 是英文站,请用具体可视的英文词,如 'office meeting' 而非 'work'")
|
|
37
|
+
return
|
|
38
|
+
print(f" 命中 {len(clips)} 条 (query={query!r})\n")
|
|
39
|
+
for i, c in enumerate(clips, 1):
|
|
40
|
+
dur = f"{c['duration']:.0f}s" if c.get("duration") else "—"
|
|
41
|
+
star = "★" if c.get("exact_dim") else " "
|
|
42
|
+
print(f" {i:2d}.{star} [{c['id']}] {c.get('width')}x{c.get('height')} "
|
|
43
|
+
f"时长={dur:>5} score={c.get('score', 0):.2f}")
|
|
44
|
+
title = (c.get("title") or "").strip()
|
|
45
|
+
if title:
|
|
46
|
+
print(f" {title[:70]}")
|
|
47
|
+
tags = c.get("tags") or []
|
|
48
|
+
if tags:
|
|
49
|
+
print(f" tags: {', '.join(tags[:6])}")
|
|
50
|
+
print(f" {c['url']}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# --------------------------------------------------------------------- 子命令
|
|
54
|
+
|
|
55
|
+
def cmd_search(a) -> int:
|
|
56
|
+
clips = core.search(
|
|
57
|
+
a.query,
|
|
58
|
+
kind=a.kind,
|
|
59
|
+
limit=a.num,
|
|
60
|
+
orientation=a.orientation,
|
|
61
|
+
target=None if a.any_size else tuple(a.target),
|
|
62
|
+
pages=a.pages,
|
|
63
|
+
need_sec=a.need_sec,
|
|
64
|
+
min_duration=a.min_duration,
|
|
65
|
+
max_duration=a.max_duration,
|
|
66
|
+
strict=a.strict,
|
|
67
|
+
)
|
|
68
|
+
if a.json:
|
|
69
|
+
print(json.dumps({"query": a.query, "kind": a.kind,
|
|
70
|
+
"count": len(clips), "clips": clips},
|
|
71
|
+
ensure_ascii=False, indent=2))
|
|
72
|
+
else:
|
|
73
|
+
print_clips(clips, a.query)
|
|
74
|
+
return 0 if clips else 3
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def cmd_get(a) -> int:
|
|
78
|
+
res = core.fetch(
|
|
79
|
+
a.query,
|
|
80
|
+
out_dir=a.out,
|
|
81
|
+
kind=a.kind,
|
|
82
|
+
n=a.num,
|
|
83
|
+
orientation=a.orientation,
|
|
84
|
+
target=None if a.any_size else tuple(a.target),
|
|
85
|
+
need_sec=a.need_sec,
|
|
86
|
+
)
|
|
87
|
+
if not a.json:
|
|
88
|
+
print_clips(res["clips"], a.query)
|
|
89
|
+
print()
|
|
90
|
+
for f in res["files"]:
|
|
91
|
+
if f["ok"]:
|
|
92
|
+
print(f" ✓ {f['path']} ({_fmt_size(f['bytes'])})")
|
|
93
|
+
else:
|
|
94
|
+
print(f" ✗ id={f.get('id')} {f['err']}")
|
|
95
|
+
if a.json:
|
|
96
|
+
print(json.dumps(res, ensure_ascii=False, indent=2))
|
|
97
|
+
return 0 if res["downloaded"] else 3
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def cmd_health(a) -> int:
|
|
101
|
+
h = core.health()
|
|
102
|
+
if a.json:
|
|
103
|
+
print(json.dumps(h, ensure_ascii=False, indent=2))
|
|
104
|
+
else:
|
|
105
|
+
print(f"通道 {'✓ 可用' if h['ok'] else '✗ 不可用'} "
|
|
106
|
+
f"耗时 {h['elapsed_sec']}s 端点 {h['endpoint']}")
|
|
107
|
+
for k, v in h["checks"].items():
|
|
108
|
+
mark = "✓" if v.get("ok") else "✗"
|
|
109
|
+
detail = v.get("err") or f"hits={v.get('hits')}"
|
|
110
|
+
print(f" {mark} {k:<12} {detail}")
|
|
111
|
+
if h.get("warning"):
|
|
112
|
+
print(f" ⚠ {h['warning']}")
|
|
113
|
+
return 0 if h["ok"] else 1
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def cmd_info(a) -> int:
|
|
117
|
+
"""不联网,打印能力边界与用法要点。"""
|
|
118
|
+
print(json.dumps({
|
|
119
|
+
"endpoint": core.API_BASE,
|
|
120
|
+
"kinds": ["video", "image"],
|
|
121
|
+
"orientations": list(core.ORIENTATIONS),
|
|
122
|
+
"per_page_max": core.PER_PAGE_MAX,
|
|
123
|
+
"needs_api_key": False,
|
|
124
|
+
"needs_browser": False,
|
|
125
|
+
"notes": [
|
|
126
|
+
"内部 API v3,无需注册、无配额",
|
|
127
|
+
"中文 query 会被拒绝,必须先翻成英文具体词",
|
|
128
|
+
"内置相关性门槛:有效词 0 命中的候选直接丢弃",
|
|
129
|
+
"video_files 直接给多档直链,可按目标分辨率挑档",
|
|
130
|
+
],
|
|
131
|
+
}, ensure_ascii=False, indent=2))
|
|
132
|
+
return 0
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# --------------------------------------------------------------------- 入口
|
|
136
|
+
|
|
137
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
138
|
+
# allow_abbrev=False:否则 `--n 3` 会被当成 `--need-sec 3` 的前缀缩写,
|
|
139
|
+
# 条数静默回落到默认值——这类「参数写错却不报错」的坑必须在发布版里堵死。
|
|
140
|
+
p = argparse.ArgumentParser(
|
|
141
|
+
prog="pexels-cli",
|
|
142
|
+
description="Pexels 图片/视频检索与下载(内部 API v3,零依赖)",
|
|
143
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
144
|
+
epilog=__doc__,
|
|
145
|
+
allow_abbrev=False,
|
|
146
|
+
)
|
|
147
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
148
|
+
|
|
149
|
+
def common(sp):
|
|
150
|
+
sp.add_argument("--kind", choices=["video", "image"], default="video",
|
|
151
|
+
help="素材类型,默认 video")
|
|
152
|
+
sp.add_argument("-n", "--num", type=int, default=5,
|
|
153
|
+
help="条数,默认 5(上限 24)")
|
|
154
|
+
sp.add_argument("--orientation", choices=list(core.ORIENTATIONS),
|
|
155
|
+
default="portrait", help="画面方向,默认竖屏")
|
|
156
|
+
sp.add_argument("--target", nargs=2, type=int, default=[1080, 1920],
|
|
157
|
+
metavar=("W", "H"), help="目标分辨率,默认 1080 1920")
|
|
158
|
+
sp.add_argument("--any-size", action="store_true",
|
|
159
|
+
help="不按目标分辨率挑档,改为挑最大档")
|
|
160
|
+
sp.add_argument("--need-sec", type=float, default=0.0,
|
|
161
|
+
help="场景需要的最短秒数,不够的会扣分排后")
|
|
162
|
+
sp.add_argument("--json", action="store_true", help="输出 JSON")
|
|
163
|
+
|
|
164
|
+
s = sub.add_parser("search", help="只检索,不下载", allow_abbrev=False)
|
|
165
|
+
s.add_argument("query")
|
|
166
|
+
s.add_argument("--pages", type=int, default=1, help="翻几页(每页最多 24)")
|
|
167
|
+
s.add_argument("--min-duration", type=float, default=0.0)
|
|
168
|
+
s.add_argument("--max-duration", type=float, default=0.0)
|
|
169
|
+
s.add_argument("--strict", action="store_true", help="网络失败时抛异常而非返回空")
|
|
170
|
+
common(s)
|
|
171
|
+
s.set_defaults(fn=cmd_search)
|
|
172
|
+
|
|
173
|
+
g = sub.add_parser("get", help="检索 + 下载", allow_abbrev=False)
|
|
174
|
+
g.add_argument("query")
|
|
175
|
+
g.add_argument("-o", "--out", default="./pexels_download", help="下载目录")
|
|
176
|
+
common(g)
|
|
177
|
+
g.set_defaults(fn=cmd_get)
|
|
178
|
+
|
|
179
|
+
h = sub.add_parser("health", help="通道自检(含假词反向校验)", allow_abbrev=False)
|
|
180
|
+
h.add_argument("--json", action="store_true")
|
|
181
|
+
h.set_defaults(fn=cmd_health)
|
|
182
|
+
|
|
183
|
+
i = sub.add_parser("info", help="打印能力边界(不联网)", allow_abbrev=False)
|
|
184
|
+
i.set_defaults(fn=cmd_info)
|
|
185
|
+
|
|
186
|
+
return p
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def main(argv=None) -> int:
|
|
190
|
+
a = build_parser().parse_args(argv)
|
|
191
|
+
try:
|
|
192
|
+
return a.fn(a)
|
|
193
|
+
except core.SearchError as e:
|
|
194
|
+
print(f"检索失败: {e}", file=sys.stderr)
|
|
195
|
+
return 1
|
|
196
|
+
except KeyboardInterrupt:
|
|
197
|
+
return 130
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
if __name__ == "__main__":
|
|
201
|
+
raise SystemExit(main())
|