@danhachuel/thunderbolt 0.6.56 → 0.6.57

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.
@@ -25,7 +25,6 @@ except ImportError: # pragma: no cover - Streamlit is a runtime dependency
25
25
 
26
26
  LOGGER = logging.getLogger(__name__)
27
27
  _WINDOWS_PLAYWRIGHT_CHECKED = False
28
- _INSTALOADER_INSTALL_ATTEMPTED = False
29
28
 
30
29
 
31
30
  ABOUT_ACCOUNT_URL = 'https://i.instagram.com/api/v1/bloks/apps/com.instagram.interactions.about_this_account/'
@@ -126,8 +125,8 @@ def _instagram_headers() -> dict[str, str]:
126
125
 
127
126
 
128
127
  def _instagram_playwright_headless() -> bool:
129
- """Use headed mode for Windows diagnostics unless explicitly overridden."""
130
- value = os.getenv('INSTAGRAM_PLAYWRIGHT_HEADLESS', 'false').strip().casefold()
128
+ """Use headless mode by default; set INSTAGRAM_PLAYWRIGHT_HEADLESS=false for debug."""
129
+ value = os.getenv('INSTAGRAM_PLAYWRIGHT_HEADLESS', 'true').strip().casefold()
131
130
  return value not in {'0', 'false', 'no', 'off'}
132
131
 
133
132
 
@@ -142,134 +141,6 @@ def _save_instagram_debug_html(username: str, document: str) -> None:
142
141
  LOGGER.warning('Não foi possível salvar o HTML de diagnóstico Instagram: %s', exc)
143
142
 
144
143
 
145
- def _instagram_instaloader():
146
- """Load the anonymous fallback, installing it once when the requirement is missing."""
147
- global _INSTALOADER_INSTALL_ATTEMPTED
148
- try:
149
- import instaloader
150
- return instaloader
151
- except ImportError:
152
- if _INSTALOADER_INSTALL_ATTEMPTED:
153
- LOGGER.warning('Fallback instaloader indisponível; instale com: python -m pip install instaloader')
154
- return None
155
- _INSTALOADER_INSTALL_ATTEMPTED = True
156
- try:
157
- LOGGER.info('Instaloader ausente; a tentar instalar a dependência automaticamente.')
158
- completed = subprocess.run(
159
- [sys.executable, '-m', 'pip', 'install', 'instaloader>=4.14,<5'],
160
- capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=180, check=False,
161
- )
162
- if completed.returncode == 0:
163
- import importlib
164
- importlib.invalidate_caches()
165
- try:
166
- import instaloader
167
- return instaloader
168
- except ImportError:
169
- LOGGER.warning('Instaloader foi instalado, mas não ficou disponível neste interpretador.')
170
- LOGGER.warning('Instalação automática do instaloader falhou: %s', (completed.stderr or completed.stdout)[-1000:])
171
- except (OSError, subprocess.SubprocessError) as exc:
172
- LOGGER.warning('Não foi possível instalar instaloader automaticamente: %s', exc)
173
- _streamlit_message('error', 'O fallback instaloader não está disponível. Instale com: python -m pip install instaloader', 'instaloader_missing')
174
- return None
175
-
176
-
177
- def _fetch_profile_instaloader(username: str) -> dict[str, Any] | None:
178
- """Fetch a public profile anonymously through Instaloader when Playwright fails."""
179
- instaloader = _instagram_instaloader()
180
- if instaloader is None:
181
- return None
182
- try:
183
- loader = instaloader.Instaloader(
184
- quiet=True,
185
- request_timeout=30,
186
- max_connection_attempts=1,
187
- download_pictures=False,
188
- download_videos=False,
189
- download_video_thumbnails=False,
190
- save_metadata=False,
191
- )
192
- profile = instaloader.Profile.from_username(loader.context, username)
193
- LOGGER.info('Fallback instaloader activado para o perfil @%s', username)
194
- country = next(
195
- (
196
- str(getattr(profile, attribute, '') or '').strip()
197
- for attribute in ('country', 'country_name', 'country_of_registration', 'account_country', 'account_based_in')
198
- if str(getattr(profile, attribute, '') or '').strip()
199
- ),
200
- '',
201
- )
202
- return {
203
- 'id': str(getattr(profile, 'userid', '') or ''),
204
- 'username': str(getattr(profile, 'username', '') or username),
205
- 'full_name': str(getattr(profile, 'full_name', '') or ''),
206
- 'biography': str(getattr(profile, 'biography', '') or ''),
207
- 'follower_count': getattr(profile, 'follower_count', getattr(profile, 'followers', None)),
208
- 'following_count': getattr(profile, 'following_count', getattr(profile, 'followees', None)),
209
- 'edge_owner_to_timeline_media': {'count': getattr(profile, 'mediacount', None)},
210
- 'country': country,
211
- 'is_private': bool(getattr(profile, 'is_private', False)),
212
- '_instagram_source': 'instaloader',
213
- }
214
- except Exception as exc:
215
- LOGGER.warning('Fallback instaloader falhou para @%s: %s', username, exc, exc_info=True)
216
- return None
217
-
218
-
219
- def _fetch_posts_instaloader(username: str, limit: int = 10) -> list[dict[str, Any]]:
220
- """Fetch public posts anonymously through Instaloader when Playwright fails."""
221
- instaloader = _instagram_instaloader()
222
- if instaloader is None:
223
- return []
224
- posts: list[dict[str, Any]] = []
225
- try:
226
- loader = instaloader.Instaloader(
227
- quiet=True,
228
- request_timeout=30,
229
- max_connection_attempts=1,
230
- download_pictures=False,
231
- download_videos=False,
232
- download_video_thumbnails=False,
233
- save_metadata=False,
234
- )
235
- profile = instaloader.Profile.from_username(loader.context, username)
236
- for post in profile.get_posts():
237
- shortcode = str(getattr(post, 'shortcode', '') or '')
238
- if not shortcode:
239
- continue
240
- image_url = str(getattr(post, 'url', '') or '')
241
- timestamp = getattr(post, 'date_utc', '') or ''
242
- if hasattr(timestamp, 'isoformat'):
243
- timestamp = timestamp.isoformat()
244
- else:
245
- timestamp = str(timestamp)
246
- posts.append({
247
- 'id': str(getattr(post, 'mediaid', '') or shortcode),
248
- 'shortcode': shortcode,
249
- 'url': f'https://www.instagram.com/p/{shortcode}/',
250
- 'image_url': image_url,
251
- 'display_url': image_url,
252
- 'caption': str(getattr(post, 'caption', '') or '').strip(),
253
- 'published_at': timestamp,
254
- 'timestamp': timestamp,
255
- 'is_video': bool(getattr(post, 'is_video', False)),
256
- })
257
- if len(posts) >= max(1, int(limit)):
258
- break
259
- LOGGER.info('Fallback instaloader carregou %s posts para @%s', len(posts), username)
260
- except Exception as exc:
261
- LOGGER.warning('Fallback instaloader falhou nos posts de @%s: %s', username, exc, exc_info=True)
262
- return posts
263
-
264
-
265
- def fetch_profile_instaloader(username: str) -> dict[str, Any] | None:
266
- return _fetch_profile_instaloader(username)
267
-
268
-
269
- def fetch_posts_instaloader(username: str, limit: int = 10) -> list[dict[str, Any]]:
270
- return _fetch_posts_instaloader(username, limit)
271
-
272
-
273
144
  @dataclass
274
145
  class IntegrationResult:
275
146
  ok: bool
@@ -429,117 +300,103 @@ def _profile_users_from_document(document: str) -> list[Mapping[str, Any]]:
429
300
  if isinstance(candidate, Mapping) and any(key in candidate for key in ('biography', 'edge_follow', 'edge_followed_by', 'username')):
430
301
  users.append(candidate)
431
302
  unique: list[Mapping[str, Any]] = []
432
- seen: set[int] = set()
303
+ seen: set[str] = set()
433
304
  for user in users:
434
- marker = id(user)
305
+ marker = str(user.get('id') or user.get('pk') or user.get('username') or id(user))
435
306
  if marker not in seen:
436
307
  seen.add(marker)
437
308
  unique.append(user)
438
309
  return unique
439
310
 
440
311
 
441
- def _fetch_profile_with_playwright(username: str, headers: Mapping[str, str] | None = None, cookies: Mapping[str, str] | None = None) -> dict[str, Any] | None:
442
- """Fetch a public profile using browser state, API responses and embedded HTML data."""
443
- headers = headers or _instagram_headers()
444
- cookies = cookies or _instagram_cookies()
445
- captured_user: dict[str, Any] | None = None
312
+ def _extract_posts_from_documents(documents: list[Any], limit: int) -> list[dict[str, Any]]:
313
+ posts: list[dict[str, Any]] = []
314
+ seen: set[str] = set()
315
+ for document in documents:
316
+ for node in _walk_json(document):
317
+ if not isinstance(node, Mapping):
318
+ continue
319
+ post = _post_from_node(dict(node))
320
+ if not post or post['id'] in seen:
321
+ continue
322
+ seen.add(post['id'])
323
+ posts.append(post)
324
+ if len(posts) >= max(1, int(limit)):
325
+ return posts
326
+ return posts
327
+
328
+
329
+ def _extract_profile_from_html(html_content: str) -> dict[str, Any]:
330
+ """Extract the public profile and available posts from rendered Instagram HTML."""
331
+ html = str(html_content or '')
332
+ documents = _shared_data_documents(html) + _embedded_json_documents(html)
333
+ users = _profile_users_from_document(html)
334
+ user = max(users, key=_profile_quality, default={})
335
+ posts = _extract_posts_from_documents(documents, limit=10)
336
+ result = {'user': dict(user) if isinstance(user, Mapping) else {}, 'posts': posts}
337
+ LOGGER.info('Instagram HTML extraction profile=%s posts=%s', bool(result['user']), len(posts))
338
+ print(f"Playwright usado no Windows, dados extraídos com {'sucesso' if result['user'] or posts else 'falha'}")
339
+ return result
340
+
341
+
342
+ def _extract_posts_from_html(html_content: str, limit: int = 10) -> list[dict[str, Any]]:
343
+ """Extract public posts from _sharedData and JSON scripts in rendered HTML."""
344
+ html = str(html_content or '')
345
+ documents = _shared_data_documents(html) + _embedded_json_documents(html)
346
+ posts = _extract_posts_from_documents(documents, limit=max(1, int(limit)))
347
+ LOGGER.info('Instagram HTML post extraction posts=%s limit=%s', len(posts), limit)
348
+ print(f"Playwright usado no Windows, posts extraídos: {len(posts)}")
349
+ return posts
350
+
351
+
352
+ def _fetch_profile_with_playwright(username: str) -> dict[str, Any]:
353
+ """Load a public profile page and extract only its rendered HTML."""
354
+ url = f'https://www.instagram.com/{username}/'
446
355
  try:
447
356
  from playwright.sync_api import TimeoutError as PlaywrightTimeoutError, sync_playwright
448
357
  with sync_playwright() as playwright:
449
358
  browser = playwright.chromium.launch(headless=_instagram_playwright_headless())
450
359
  try:
451
- headers_for_context = {'x-ig-app-id': headers['x-ig-app-id'], 'Accept-Language': 'pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7', 'Accept': 'application/json, text/plain, */*', 'Origin': 'https://www.instagram.com', 'Referer': 'https://www.instagram.com/'}
452
- state_path = Path(__file__).resolve().parents[1] / 'storage' / 'instagram_storage_state.json'
453
- context_kwargs = {'user_agent': headers['User-Agent'], 'extra_http_headers': headers_for_context}
454
- if state_path.is_file():
455
- context_kwargs['storage_state'] = str(state_path)
456
- context = browser.new_context(**context_kwargs)
457
- if cookies:
458
- context.add_cookies([{'name': name, 'value': value, 'domain': '.instagram.com', 'path': '/'} for name, value in cookies.items()])
360
+ context = browser.new_context(
361
+ user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36',
362
+ extra_http_headers={'Accept-Language': 'pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7'},
363
+ )
459
364
  page = context.new_page()
460
-
461
- def capture_profile_response(response: Any) -> None:
462
- nonlocal captured_user
463
- response_url = str(getattr(response, 'url', '') or '')
464
- if not any(marker in response_url for marker in ('/api/v1/users/web_profile_info/', '/graphql/query')):
465
- return
466
- status = getattr(response, 'status', None)
467
- LOGGER.info('Instagram Playwright API response URL=%s status=%s', response_url, status)
468
- try:
469
- payload = response.json()
470
- user = _extract_profile_user(payload)
471
- if isinstance(user, Mapping):
472
- captured_user = _merge_profile(captured_user, user)
473
- LOGGER.info('Instagram Playwright perfil capturado URL=%s status=%s quality=%s', response_url, status, _profile_quality(captured_user))
474
- except (ValueError, TypeError, AttributeError) as exc:
475
- LOGGER.warning('Não foi possível interpretar resposta Instagram URL=%s status=%s: %s', response_url, status, exc)
476
-
477
-
478
- page.on('response', capture_profile_response)
479
- try:
480
- page.goto('https://www.instagram.com/', wait_until='domcontentloaded', timeout=30000)
481
- state_path.parent.mkdir(parents=True, exist_ok=True)
482
- context.storage_state(path=str(state_path))
483
- LOGGER.debug('Instagram Playwright página inicial carregada; storage_state salvo em %s.', state_path)
484
- except Exception as exc:
485
- LOGGER.debug('Instagram Playwright não carregou a página inicial: %s', exc)
486
- url = f'https://www.instagram.com/{username}/'
487
- response = None
488
- status = None
489
- try:
490
- response = page.goto(url, wait_until='domcontentloaded', timeout=30000)
491
- status = response.status if response else None
492
- except Exception as exc:
493
- LOGGER.warning('Instagram Playwright navegação falhou URL=%s: %s', url, exc)
494
- LOGGER.info('Instagram Playwright profile URL=%s status=%s', url, status)
495
- try:
496
- page.wait_for_timeout(5000)
497
- except Exception:
498
- pass
365
+ response = page.goto(url, wait_until='domcontentloaded', timeout=30000)
366
+ status = response.status if response else None
367
+ LOGGER.info('Instagram Playwright HTML profile URL=%s status=%s', url, status)
368
+ print(f'Playwright usado no Windows para o perfil @{username}')
369
+ page.wait_for_timeout(5000)
499
370
  try:
500
371
  page.wait_for_load_state('networkidle', timeout=10000)
501
372
  except (PlaywrightTimeoutError, TimeoutError):
502
- LOGGER.debug('Instagram Playwright networkidle excedido; usando dados já capturados.')
503
- try:
504
- document = page.content()
505
- _save_instagram_debug_html(username, document)
506
- for user in _profile_users_from_document(document):
507
- if isinstance(user, Mapping):
508
- candidate = _merge_profile(captured_user, user)
509
- if _profile_quality(candidate) >= _profile_quality(captured_user or {}):
510
- captured_user = candidate
511
- except Exception as exc:
512
- LOGGER.warning('Não foi possível guardar/analisar HTML de diagnóstico URL=%s: %s', url, exc)
513
- if response is None or status is None or status >= 400:
514
- LOGGER.warning('Instagram Playwright perfil HTTP status=%s URL=%s', status, url)
515
- return captured_user
516
- LOGGER.info('Instagram Playwright perfil final captured=%s quality=%s URL=%s', bool(captured_user), _profile_quality(captured_user or {}), url)
373
+ LOGGER.debug('Instagram Playwright networkidle excedido; usando HTML já carregado.')
374
+ html_content = page.content()
375
+ _save_instagram_debug_html(username, html_content)
376
+ extracted = _extract_profile_from_html(html_content)
377
+ user = extracted.get('user') if isinstance(extracted, Mapping) else {}
378
+ if isinstance(user, Mapping) and user:
379
+ LOGGER.info('Instagram Playwright HTML profile extracted URL=%s status=%s', url, status)
380
+ return dict(user)
381
+ return {}
517
382
  finally:
518
383
  browser.close()
519
384
  except Exception as exc:
520
- LOGGER.warning('Falha no Playwright do perfil Instagram: %s', exc, exc_info=True)
385
+ LOGGER.warning('Falha no Playwright HTML do perfil Instagram URL=%s: %s', url, exc, exc_info=True)
386
+ print(f'Playwright falhou no Windows para o perfil @{username}: {exc}')
521
387
  _streamlit_message('error', f'Playwright não conseguiu consultar o perfil Instagram: {exc}', 'profile_playwright_error')
522
- return captured_user
388
+ return {}
523
389
 
524
390
 
525
391
  def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
526
392
  _ensure_windows_playwright()
527
- cookies = _instagram_cookies()
528
393
  headers = _instagram_headers()
529
394
  headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36'
530
395
  if platform.system() == 'Windows':
531
- LOGGER.debug('Windows detectado: perfil Instagram exclusivamente via Playwright.')
532
- playwright_user = _fetch_profile_with_playwright(username, headers, cookies)
533
- if playwright_user:
534
- return playwright_user
535
- LOGGER.warning('Fallback para instaloader activado no perfil @%s.', username)
536
- fallback_user = fetch_profile_instaloader(username)
537
- if fallback_user:
538
- _streamlit_message('info', 'Dados obtidos via fallback (instaloader). O país pode não estar disponível.', 'profile_instaloader_fallback')
539
- else:
540
- _streamlit_message('error', 'Não foi possível obter os dados do Instagram. Verifique sua conexão ou tente novamente mais tarde.', 'profile_instagram_failed')
541
- return fallback_user
396
+ LOGGER.debug('Windows detectado: perfil Instagram exclusivamente via HTML Playwright sem cookies manuais.')
397
+ return _fetch_profile_with_playwright(username)
542
398
 
399
+ cookies = _instagram_cookies()
543
400
  endpoints = (
544
401
  f'https://www.instagram.com/api/v1/users/web_profile_info/?username={username}',
545
402
  f'https://i.instagram.com/api/v1/users/web_profile_info/?username={username}',
@@ -599,7 +456,7 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
599
456
  LOGGER.warning('curl não está disponível no PATH; a tentar Playwright.')
600
457
  _streamlit_message('warning', 'curl não foi encontrado no PATH do Windows; será tentado o fallback Playwright.', 'curl_missing')
601
458
 
602
- return _fetch_profile_with_playwright(username, headers, cookies) or best_user
459
+ return _fetch_profile_with_playwright(username) or best_user
603
460
 
604
461
 
605
462
  def _country_from_bloks(value: Any) -> str:
@@ -708,7 +565,9 @@ def _profile_data_from_api(user: Mapping[str, Any], reference: Mapping[str, str]
708
565
  media = media if media is not None else _profile_metric(user, 'posts', 'post_count', 'media_count')
709
566
  biography = str(user.get('biography') or user.get('bio') or user.get('description') or '').strip()
710
567
  direct_country = _country_value(user.get('country') or user.get('country_code'))
711
- country = direct_country or extract_public_instagram_country(user) or _fetch_instagram_about_country(user.get('id') or user.get('pk'))
568
+ country = direct_country or extract_public_instagram_country(user)
569
+ if platform.system() != 'Windows':
570
+ country = country or _fetch_instagram_about_country(user.get('id') or user.get('pk'))
712
571
  country = country or _country_from_bio_fallback(biography)
713
572
  LOGGER.debug('Instagram profile extracted keys=%s edge_follow=%r bio=%s followers=%s following=%s posts=%s country=%s', sorted(str(key) for key in user.keys()), following_node, bool(biography), followers, following, media, country or '—')
714
573
  return {
@@ -800,8 +659,6 @@ def fetch_public_instagram_profile(source: str) -> IntegrationResult:
800
659
  api_user = _fetch_web_profile_user(reference['username'])
801
660
  if api_user:
802
661
  data = _profile_data_from_api(api_user, reference)
803
- if api_user.get('_instagram_source') == 'instaloader':
804
- return IntegrationResult(True, 'Perfil encontrado via fallback instaloader. O país pode não estar disponível.', data)
805
662
  return IntegrationResult(True, 'Perfil Instagram encontrado publicamente.', data)
806
663
  if platform.system() == 'Windows':
807
664
  return IntegrationResult(False, 'Não foi possível obter os dados do Instagram. Verifique sua conexão ou tente novamente mais tarde.', reference)
@@ -898,66 +755,40 @@ def _post_from_node(node: dict[str, Any]) -> dict[str, Any] | None:
898
755
  }
899
756
 
900
757
 
901
- def _fetch_posts_with_playwright(reference: Mapping[str, str], limit: int, cookies: Mapping[str, str] | None = None) -> list[dict[str, Any]]:
902
- # CORREÇÃO WINDOWS: posts são extraídos exclusivamente da resposta API capturada pelo Playwright.
903
- posts: list[dict[str, Any]] = []
904
- seen: set[str] = set()
905
- cookies = cookies or _instagram_cookies()
758
+ def _fetch_posts_with_playwright(reference: Mapping[str, str], limit: int) -> list[dict[str, Any]]:
759
+ """Load a public profile page and extract posts only from its rendered HTML."""
760
+ url = str(reference.get('url') or '')
906
761
  try:
907
762
  from playwright.sync_api import TimeoutError as PlaywrightTimeoutError, sync_playwright
908
763
  with sync_playwright() as playwright:
909
764
  browser = playwright.chromium.launch(headless=_instagram_playwright_headless())
910
765
  try:
911
- state_path = Path(__file__).resolve().parents[1] / 'storage' / 'instagram_storage_state.json'
912
- headers_for_context = {'x-ig-app-id': '936619743392459', 'Accept-Language': 'pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7', 'Accept': 'application/json, text/plain, */*', 'Origin': 'https://www.instagram.com', 'Referer': 'https://www.instagram.com/'}
913
- context_kwargs = {'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36', 'extra_http_headers': headers_for_context}
914
- if state_path.is_file():
915
- context_kwargs['storage_state'] = str(state_path)
916
- context = browser.new_context(**context_kwargs)
917
- if cookies:
918
- context.add_cookies([{'name': name, 'value': value, 'domain': '.instagram.com', 'path': '/'} for name, value in cookies.items()])
766
+ context = browser.new_context(
767
+ user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36',
768
+ extra_http_headers={'Accept-Language': 'pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7'},
769
+ )
919
770
  page = context.new_page()
920
-
921
- def capture_profile_posts(response: Any) -> None:
922
- response_url = str(getattr(response, 'url', '') or '')
923
- if not any(marker in response_url for marker in ('/api/v1/users/web_profile_info/', '/graphql/query')):
924
- return
925
- LOGGER.info('Instagram Playwright posts response URL=%s status=%s', response_url, getattr(response, 'status', None))
926
- try:
927
- payload = response.json()
928
- user = _extract_profile_user(payload)
929
- for node in _profile_post_nodes(user) if isinstance(user, Mapping) else []:
930
- post = _post_from_node(dict(node))
931
- if post and post['id'] not in seen:
932
- seen.add(post['id'])
933
- posts.append(post)
934
- except (ValueError, TypeError, AttributeError) as exc:
935
- LOGGER.debug('Não foi possível interpretar posts web_profile_info: %s', exc)
936
-
937
- page.on('response', capture_profile_posts)
938
- try:
939
- page.goto('https://www.instagram.com/', wait_until='domcontentloaded', timeout=30000)
940
- state_path.parent.mkdir(parents=True, exist_ok=True)
941
- context.storage_state(path=str(state_path))
942
- except Exception as exc:
943
- LOGGER.debug('Instagram Playwright não carregou a página inicial para posts: %s', exc)
944
- response = page.goto(reference['url'], wait_until='domcontentloaded', timeout=30000)
945
- LOGGER.debug('Instagram Playwright posts URL=%s status=%s', reference['url'], response.status if response else None)
946
- if response is None or response.status >= 400:
947
- return []
771
+ response = page.goto(url, wait_until='domcontentloaded', timeout=30000)
772
+ status = response.status if response else None
773
+ LOGGER.info('Instagram Playwright HTML posts URL=%s status=%s', url, status)
774
+ print(f'Playwright usado no Windows para os posts de @{reference.get("username", "")}'.strip())
948
775
  page.wait_for_timeout(5000)
949
776
  try:
950
777
  page.wait_for_load_state('networkidle', timeout=10000)
951
778
  except (PlaywrightTimeoutError, TimeoutError):
952
- LOGGER.debug('Instagram Playwright networkidle excedido; usando posts API já capturados.')
953
- LOGGER.debug('Instagram Playwright API posts extracted=%s limit=%s', len(posts), limit)
954
- return posts[:max(1, int(limit))]
779
+ LOGGER.debug('Instagram Playwright networkidle excedido; usando HTML já carregado para posts.')
780
+ html_content = page.content()
781
+ _save_instagram_debug_html(reference.get('username', 'instagram'), html_content)
782
+ if status is not None and status >= 400:
783
+ return []
784
+ return _extract_posts_from_html(html_content, limit)
955
785
  finally:
956
786
  browser.close()
957
787
  except Exception as exc:
958
- LOGGER.warning('Falha no Playwright dos posts Instagram: %s', exc, exc_info=True)
788
+ LOGGER.warning('Falha no Playwright HTML dos posts Instagram URL=%s: %s', url, exc, exc_info=True)
789
+ print(f'Playwright falhou no Windows para os posts: {exc}')
959
790
  _streamlit_message('error', f'Playwright não conseguiu consultar os posts Instagram: {exc}', 'posts_playwright_error')
960
- return posts
791
+ return []
961
792
 
962
793
 
963
794
  def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationResult:
@@ -968,15 +799,10 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
968
799
  except ValueError as exc:
969
800
  return IntegrationResult(False, str(exc), {})
970
801
  if platform.system() == 'Windows':
971
- LOGGER.debug('Windows detectado: posts Instagram exclusivamente via Playwright.')
972
- posts = _fetch_posts_with_playwright(reference, limit, _instagram_cookies())
802
+ LOGGER.debug('Windows detectado: posts Instagram exclusivamente via HTML Playwright.')
803
+ posts = _fetch_posts_with_playwright(reference, limit)
973
804
  if not posts:
974
- LOGGER.warning('Fallback para instaloader activado nos posts de @%s.', reference['username'])
975
- posts = fetch_posts_instaloader(reference['username'], limit)
976
- if posts:
977
- _streamlit_message('info', 'Dados obtidos via fallback (instaloader). O país pode não estar disponível.', 'posts_instaloader_fallback')
978
- else:
979
- _streamlit_message('error', 'Não foi possível obter os dados do Instagram. Verifique sua conexão ou tente novamente mais tarde.', 'posts_instagram_failed')
805
+ _streamlit_message('error', 'Não foi possível obter os dados do Instagram. Verifique sua conexão ou tente novamente mais tarde.', 'posts_instagram_failed')
980
806
  return IntegrationResult(bool(posts), 'Posts públicos encontrados.' if posts else 'Não foi possível obter os dados do Instagram. Verifique sua conexão ou tente novamente mais tarde.', reference | {'posts': posts[:max(1, int(limit))]})
981
807
  api_user = _fetch_web_profile_user(reference['username'])
982
808
  if api_user:
@@ -1012,9 +838,9 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
1012
838
  if len(posts) >= max(1, int(limit)):
1013
839
  break
1014
840
  if not posts:
1015
- posts = _fetch_posts_with_playwright(reference, limit, _instagram_cookies())
841
+ posts = _fetch_posts_with_playwright(reference, limit)
1016
842
  LOGGER.debug('Instagram posts extracted=%s limit=%s', len(posts), limit)
1017
843
  return IntegrationResult(bool(posts), 'Posts públicos encontrados.' if posts else 'Não foi possível obter os dados do Instagram. Verifique sua conexão ou tente novamente mais tarde.', reference | {'posts': posts[:max(1, int(limit))]})
1018
844
 
1019
845
 
1020
- __all__ = ['IntegrationResult', 'extract_public_instagram_country', 'fetch_posts_instaloader', 'fetch_profile_instaloader', 'fetch_public_instagram_posts', 'fetch_public_instagram_profile', 'normalize_instagram_bio', 'normalize_instagram_metric', 'normalize_instagram_reference']
846
+ __all__ = ['IntegrationResult', 'extract_public_instagram_country', 'fetch_public_instagram_posts', 'fetch_public_instagram_profile', 'normalize_instagram_bio', 'normalize_instagram_metric', 'normalize_instagram_reference']
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.6.56",
3
+ "version": "0.6.57",
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",
package/requirements.txt CHANGED
@@ -26,6 +26,5 @@ kagglehub>=0.3,<1
26
26
  supabase>=2.4,<3
27
27
  bilibili-api-python>=17.4,<18
28
28
  playwright>=1.45,<2
29
- instaloader>=4.14,<5
30
29
  composio>=0.21,<1
31
30
  justokenmax @ https://github.com/Kalmantic/jusTokenMax/archive/refs/heads/main.zip#subdirectory=python