@danhachuel/thunderbolt 0.6.50 → 0.6.51

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.
@@ -238,12 +238,55 @@ def _extract_profile_user(payload: Any) -> Mapping[str, Any] | None:
238
238
  return next((candidate for candidate in candidates if isinstance(candidate, Mapping)), None)
239
239
 
240
240
 
241
- def _fetch_profile_with_playwright(username: str, headers: Mapping[str, str], cookies: Mapping[str, str]) -> dict[str, Any] | None:
242
- # CORREÇÃO WINDOWS: este helper é o único caminho de rede usado pelo perfil no Windows.
243
- endpoints = (
244
- f'https://www.instagram.com/api/v1/users/web_profile_info/?username={username}',
245
- f'https://i.instagram.com/api/v1/users/web_profile_info/?username={username}',
241
+ def _shared_data_documents(document: str) -> list[Any]:
242
+ """Extract JSON assigned to window._sharedData from the rendered profile HTML."""
243
+ decoder = json.JSONDecoder()
244
+ documents: list[Any] = []
245
+ patterns = (
246
+ r'window\s*\.\s*_sharedData\s*=\s*',
247
+ r'window\s*\[\s*["\']_sharedData["\']\s*\]\s*=\s*',
246
248
  )
249
+ for pattern in patterns:
250
+ for match in re.finditer(pattern, document, flags=re.IGNORECASE):
251
+ start = document.find('{', match.end())
252
+ if start < 0:
253
+ continue
254
+ try:
255
+ parsed, _ = decoder.raw_decode(document[start:])
256
+ except json.JSONDecodeError:
257
+ continue
258
+ documents.append(parsed)
259
+ return documents
260
+
261
+
262
+ def _profile_users_from_document(document: str) -> list[Mapping[str, Any]]:
263
+ """Return users found in legacy _sharedData and current embedded profile payloads."""
264
+ users: list[Mapping[str, Any]] = []
265
+ documents = _shared_data_documents(document) + _embedded_json_documents(document)
266
+ for payload in documents:
267
+ direct = _extract_profile_user(payload)
268
+ if isinstance(direct, Mapping):
269
+ users.append(direct)
270
+ for node in _walk_json(payload):
271
+ if not isinstance(node, Mapping):
272
+ continue
273
+ candidate = node.get('user')
274
+ if isinstance(candidate, Mapping) and any(key in candidate for key in ('biography', 'edge_follow', 'edge_followed_by', 'username')):
275
+ users.append(candidate)
276
+ unique: list[Mapping[str, Any]] = []
277
+ seen: set[int] = set()
278
+ for user in users:
279
+ marker = id(user)
280
+ if marker not in seen:
281
+ seen.add(marker)
282
+ unique.append(user)
283
+ return unique
284
+
285
+
286
+ def _fetch_profile_with_playwright(username: str, headers: Mapping[str, str] | None = None, cookies: Mapping[str, str] | None = None) -> dict[str, Any] | None:
287
+ """Fetch the public profile page and read its embedded structured profile data."""
288
+ headers = headers or _instagram_headers()
289
+ cookies = cookies or _instagram_cookies()
247
290
  best_user: dict[str, Any] | None = None
248
291
  try:
249
292
  from playwright.sync_api import sync_playwright
@@ -256,20 +299,15 @@ def _fetch_profile_with_playwright(username: str, headers: Mapping[str, str], co
256
299
  )
257
300
  if cookies:
258
301
  page.context.add_cookies([{'name': name, 'value': value, 'domain': '.instagram.com', 'path': '/'} for name, value in cookies.items()])
259
- for endpoint in endpoints:
260
- try:
261
- response = page.goto(endpoint, wait_until='domcontentloaded', timeout=30000)
262
- LOGGER.debug('Instagram Playwright profile URL=%s status=%s', endpoint, response.status if response else None)
263
- if response is None or response.status >= 400:
264
- continue
265
- payload = json.loads(page.locator('body').inner_text(timeout=5000))
266
- user = _extract_profile_user(payload)
267
- if isinstance(user, dict):
268
- best_user = _merge_profile(best_user, user)
269
- if _profile_quality(best_user) >= 4:
270
- return best_user
271
- except (ValueError, TimeoutError) as exc:
272
- LOGGER.warning('Falha ao interpretar resposta Playwright do perfil URL=%s: %s', endpoint, exc)
302
+ url = f'https://www.instagram.com/{username}/'
303
+ response = page.goto(url, wait_until='domcontentloaded', timeout=30000)
304
+ LOGGER.debug('Instagram Playwright profile URL=%s status=%s', url, response.status if response else None)
305
+ if response is None or response.status >= 400:
306
+ return None
307
+ document = page.content()
308
+ for user in _profile_users_from_document(document):
309
+ best_user = _merge_profile(best_user, user)
310
+ LOGGER.debug('Instagram Playwright _sharedData profile user=%s quality=%s', bool(best_user), _profile_quality(best_user or {}))
273
311
  finally:
274
312
  browser.close()
275
313
  except Exception as exc:
@@ -283,13 +321,14 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
283
321
  cookies = _instagram_cookies()
284
322
  headers = _instagram_headers()
285
323
  headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36'
324
+ if platform.system() == 'Windows':
325
+ LOGGER.debug('Windows detectado: perfil Instagram exclusivamente via Playwright.')
326
+ return _fetch_profile_with_playwright(username, headers, cookies)
327
+
286
328
  endpoints = (
287
329
  f'https://www.instagram.com/api/v1/users/web_profile_info/?username={username}',
288
330
  f'https://i.instagram.com/api/v1/users/web_profile_info/?username={username}',
289
331
  )
290
- if platform.system() == 'Windows':
291
- LOGGER.debug('Windows detectado: perfil Instagram exclusivamente via Playwright.')
292
- return _fetch_profile_with_playwright(username, headers, cookies)
293
332
 
294
333
  request_headers = [
295
334
  headers,
@@ -516,6 +555,8 @@ def fetch_public_instagram_profile(source: str) -> IntegrationResult:
516
555
  api_user = _fetch_web_profile_user(reference['username'])
517
556
  if api_user:
518
557
  return IntegrationResult(True, 'Perfil Instagram encontrado publicamente.', _profile_data_from_api(api_user, reference))
558
+ if platform.system() == 'Windows':
559
+ return IntegrationResult(False, 'Não foi possível extrair os dados públicos do Instagram com o Playwright.', reference)
519
560
  try:
520
561
  response = requests.get(reference['url'], headers={'User-Agent': 'Mozilla/5.0', 'Accept-Language': 'pt-BR,pt;q=0.9,en;q=0.8'}, timeout=12)
521
562
  except requests.RequestException as exc:
@@ -609,10 +650,11 @@ def _post_from_node(node: dict[str, Any]) -> dict[str, Any] | None:
609
650
  }
610
651
 
611
652
 
612
- def _fetch_posts_with_playwright(reference: Mapping[str, str], limit: int, cookies: Mapping[str, str]) -> list[dict[str, Any]]:
653
+ def _fetch_posts_with_playwright(reference: Mapping[str, str], limit: int, cookies: Mapping[str, str] | None = None) -> list[dict[str, Any]]:
613
654
  # CORREÇÃO WINDOWS: posts são extraídos exclusivamente do JSON/HTML obtido pelo Playwright.
614
655
  posts: list[dict[str, Any]] = []
615
656
  seen: set[str] = set()
657
+ cookies = cookies or _instagram_cookies()
616
658
  try:
617
659
  from playwright.sync_api import sync_playwright
618
660
  with sync_playwright() as playwright:
@@ -630,7 +672,8 @@ def _fetch_posts_with_playwright(reference: Mapping[str, str], limit: int, cooki
630
672
  return []
631
673
  browser_document = page.content()
632
674
  LOGGER.debug('Instagram Playwright posts document_bytes=%s', len(browser_document))
633
- for document in _embedded_json_documents(browser_document):
675
+ documents = _shared_data_documents(browser_document) + _embedded_json_documents(browser_document)
676
+ for document in documents:
634
677
  for node in _walk_json(document):
635
678
  post = _post_from_node(node)
636
679
  if not post or post['id'] in seen:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.6.50",
3
+ "version": "0.6.51",
4
4
  "description": "Thunderbolt — interface local para operação de canais faceless e motor MoneyPrinterTurbo",
5
5
  "license": "MIT",
6
6
  "main": "scripts/cli.mjs",