@danhachuel/thunderbolt 0.6.51 → 0.6.52

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.
@@ -284,12 +284,12 @@ def _profile_users_from_document(document: str) -> list[Mapping[str, Any]]:
284
284
 
285
285
 
286
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."""
287
+ """Fetch the structured profile JSON requested by the public Instagram page."""
288
288
  headers = headers or _instagram_headers()
289
289
  cookies = cookies or _instagram_cookies()
290
- best_user: dict[str, Any] | None = None
290
+ captured_user: dict[str, Any] | None = None
291
291
  try:
292
- from playwright.sync_api import sync_playwright
292
+ from playwright.sync_api import TimeoutError as PlaywrightTimeoutError, sync_playwright
293
293
  with sync_playwright() as playwright:
294
294
  browser = playwright.chromium.launch(headless=True)
295
295
  try:
@@ -299,21 +299,37 @@ def _fetch_profile_with_playwright(username: str, headers: Mapping[str, str] | N
299
299
  )
300
300
  if cookies:
301
301
  page.context.add_cookies([{'name': name, 'value': value, 'domain': '.instagram.com', 'path': '/'} for name, value in cookies.items()])
302
+
303
+ def capture_profile_response(response: Any) -> None:
304
+ nonlocal captured_user
305
+ if '/api/v1/users/web_profile_info/' not in response.url:
306
+ return
307
+ try:
308
+ payload = response.json()
309
+ user = ((payload.get('data') or {}).get('user') if isinstance(payload, Mapping) else None)
310
+ if isinstance(user, Mapping):
311
+ captured_user = _merge_profile(captured_user, user)
312
+ LOGGER.debug('Instagram Playwright API profile captured user=%s', username)
313
+ except (ValueError, TypeError, AttributeError) as exc:
314
+ LOGGER.debug('Não foi possível interpretar resposta web_profile_info: %s', exc)
315
+
316
+ page.on('response', capture_profile_response)
302
317
  url = f'https://www.instagram.com/{username}/'
303
318
  response = page.goto(url, wait_until='domcontentloaded', timeout=30000)
304
319
  LOGGER.debug('Instagram Playwright profile URL=%s status=%s', url, response.status if response else None)
305
320
  if response is None or response.status >= 400:
306
321
  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 {}))
322
+ try:
323
+ page.wait_for_load_state('networkidle', timeout=10000)
324
+ except (PlaywrightTimeoutError, TimeoutError):
325
+ LOGGER.debug('Instagram Playwright networkidle excedido; usando resposta API já capturada.')
326
+ LOGGER.debug('Instagram Playwright API profile user=%s quality=%s', bool(captured_user), _profile_quality(captured_user or {}))
311
327
  finally:
312
328
  browser.close()
313
329
  except Exception as exc:
314
330
  LOGGER.warning('Falha no Playwright do perfil Instagram: %s', exc, exc_info=True)
315
331
  _streamlit_message('error', f'Playwright não conseguiu consultar o perfil Instagram: {exc}', 'profile_playwright_error')
316
- return best_user
332
+ return captured_user
317
333
 
318
334
 
319
335
  def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
@@ -651,12 +667,12 @@ def _post_from_node(node: dict[str, Any]) -> dict[str, Any] | None:
651
667
 
652
668
 
653
669
  def _fetch_posts_with_playwright(reference: Mapping[str, str], limit: int, cookies: Mapping[str, str] | None = None) -> list[dict[str, Any]]:
654
- # CORREÇÃO WINDOWS: posts são extraídos exclusivamente do JSON/HTML obtido pelo Playwright.
670
+ # CORREÇÃO WINDOWS: posts são extraídos exclusivamente da resposta API capturada pelo Playwright.
655
671
  posts: list[dict[str, Any]] = []
656
672
  seen: set[str] = set()
657
673
  cookies = cookies or _instagram_cookies()
658
674
  try:
659
- from playwright.sync_api import sync_playwright
675
+ from playwright.sync_api import TimeoutError as PlaywrightTimeoutError, sync_playwright
660
676
  with sync_playwright() as playwright:
661
677
  browser = playwright.chromium.launch(headless=True)
662
678
  try:
@@ -666,23 +682,37 @@ def _fetch_posts_with_playwright(reference: Mapping[str, str], limit: int, cooki
666
682
  )
667
683
  if cookies:
668
684
  page.context.add_cookies([{'name': name, 'value': value, 'domain': '.instagram.com', 'path': '/'} for name, value in cookies.items()])
685
+
686
+ def capture_profile_posts(response: Any) -> None:
687
+ if '/api/v1/users/web_profile_info/' not in response.url:
688
+ return
689
+ try:
690
+ payload = response.json()
691
+ user = ((payload.get('data') or {}).get('user') if isinstance(payload, Mapping) else None)
692
+ media = user.get('edge_owner_to_timeline_media') if isinstance(user, Mapping) else None
693
+ edges = media.get('edges') if isinstance(media, Mapping) else []
694
+ for edge in edges or []:
695
+ node = edge.get('node') if isinstance(edge, Mapping) else None
696
+ if not isinstance(node, dict):
697
+ continue
698
+ post = _post_from_node(node)
699
+ if post and post['id'] not in seen:
700
+ seen.add(post['id'])
701
+ posts.append(post)
702
+ except (ValueError, TypeError, AttributeError) as exc:
703
+ LOGGER.debug('Não foi possível interpretar posts web_profile_info: %s', exc)
704
+
705
+ page.on('response', capture_profile_posts)
669
706
  response = page.goto(reference['url'], wait_until='domcontentloaded', timeout=30000)
670
707
  LOGGER.debug('Instagram Playwright posts URL=%s status=%s', reference['url'], response.status if response else None)
671
708
  if response is None or response.status >= 400:
672
709
  return []
673
- browser_document = page.content()
674
- LOGGER.debug('Instagram Playwright posts document_bytes=%s', len(browser_document))
675
- documents = _shared_data_documents(browser_document) + _embedded_json_documents(browser_document)
676
- for document in documents:
677
- for node in _walk_json(document):
678
- post = _post_from_node(node)
679
- if not post or post['id'] in seen:
680
- continue
681
- seen.add(post['id'])
682
- posts.append(post)
683
- LOGGER.debug('Instagram Playwright post extracted id=%s', post['id'])
684
- if len(posts) >= max(1, int(limit)):
685
- return posts
710
+ try:
711
+ page.wait_for_load_state('networkidle', timeout=10000)
712
+ except (PlaywrightTimeoutError, TimeoutError):
713
+ LOGGER.debug('Instagram Playwright networkidle excedido; usando posts API já capturados.')
714
+ LOGGER.debug('Instagram Playwright API posts extracted=%s limit=%s', len(posts), limit)
715
+ return posts[:max(1, int(limit))]
686
716
  finally:
687
717
  browser.close()
688
718
  except Exception as exc:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.6.51",
3
+ "version": "0.6.52",
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",