openmd-cli 0.1.0__py3-none-any.whl
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.
- openmd/__init__.py +3 -0
- openmd/__main__.py +5 -0
- openmd/adapters/__init__.py +1 -0
- openmd/adapters/base.py +95 -0
- openmd/adapters/bilibili/__init__.py +11 -0
- openmd/adapters/bilibili/adapter.py +104 -0
- openmd/adapters/bilibili/discovery.py +33 -0
- openmd/adapters/douyin/__init__.py +11 -0
- openmd/adapters/douyin/adapter.py +117 -0
- openmd/adapters/douyin/discovery.py +36 -0
- openmd/adapters/douyin/sidecar.py +77 -0
- openmd/adapters/generic_media/__init__.py +14 -0
- openmd/adapters/high_privacy.py +77 -0
- openmd/adapters/local_audio/__init__.py +14 -0
- openmd/adapters/local_audio/adapter.py +169 -0
- openmd/adapters/local_audio/asr.py +58 -0
- openmd/adapters/local_audio/backends/__init__.py +1 -0
- openmd/adapters/local_audio/backends/auto.py +177 -0
- openmd/adapters/local_audio/backends/faster_whisper.py +285 -0
- openmd/adapters/local_audio/backends/funasr_sense_voice.py +14 -0
- openmd/adapters/local_audio/backends/qwen3_asr.py +254 -0
- openmd/adapters/opencli.py +770 -0
- openmd/adapters/pdf/__init__.py +11 -0
- openmd/adapters/pdf/adapter.py +153 -0
- openmd/adapters/pdf/backends/__init__.py +1 -0
- openmd/adapters/pdf/backends/markitdown.py +165 -0
- openmd/adapters/pdf/parser.py +56 -0
- openmd/adapters/qq_group/__init__.py +11 -0
- openmd/adapters/qq_group/adapter.py +289 -0
- openmd/adapters/qq_group/parsers/__init__.py +1 -0
- openmd/adapters/qq_group/parsers/html.py +60 -0
- openmd/adapters/qq_group/parsers/json.py +71 -0
- openmd/adapters/registry.py +149 -0
- openmd/adapters/screenshot/__init__.py +11 -0
- openmd/adapters/screenshot/adapter.py +154 -0
- openmd/adapters/screenshot/backends/__init__.py +1 -0
- openmd/adapters/screenshot/backends/paddle.py +15 -0
- openmd/adapters/screenshot/backends/rapidocr.py +176 -0
- openmd/adapters/screenshot/backends/tesseract.py +15 -0
- openmd/adapters/screenshot/ocr.py +84 -0
- openmd/adapters/wechat_group/__init__.py +11 -0
- openmd/adapters/wechat_group/adapter.py +160 -0
- openmd/adapters/wechat_group/wx_cli.py +268 -0
- openmd/adapters/wechat_moments/__init__.py +11 -0
- openmd/adapters/wechat_moments/adapter.py +167 -0
- openmd/adapters/wechat_moments/wx_cli.py +265 -0
- openmd/adapters/weixin/__init__.py +11 -0
- openmd/adapters/weixin/adapter.py +148 -0
- openmd/adapters/weixin/discovery.py +510 -0
- openmd/adapters/weixin/report.py +278 -0
- openmd/adapters/xiaohongshu.py +158 -0
- openmd/backends/__init__.py +1 -0
- openmd/backends/base.py +204 -0
- openmd/backends/bilibili.py +1480 -0
- openmd/backends/bilibili_wbi.py +277 -0
- openmd/backends/discourse.py +426 -0
- openmd/backends/douyin.py +868 -0
- openmd/backends/douyin_detail.py +124 -0
- openmd/backends/douyin_providers.py +602 -0
- openmd/backends/executor.py +546 -0
- openmd/backends/feishu.py +861 -0
- openmd/backends/generic_media.py +77 -0
- openmd/backends/interactive.py +60 -0
- openmd/backends/ocr.py +41 -0
- openmd/backends/opencli.py +82 -0
- openmd/backends/pdf.py +122 -0
- openmd/backends/registry.py +234 -0
- openmd/backends/screenshot.py +79 -0
- openmd/backends/video_metadata.py +167 -0
- openmd/backends/web_article.py +363 -0
- openmd/backends/wechat_group.py +35 -0
- openmd/backends/wechat_moments.py +35 -0
- openmd/backends/weixin.py +767 -0
- openmd/backends/x.py +455 -0
- openmd/backends/xiaoe.py +940 -0
- openmd/backends/xiaohongshu.py +911 -0
- openmd/backends/xiaohongshu_providers.py +876 -0
- openmd/backends/xiaoyuzhou.py +1017 -0
- openmd/backends/youtube.py +280 -0
- openmd/backends/youtube_discovery.py +1245 -0
- openmd/backends/youtube_transcript.py +213 -0
- openmd/browser_extension/background.js +231 -0
- openmd/browser_extension/icons/icon-128.png +0 -0
- openmd/browser_extension/icons/icon-16.png +0 -0
- openmd/browser_extension/icons/icon-32.png +0 -0
- openmd/browser_extension/icons/icon-48.png +0 -0
- openmd/browser_extension/icons/icon-512.png +0 -0
- openmd/browser_extension/idle.html +6 -0
- openmd/browser_extension/manifest.json +43 -0
- openmd/browser_extension/options.html +42 -0
- openmd/browser_extension/options.js +117 -0
- openmd/browser_extension/popup.css +71 -0
- openmd/browser_extension/popup.html +17 -0
- openmd/browser_extension/popup.js +59 -0
- openmd/browser_extension/vendor/opencli-bridge/LICENSE +190 -0
- openmd/browser_extension/vendor/opencli-bridge/VENDOR.md +31 -0
- openmd/browser_extension/vendor/opencli-bridge/background.js +2742 -0
- openmd/browser_host.py +503 -0
- openmd/browser_setup.py +106 -0
- openmd/cli.py +2012 -0
- openmd/console.py +32 -0
- openmd/core/__init__.py +1 -0
- openmd/core/capabilities.py +162 -0
- openmd/core/content_validation.py +323 -0
- openmd/core/coverage.py +163 -0
- openmd/core/detector.py +247 -0
- openmd/core/doctor.py +104 -0
- openmd/core/execution_policy.py +164 -0
- openmd/core/filename.py +64 -0
- openmd/core/input_validation.py +91 -0
- openmd/core/local_config.py +96 -0
- openmd/core/manifest.py +156 -0
- openmd/core/models.py +168 -0
- openmd/core/opencli_output.py +34 -0
- openmd/core/operation_contract.py +60 -0
- openmd/core/output.py +194 -0
- openmd/core/package_contract.py +335 -0
- openmd/core/package_validator.py +489 -0
- openmd/core/package_writer.py +666 -0
- openmd/core/planner.py +345 -0
- openmd/core/preferences.py +61 -0
- openmd/core/readable.py +129 -0
- openmd/core/receipt.py +285 -0
- openmd/core/renderer.py +185 -0
- openmd/core/runlog.py +208 -0
- openmd/core/secrets.py +313 -0
- openmd/doctor_view.py +68 -0
- openmd/interactive.py +453 -0
- openmd/ocr/__init__.py +1 -0
- openmd/ocr/breakers.py +84 -0
- openmd/ocr/config.py +54 -0
- openmd/ocr/documents.py +522 -0
- openmd/ocr/endpoint.py +72 -0
- openmd/ocr/layout.py +92 -0
- openmd/ocr/layout_filter.py +118 -0
- openmd/ocr/layout_worker.py +89 -0
- openmd/ocr/native_math.py +220 -0
- openmd/ocr/office.py +305 -0
- openmd/ocr/ollama.py +200 -0
- openmd/ocr/otsl.py +51 -0
- openmd/ocr/ovis-prompt.txt +1 -0
- openmd/ocr/pipeline.py +384 -0
- openmd/ocr/settings.json +25 -0
- openmd/ocr/tiling.py +631 -0
- openmd/post_video.py +154 -0
- openmd/probe.py +397 -0
- openmd/routing/__init__.py +1 -0
- openmd/routing/cards/builtin.json +620 -0
- openmd/routing/cards/defuddle.json +70 -0
- openmd/routing/cards/x-tweet-fetcher.json +71 -0
- openmd/routing/cards/xyz-dl.json +60 -0
- openmd/routing/cards/yutto.json +68 -0
- openmd/routing/entry_shim.py +71 -0
- openmd/routing/lifecycle.py +459 -0
- openmd/routing/loader.py +138 -0
- openmd/routing/providers.py +158 -0
- openmd/routing/readers.py +108 -0
- openmd/routing/routes.json +504 -0
- openmd/routing/runner.py +192 -0
- openmd/routing/view.py +40 -0
- openmd/runtime/__init__.py +1 -0
- openmd/runtime/_windows_job.py +234 -0
- openmd/runtime/asr.py +217 -0
- openmd/runtime/asr_worker.py +119 -0
- openmd/runtime/gpu_lock.py +81 -0
- openmd/runtime/gpu_quarantine.py +85 -0
- openmd/runtime/interpreters.py +407 -0
- openmd/runtime/managed_tools.py +204 -0
- openmd/runtime/models.py +202 -0
- openmd/runtime/services.py +175 -0
- openmd/runtime/tempdirs.py +227 -0
- openmd_cli-0.1.0.dist-info/METADATA +168 -0
- openmd_cli-0.1.0.dist-info/RECORD +177 -0
- openmd_cli-0.1.0.dist-info/WHEEL +5 -0
- openmd_cli-0.1.0.dist-info/entry_points.txt +2 -0
- openmd_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- openmd_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Bilibili WBI signing helpers for Openmd backends.
|
|
2
|
+
|
|
3
|
+
This module ports only the request-signing core needed by Openmd's public API
|
|
4
|
+
wrappers. It does not read browser profiles, local app tokens, or cookie stores.
|
|
5
|
+
|
|
6
|
+
WBI core reference: Bilibili-Harvest, Copyright (c) 2023 Lanbin07, MIT.
|
|
7
|
+
See THIRD_PARTY_NOTICES.md and LICENSES/Bilibili-Harvest-MIT.txt.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import posixpath
|
|
16
|
+
import re
|
|
17
|
+
import threading
|
|
18
|
+
import time
|
|
19
|
+
from pathlib import Path, PurePosixPath
|
|
20
|
+
from typing import Any, Callable, Mapping
|
|
21
|
+
from urllib.parse import urlencode, urlparse
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
BILIBILI_COOKIE_HEADER_ENV = "OPENMD_BILIBILI_COOKIE_HEADER"
|
|
25
|
+
BILIBILI_COOKIE_FILE_ENV = "OPENMD_BILIBILI_COOKIE_FILE"
|
|
26
|
+
BILIBILI_NAV_URL = "https://api.bilibili.com/x/web-interface/nav"
|
|
27
|
+
BILIBILI_WEB_REFERER = "https://www.bilibili.com/"
|
|
28
|
+
BILIBILI_WBI_KEY_TTL_SECONDS = 12 * 60 * 60
|
|
29
|
+
|
|
30
|
+
MIXIN_KEY_ENC_TAB: tuple[int, ...] = (
|
|
31
|
+
46,
|
|
32
|
+
47,
|
|
33
|
+
18,
|
|
34
|
+
2,
|
|
35
|
+
53,
|
|
36
|
+
8,
|
|
37
|
+
23,
|
|
38
|
+
32,
|
|
39
|
+
15,
|
|
40
|
+
50,
|
|
41
|
+
10,
|
|
42
|
+
31,
|
|
43
|
+
58,
|
|
44
|
+
3,
|
|
45
|
+
45,
|
|
46
|
+
35,
|
|
47
|
+
27,
|
|
48
|
+
43,
|
|
49
|
+
5,
|
|
50
|
+
49,
|
|
51
|
+
33,
|
|
52
|
+
9,
|
|
53
|
+
42,
|
|
54
|
+
19,
|
|
55
|
+
29,
|
|
56
|
+
28,
|
|
57
|
+
14,
|
|
58
|
+
39,
|
|
59
|
+
12,
|
|
60
|
+
38,
|
|
61
|
+
41,
|
|
62
|
+
13,
|
|
63
|
+
37,
|
|
64
|
+
48,
|
|
65
|
+
7,
|
|
66
|
+
16,
|
|
67
|
+
24,
|
|
68
|
+
55,
|
|
69
|
+
40,
|
|
70
|
+
61,
|
|
71
|
+
26,
|
|
72
|
+
17,
|
|
73
|
+
0,
|
|
74
|
+
1,
|
|
75
|
+
60,
|
|
76
|
+
51,
|
|
77
|
+
30,
|
|
78
|
+
4,
|
|
79
|
+
22,
|
|
80
|
+
25,
|
|
81
|
+
54,
|
|
82
|
+
21,
|
|
83
|
+
56,
|
|
84
|
+
59,
|
|
85
|
+
6,
|
|
86
|
+
63,
|
|
87
|
+
57,
|
|
88
|
+
62,
|
|
89
|
+
11,
|
|
90
|
+
36,
|
|
91
|
+
20,
|
|
92
|
+
34,
|
|
93
|
+
44,
|
|
94
|
+
52,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
HttpFetcher = Callable[[str, dict[str, str], int], tuple[int, str]]
|
|
98
|
+
TimeProvider = Callable[[], float]
|
|
99
|
+
|
|
100
|
+
_INVALID_WBI_VALUE = re.compile(r"[!'()*]")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class WbiSigningError(RuntimeError):
|
|
104
|
+
"""Raised when WBI keys cannot be fetched or parsed."""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def bilibili_cookie_header(environ: Mapping[str, str] | None = None) -> str | None:
|
|
108
|
+
"""Read an explicit user-owned header file, or the legacy environment value."""
|
|
109
|
+
config = os.environ if environ is None else environ
|
|
110
|
+
cookie_path = config.get(BILIBILI_COOKIE_FILE_ENV, "").strip()
|
|
111
|
+
if cookie_path:
|
|
112
|
+
try:
|
|
113
|
+
value = Path(cookie_path).read_text(encoding="utf-8-sig").strip()
|
|
114
|
+
except (OSError, UnicodeError):
|
|
115
|
+
raise WbiSigningError("The user-provided cookie header file is unreadable.") from None
|
|
116
|
+
if "\r" in value or "\n" in value:
|
|
117
|
+
raise WbiSigningError("The user-provided cookie header must be a single line.")
|
|
118
|
+
return value or None
|
|
119
|
+
value = config.get(BILIBILI_COOKIE_HEADER_ENV, "").strip()
|
|
120
|
+
return value or None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def bilibili_cookie_header_for_url(
|
|
124
|
+
url: str,
|
|
125
|
+
environ: Mapping[str, str] | None = None,
|
|
126
|
+
) -> str | None:
|
|
127
|
+
"""Return env cookie only for Bilibili-owned API/page hosts."""
|
|
128
|
+
host = (urlparse(url).hostname or "").lower()
|
|
129
|
+
if host == "bilibili.com" or host.endswith(".bilibili.com"):
|
|
130
|
+
return bilibili_cookie_header(environ)
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def bilibili_headers(
|
|
135
|
+
*,
|
|
136
|
+
cookie_header: str | None = None,
|
|
137
|
+
referer: str = BILIBILI_WEB_REFERER,
|
|
138
|
+
) -> dict[str, str]:
|
|
139
|
+
"""Build browser-like Bilibili headers without logging their contents."""
|
|
140
|
+
headers = {
|
|
141
|
+
"User-Agent": (
|
|
142
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
143
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
144
|
+
"Chrome/120.0.0.0 Safari/537.36"
|
|
145
|
+
),
|
|
146
|
+
"Referer": referer,
|
|
147
|
+
"Origin": "https://www.bilibili.com",
|
|
148
|
+
"Accept": "application/json, text/plain, */*",
|
|
149
|
+
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
|
150
|
+
"X-Wbi-UA": "Win32.Chrome.109.0.0.0",
|
|
151
|
+
}
|
|
152
|
+
if cookie_header:
|
|
153
|
+
headers["Cookie"] = cookie_header.strip()
|
|
154
|
+
return headers
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class BilibiliWbiSigner:
|
|
158
|
+
"""Fetch, cache, and apply Bilibili WBI keys for signed endpoints."""
|
|
159
|
+
|
|
160
|
+
def __init__(
|
|
161
|
+
self,
|
|
162
|
+
*,
|
|
163
|
+
fetcher: HttpFetcher,
|
|
164
|
+
timeout: int = 30,
|
|
165
|
+
ttl_seconds: int = BILIBILI_WBI_KEY_TTL_SECONDS,
|
|
166
|
+
time_provider: TimeProvider | None = None,
|
|
167
|
+
) -> None:
|
|
168
|
+
self._fetcher = fetcher
|
|
169
|
+
self._timeout = timeout
|
|
170
|
+
self._ttl_seconds = ttl_seconds
|
|
171
|
+
self._time_provider = time_provider or time.time
|
|
172
|
+
self._lock = threading.Lock()
|
|
173
|
+
self._cached_img_key = ""
|
|
174
|
+
self._cached_sub_key = ""
|
|
175
|
+
self._cached_at = 0.0
|
|
176
|
+
|
|
177
|
+
def get_keys(
|
|
178
|
+
self,
|
|
179
|
+
*,
|
|
180
|
+
force_refresh: bool = False,
|
|
181
|
+
timeout: int | None = None,
|
|
182
|
+
) -> tuple[str, str]:
|
|
183
|
+
now = self._time_provider()
|
|
184
|
+
with self._lock:
|
|
185
|
+
if not force_refresh and self._is_cache_valid(now):
|
|
186
|
+
return self._cached_img_key, self._cached_sub_key
|
|
187
|
+
|
|
188
|
+
img_key, sub_key = self._refresh_keys(timeout=timeout or self._timeout)
|
|
189
|
+
self._cached_img_key = img_key
|
|
190
|
+
self._cached_sub_key = sub_key
|
|
191
|
+
self._cached_at = now
|
|
192
|
+
return img_key, sub_key
|
|
193
|
+
|
|
194
|
+
def sign_params(
|
|
195
|
+
self,
|
|
196
|
+
params: Mapping[str, Any] | None,
|
|
197
|
+
*,
|
|
198
|
+
force_refresh: bool = False,
|
|
199
|
+
timeout: int | None = None,
|
|
200
|
+
) -> dict[str, str]:
|
|
201
|
+
img_key, sub_key = self.get_keys(force_refresh=force_refresh, timeout=timeout)
|
|
202
|
+
return sign_wbi_params_with_keys(
|
|
203
|
+
params,
|
|
204
|
+
img_key=img_key,
|
|
205
|
+
sub_key=sub_key,
|
|
206
|
+
wts=int(self._time_provider()),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def _is_cache_valid(self, now: float) -> bool:
|
|
210
|
+
if not self._cached_img_key or not self._cached_sub_key:
|
|
211
|
+
return False
|
|
212
|
+
return now - self._cached_at <= self._ttl_seconds
|
|
213
|
+
|
|
214
|
+
def _refresh_keys(self, *, timeout: int) -> tuple[str, str]:
|
|
215
|
+
status, body = self._fetcher(BILIBILI_NAV_URL, {}, timeout)
|
|
216
|
+
if status < 200 or status >= 300:
|
|
217
|
+
raise WbiSigningError(f"Bilibili WBI nav request failed with HTTP {status}.")
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
payload = json.loads(body)
|
|
221
|
+
except json.JSONDecodeError as exc:
|
|
222
|
+
raise WbiSigningError("Bilibili WBI nav response was not valid JSON.") from exc
|
|
223
|
+
if not isinstance(payload, dict):
|
|
224
|
+
raise WbiSigningError("Bilibili WBI nav response was not a JSON object.")
|
|
225
|
+
|
|
226
|
+
code = payload.get("code", 0)
|
|
227
|
+
if code not in {0, "0", None}:
|
|
228
|
+
raise WbiSigningError(f"Bilibili WBI nav returned code {code}.")
|
|
229
|
+
data = payload.get("data")
|
|
230
|
+
if not isinstance(data, dict):
|
|
231
|
+
raise WbiSigningError("Bilibili WBI nav response did not include data.")
|
|
232
|
+
wbi_img = data.get("wbi_img")
|
|
233
|
+
if not isinstance(wbi_img, dict):
|
|
234
|
+
raise WbiSigningError("Bilibili WBI nav response did not include wbi_img.")
|
|
235
|
+
|
|
236
|
+
img_key = _key_from_asset_url(str(wbi_img.get("img_url") or ""))
|
|
237
|
+
sub_key = _key_from_asset_url(str(wbi_img.get("sub_url") or ""))
|
|
238
|
+
if not img_key or not sub_key:
|
|
239
|
+
raise WbiSigningError("Bilibili WBI nav response did not include usable keys.")
|
|
240
|
+
return img_key, sub_key
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def sign_wbi_params_with_keys(
|
|
244
|
+
params: Mapping[str, Any] | None,
|
|
245
|
+
*,
|
|
246
|
+
img_key: str,
|
|
247
|
+
sub_key: str,
|
|
248
|
+
wts: int | str | None = None,
|
|
249
|
+
) -> dict[str, str]:
|
|
250
|
+
"""Sign params with already-known WBI keys; useful for deterministic tests."""
|
|
251
|
+
signed = {
|
|
252
|
+
str(key): _normalize_wbi_value(value)
|
|
253
|
+
for key, value in (params or {}).items()
|
|
254
|
+
if value is not None
|
|
255
|
+
}
|
|
256
|
+
signed["wts"] = str(int(time.time() if wts is None else wts))
|
|
257
|
+
query = urlencode(sorted(signed.items()), safe="")
|
|
258
|
+
signed["w_rid"] = hashlib.md5((query + _mixin_key(img_key, sub_key)).encode("utf-8")).hexdigest()
|
|
259
|
+
return signed
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _mixin_key(img_key: str, sub_key: str) -> str:
|
|
263
|
+
source = f"{img_key}{sub_key}"
|
|
264
|
+
mixed = "".join(source[index] for index in MIXIN_KEY_ENC_TAB if index < len(source))
|
|
265
|
+
return mixed[:32]
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _normalize_wbi_value(value: Any) -> str:
|
|
269
|
+
return _INVALID_WBI_VALUE.sub("", str(value))
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _key_from_asset_url(value: str) -> str:
|
|
273
|
+
parsed = urlparse(value.strip())
|
|
274
|
+
filename = posixpath.basename(parsed.path) or PurePosixPath(value).name
|
|
275
|
+
if "." in filename:
|
|
276
|
+
return filename.rsplit(".", 1)[0]
|
|
277
|
+
return filename
|