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.
Files changed (177) hide show
  1. openmd/__init__.py +3 -0
  2. openmd/__main__.py +5 -0
  3. openmd/adapters/__init__.py +1 -0
  4. openmd/adapters/base.py +95 -0
  5. openmd/adapters/bilibili/__init__.py +11 -0
  6. openmd/adapters/bilibili/adapter.py +104 -0
  7. openmd/adapters/bilibili/discovery.py +33 -0
  8. openmd/adapters/douyin/__init__.py +11 -0
  9. openmd/adapters/douyin/adapter.py +117 -0
  10. openmd/adapters/douyin/discovery.py +36 -0
  11. openmd/adapters/douyin/sidecar.py +77 -0
  12. openmd/adapters/generic_media/__init__.py +14 -0
  13. openmd/adapters/high_privacy.py +77 -0
  14. openmd/adapters/local_audio/__init__.py +14 -0
  15. openmd/adapters/local_audio/adapter.py +169 -0
  16. openmd/adapters/local_audio/asr.py +58 -0
  17. openmd/adapters/local_audio/backends/__init__.py +1 -0
  18. openmd/adapters/local_audio/backends/auto.py +177 -0
  19. openmd/adapters/local_audio/backends/faster_whisper.py +285 -0
  20. openmd/adapters/local_audio/backends/funasr_sense_voice.py +14 -0
  21. openmd/adapters/local_audio/backends/qwen3_asr.py +254 -0
  22. openmd/adapters/opencli.py +770 -0
  23. openmd/adapters/pdf/__init__.py +11 -0
  24. openmd/adapters/pdf/adapter.py +153 -0
  25. openmd/adapters/pdf/backends/__init__.py +1 -0
  26. openmd/adapters/pdf/backends/markitdown.py +165 -0
  27. openmd/adapters/pdf/parser.py +56 -0
  28. openmd/adapters/qq_group/__init__.py +11 -0
  29. openmd/adapters/qq_group/adapter.py +289 -0
  30. openmd/adapters/qq_group/parsers/__init__.py +1 -0
  31. openmd/adapters/qq_group/parsers/html.py +60 -0
  32. openmd/adapters/qq_group/parsers/json.py +71 -0
  33. openmd/adapters/registry.py +149 -0
  34. openmd/adapters/screenshot/__init__.py +11 -0
  35. openmd/adapters/screenshot/adapter.py +154 -0
  36. openmd/adapters/screenshot/backends/__init__.py +1 -0
  37. openmd/adapters/screenshot/backends/paddle.py +15 -0
  38. openmd/adapters/screenshot/backends/rapidocr.py +176 -0
  39. openmd/adapters/screenshot/backends/tesseract.py +15 -0
  40. openmd/adapters/screenshot/ocr.py +84 -0
  41. openmd/adapters/wechat_group/__init__.py +11 -0
  42. openmd/adapters/wechat_group/adapter.py +160 -0
  43. openmd/adapters/wechat_group/wx_cli.py +268 -0
  44. openmd/adapters/wechat_moments/__init__.py +11 -0
  45. openmd/adapters/wechat_moments/adapter.py +167 -0
  46. openmd/adapters/wechat_moments/wx_cli.py +265 -0
  47. openmd/adapters/weixin/__init__.py +11 -0
  48. openmd/adapters/weixin/adapter.py +148 -0
  49. openmd/adapters/weixin/discovery.py +510 -0
  50. openmd/adapters/weixin/report.py +278 -0
  51. openmd/adapters/xiaohongshu.py +158 -0
  52. openmd/backends/__init__.py +1 -0
  53. openmd/backends/base.py +204 -0
  54. openmd/backends/bilibili.py +1480 -0
  55. openmd/backends/bilibili_wbi.py +277 -0
  56. openmd/backends/discourse.py +426 -0
  57. openmd/backends/douyin.py +868 -0
  58. openmd/backends/douyin_detail.py +124 -0
  59. openmd/backends/douyin_providers.py +602 -0
  60. openmd/backends/executor.py +546 -0
  61. openmd/backends/feishu.py +861 -0
  62. openmd/backends/generic_media.py +77 -0
  63. openmd/backends/interactive.py +60 -0
  64. openmd/backends/ocr.py +41 -0
  65. openmd/backends/opencli.py +82 -0
  66. openmd/backends/pdf.py +122 -0
  67. openmd/backends/registry.py +234 -0
  68. openmd/backends/screenshot.py +79 -0
  69. openmd/backends/video_metadata.py +167 -0
  70. openmd/backends/web_article.py +363 -0
  71. openmd/backends/wechat_group.py +35 -0
  72. openmd/backends/wechat_moments.py +35 -0
  73. openmd/backends/weixin.py +767 -0
  74. openmd/backends/x.py +455 -0
  75. openmd/backends/xiaoe.py +940 -0
  76. openmd/backends/xiaohongshu.py +911 -0
  77. openmd/backends/xiaohongshu_providers.py +876 -0
  78. openmd/backends/xiaoyuzhou.py +1017 -0
  79. openmd/backends/youtube.py +280 -0
  80. openmd/backends/youtube_discovery.py +1245 -0
  81. openmd/backends/youtube_transcript.py +213 -0
  82. openmd/browser_extension/background.js +231 -0
  83. openmd/browser_extension/icons/icon-128.png +0 -0
  84. openmd/browser_extension/icons/icon-16.png +0 -0
  85. openmd/browser_extension/icons/icon-32.png +0 -0
  86. openmd/browser_extension/icons/icon-48.png +0 -0
  87. openmd/browser_extension/icons/icon-512.png +0 -0
  88. openmd/browser_extension/idle.html +6 -0
  89. openmd/browser_extension/manifest.json +43 -0
  90. openmd/browser_extension/options.html +42 -0
  91. openmd/browser_extension/options.js +117 -0
  92. openmd/browser_extension/popup.css +71 -0
  93. openmd/browser_extension/popup.html +17 -0
  94. openmd/browser_extension/popup.js +59 -0
  95. openmd/browser_extension/vendor/opencli-bridge/LICENSE +190 -0
  96. openmd/browser_extension/vendor/opencli-bridge/VENDOR.md +31 -0
  97. openmd/browser_extension/vendor/opencli-bridge/background.js +2742 -0
  98. openmd/browser_host.py +503 -0
  99. openmd/browser_setup.py +106 -0
  100. openmd/cli.py +2012 -0
  101. openmd/console.py +32 -0
  102. openmd/core/__init__.py +1 -0
  103. openmd/core/capabilities.py +162 -0
  104. openmd/core/content_validation.py +323 -0
  105. openmd/core/coverage.py +163 -0
  106. openmd/core/detector.py +247 -0
  107. openmd/core/doctor.py +104 -0
  108. openmd/core/execution_policy.py +164 -0
  109. openmd/core/filename.py +64 -0
  110. openmd/core/input_validation.py +91 -0
  111. openmd/core/local_config.py +96 -0
  112. openmd/core/manifest.py +156 -0
  113. openmd/core/models.py +168 -0
  114. openmd/core/opencli_output.py +34 -0
  115. openmd/core/operation_contract.py +60 -0
  116. openmd/core/output.py +194 -0
  117. openmd/core/package_contract.py +335 -0
  118. openmd/core/package_validator.py +489 -0
  119. openmd/core/package_writer.py +666 -0
  120. openmd/core/planner.py +345 -0
  121. openmd/core/preferences.py +61 -0
  122. openmd/core/readable.py +129 -0
  123. openmd/core/receipt.py +285 -0
  124. openmd/core/renderer.py +185 -0
  125. openmd/core/runlog.py +208 -0
  126. openmd/core/secrets.py +313 -0
  127. openmd/doctor_view.py +68 -0
  128. openmd/interactive.py +453 -0
  129. openmd/ocr/__init__.py +1 -0
  130. openmd/ocr/breakers.py +84 -0
  131. openmd/ocr/config.py +54 -0
  132. openmd/ocr/documents.py +522 -0
  133. openmd/ocr/endpoint.py +72 -0
  134. openmd/ocr/layout.py +92 -0
  135. openmd/ocr/layout_filter.py +118 -0
  136. openmd/ocr/layout_worker.py +89 -0
  137. openmd/ocr/native_math.py +220 -0
  138. openmd/ocr/office.py +305 -0
  139. openmd/ocr/ollama.py +200 -0
  140. openmd/ocr/otsl.py +51 -0
  141. openmd/ocr/ovis-prompt.txt +1 -0
  142. openmd/ocr/pipeline.py +384 -0
  143. openmd/ocr/settings.json +25 -0
  144. openmd/ocr/tiling.py +631 -0
  145. openmd/post_video.py +154 -0
  146. openmd/probe.py +397 -0
  147. openmd/routing/__init__.py +1 -0
  148. openmd/routing/cards/builtin.json +620 -0
  149. openmd/routing/cards/defuddle.json +70 -0
  150. openmd/routing/cards/x-tweet-fetcher.json +71 -0
  151. openmd/routing/cards/xyz-dl.json +60 -0
  152. openmd/routing/cards/yutto.json +68 -0
  153. openmd/routing/entry_shim.py +71 -0
  154. openmd/routing/lifecycle.py +459 -0
  155. openmd/routing/loader.py +138 -0
  156. openmd/routing/providers.py +158 -0
  157. openmd/routing/readers.py +108 -0
  158. openmd/routing/routes.json +504 -0
  159. openmd/routing/runner.py +192 -0
  160. openmd/routing/view.py +40 -0
  161. openmd/runtime/__init__.py +1 -0
  162. openmd/runtime/_windows_job.py +234 -0
  163. openmd/runtime/asr.py +217 -0
  164. openmd/runtime/asr_worker.py +119 -0
  165. openmd/runtime/gpu_lock.py +81 -0
  166. openmd/runtime/gpu_quarantine.py +85 -0
  167. openmd/runtime/interpreters.py +407 -0
  168. openmd/runtime/managed_tools.py +204 -0
  169. openmd/runtime/models.py +202 -0
  170. openmd/runtime/services.py +175 -0
  171. openmd/runtime/tempdirs.py +227 -0
  172. openmd_cli-0.1.0.dist-info/METADATA +168 -0
  173. openmd_cli-0.1.0.dist-info/RECORD +177 -0
  174. openmd_cli-0.1.0.dist-info/WHEEL +5 -0
  175. openmd_cli-0.1.0.dist-info/entry_points.txt +2 -0
  176. openmd_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  177. 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