@danhachuel/thunderbolt 0.6.52 → 0.6.53

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.
@@ -124,6 +124,98 @@ def _instagram_headers() -> dict[str, str]:
124
124
  }
125
125
 
126
126
 
127
+ def _instagram_playwright_headless() -> bool:
128
+ """Use headed mode for Windows diagnostics unless explicitly overridden."""
129
+ value = os.getenv('INSTAGRAM_PLAYWRIGHT_HEADLESS', 'false').strip().casefold()
130
+ return value not in {'0', 'false', 'no', 'off'}
131
+
132
+
133
+ def _save_instagram_debug_html(username: str, document: str) -> None:
134
+ path = Path(os.getenv('INSTAGRAM_DEBUG_PROFILE_HTML', str(Path(__file__).resolve().parents[1] / 'debug_profile.html')))
135
+ try:
136
+ path.write_text(document, encoding='utf-8')
137
+ LOGGER.info('HTML salvo em %s', path)
138
+ except OSError as exc:
139
+ LOGGER.warning('Não foi possível salvar o HTML de diagnóstico Instagram: %s', exc)
140
+
141
+
142
+ def _instagram_instaloader():
143
+ """Load the optional anonymous fallback without mutating the user's environment."""
144
+ try:
145
+ import instaloader
146
+ except ImportError:
147
+ LOGGER.warning('Fallback instaloader indisponível; instale com: python -m pip install instaloader')
148
+ return None
149
+ return instaloader
150
+
151
+
152
+ def fetch_profile_instaloader(username: str) -> dict[str, Any] | None:
153
+ """Fetch a public profile anonymously through Instaloader when Playwright fails."""
154
+ instaloader = _instagram_instaloader()
155
+ if instaloader is None:
156
+ return None
157
+ try:
158
+ loader = instaloader.Instaloader(
159
+ quiet=True,
160
+ download_pictures=False,
161
+ download_videos=False,
162
+ download_video_thumbnails=False,
163
+ save_metadata=False,
164
+ )
165
+ profile = instaloader.Profile.from_username(loader.context, username)
166
+ LOGGER.info('Fallback instaloader activado para o perfil @%s', username)
167
+ return {
168
+ 'id': str(getattr(profile, 'userid', '') or ''),
169
+ 'username': str(getattr(profile, 'username', '') or username),
170
+ 'full_name': str(getattr(profile, 'full_name', '') or ''),
171
+ 'biography': str(getattr(profile, 'biography', '') or ''),
172
+ 'follower_count': getattr(profile, 'followers', None),
173
+ 'following_count': getattr(profile, 'followees', None),
174
+ 'edge_owner_to_timeline_media': {'count': getattr(profile, 'mediacount', None)},
175
+ 'is_private': bool(getattr(profile, 'is_private', False)),
176
+ '_instagram_source': 'instaloader',
177
+ }
178
+ except Exception as exc:
179
+ LOGGER.warning('Fallback instaloader falhou para @%s: %s', username, exc, exc_info=True)
180
+ return None
181
+
182
+
183
+ def fetch_posts_instaloader(username: str, limit: int = 10) -> list[dict[str, Any]]:
184
+ """Fetch public posts anonymously through Instaloader when Playwright fails."""
185
+ instaloader = _instagram_instaloader()
186
+ if instaloader is None:
187
+ return []
188
+ posts: list[dict[str, Any]] = []
189
+ try:
190
+ loader = instaloader.Instaloader(
191
+ quiet=True,
192
+ download_pictures=False,
193
+ download_videos=False,
194
+ download_video_thumbnails=False,
195
+ save_metadata=False,
196
+ )
197
+ profile = instaloader.Profile.from_username(loader.context, username)
198
+ for post in profile.get_posts():
199
+ shortcode = str(getattr(post, 'shortcode', '') or '')
200
+ if not shortcode:
201
+ continue
202
+ posts.append({
203
+ 'id': str(getattr(post, 'mediaid', '') or shortcode),
204
+ 'shortcode': shortcode,
205
+ 'url': f'https://www.instagram.com/p/{shortcode}/',
206
+ 'image_url': str(getattr(post, 'url', '') or ''),
207
+ 'caption': str(getattr(post, 'caption', '') or '').strip(),
208
+ 'published_at': getattr(post, 'date_utc', '') or '',
209
+ 'is_video': bool(getattr(post, 'is_video', False)),
210
+ })
211
+ if len(posts) >= max(1, int(limit)):
212
+ break
213
+ LOGGER.info('Fallback instaloader carregou %s posts para @%s', len(posts), username)
214
+ except Exception as exc:
215
+ LOGGER.warning('Fallback instaloader falhou nos posts de @%s: %s', username, exc, exc_info=True)
216
+ return posts
217
+
218
+
127
219
  @dataclass
128
220
  class IntegrationResult:
129
221
  ok: bool
@@ -291,11 +383,11 @@ def _fetch_profile_with_playwright(username: str, headers: Mapping[str, str] | N
291
383
  try:
292
384
  from playwright.sync_api import TimeoutError as PlaywrightTimeoutError, sync_playwright
293
385
  with sync_playwright() as playwright:
294
- browser = playwright.chromium.launch(headless=True)
386
+ browser = playwright.chromium.launch(headless=_instagram_playwright_headless())
295
387
  try:
296
388
  page = browser.new_page(
297
389
  user_agent=headers['User-Agent'],
298
- extra_http_headers={'x-ig-app-id': headers['x-ig-app-id'], 'Accept-Language': 'en-US,en;q=0.9'},
390
+ extra_http_headers={'x-ig-app-id': headers['x-ig-app-id'], 'Accept-Language': 'en-US,en;q=0.9', 'Accept': 'application/json, text/plain, */*', 'Origin': 'https://www.instagram.com', 'Referer': 'https://www.instagram.com/'},
299
391
  )
300
392
  if cookies:
301
393
  page.context.add_cookies([{'name': name, 'value': value, 'domain': '.instagram.com', 'path': '/'} for name, value in cookies.items()])
@@ -314,16 +406,26 @@ def _fetch_profile_with_playwright(username: str, headers: Mapping[str, str] | N
314
406
  LOGGER.debug('Não foi possível interpretar resposta web_profile_info: %s', exc)
315
407
 
316
408
  page.on('response', capture_profile_response)
409
+ try:
410
+ page.goto('https://www.instagram.com/', wait_until='domcontentloaded', timeout=10000)
411
+ LOGGER.debug('Instagram Playwright página inicial carregada para cookies.')
412
+ except Exception as exc:
413
+ LOGGER.debug('Instagram Playwright não carregou a página inicial: %s', exc)
317
414
  url = f'https://www.instagram.com/{username}/'
318
- response = page.goto(url, wait_until='domcontentloaded', timeout=30000)
415
+ response = page.goto(url, wait_until='domcontentloaded', timeout=10000)
319
416
  LOGGER.debug('Instagram Playwright profile URL=%s status=%s', url, response.status if response else None)
320
417
  if response is None or response.status >= 400:
321
- return None
418
+ LOGGER.warning('Instagram Playwright perfil HTTP status=%s URL=%s', response.status if response else None, url)
419
+ return captured_user
420
+ try:
421
+ _save_instagram_debug_html(username, page.content())
422
+ except Exception as exc:
423
+ LOGGER.debug('Não foi possível guardar HTML de diagnóstico: %s', exc)
322
424
  try:
323
425
  page.wait_for_load_state('networkidle', timeout=10000)
324
426
  except (PlaywrightTimeoutError, TimeoutError):
325
427
  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 {}))
428
+ LOGGER.info('Instagram Playwright API profile captured=%s quality=%s URL=%s', bool(captured_user), _profile_quality(captured_user or {}), url)
327
429
  finally:
328
430
  browser.close()
329
431
  except Exception as exc:
@@ -339,7 +441,11 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
339
441
  headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36'
340
442
  if platform.system() == 'Windows':
341
443
  LOGGER.debug('Windows detectado: perfil Instagram exclusivamente via Playwright.')
342
- return _fetch_profile_with_playwright(username, headers, cookies)
444
+ playwright_user = _fetch_profile_with_playwright(username, headers, cookies)
445
+ if playwright_user:
446
+ return playwright_user
447
+ LOGGER.warning('Fallback para instaloader activado no perfil @%s.', username)
448
+ return fetch_profile_instaloader(username)
343
449
 
344
450
  endpoints = (
345
451
  f'https://www.instagram.com/api/v1/users/web_profile_info/?username={username}',
@@ -570,7 +676,10 @@ def fetch_public_instagram_profile(source: str) -> IntegrationResult:
570
676
  return IntegrationResult(False, str(exc), {})
571
677
  api_user = _fetch_web_profile_user(reference['username'])
572
678
  if api_user:
573
- return IntegrationResult(True, 'Perfil Instagram encontrado publicamente.', _profile_data_from_api(api_user, reference))
679
+ data = _profile_data_from_api(api_user, reference)
680
+ if api_user.get('_instagram_source') == 'instaloader':
681
+ return IntegrationResult(True, 'Perfil encontrado via fallback instaloader. O país pode não estar disponível.', data)
682
+ return IntegrationResult(True, 'Perfil Instagram encontrado publicamente.', data)
574
683
  if platform.system() == 'Windows':
575
684
  return IntegrationResult(False, 'Não foi possível extrair os dados públicos do Instagram com o Playwright.', reference)
576
685
  try:
@@ -674,11 +783,11 @@ def _fetch_posts_with_playwright(reference: Mapping[str, str], limit: int, cooki
674
783
  try:
675
784
  from playwright.sync_api import TimeoutError as PlaywrightTimeoutError, sync_playwright
676
785
  with sync_playwright() as playwright:
677
- browser = playwright.chromium.launch(headless=True)
786
+ browser = playwright.chromium.launch(headless=_instagram_playwright_headless())
678
787
  try:
679
788
  page = browser.new_page(
680
789
  user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36',
681
- extra_http_headers={'x-ig-app-id': '936619743392459', 'Accept-Language': 'en-US,en;q=0.9'},
790
+ extra_http_headers={'x-ig-app-id': '936619743392459', 'Accept-Language': 'en-US,en;q=0.9', 'Accept': 'application/json, text/plain, */*', 'Origin': 'https://www.instagram.com', 'Referer': 'https://www.instagram.com/'},
682
791
  )
683
792
  if cookies:
684
793
  page.context.add_cookies([{'name': name, 'value': value, 'domain': '.instagram.com', 'path': '/'} for name, value in cookies.items()])
@@ -703,7 +812,11 @@ def _fetch_posts_with_playwright(reference: Mapping[str, str], limit: int, cooki
703
812
  LOGGER.debug('Não foi possível interpretar posts web_profile_info: %s', exc)
704
813
 
705
814
  page.on('response', capture_profile_posts)
706
- response = page.goto(reference['url'], wait_until='domcontentloaded', timeout=30000)
815
+ try:
816
+ page.goto('https://www.instagram.com/', wait_until='domcontentloaded', timeout=10000)
817
+ except Exception as exc:
818
+ LOGGER.debug('Instagram Playwright não carregou a página inicial para posts: %s', exc)
819
+ response = page.goto(reference['url'], wait_until='domcontentloaded', timeout=10000)
707
820
  LOGGER.debug('Instagram Playwright posts URL=%s status=%s', reference['url'], response.status if response else None)
708
821
  if response is None or response.status >= 400:
709
822
  return []
@@ -731,6 +844,9 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
731
844
  if platform.system() == 'Windows':
732
845
  LOGGER.debug('Windows detectado: posts Instagram exclusivamente via Playwright.')
733
846
  posts = _fetch_posts_with_playwright(reference, limit, _instagram_cookies())
847
+ if not posts:
848
+ LOGGER.warning('Fallback para instaloader activado nos posts de @%s.', reference['username'])
849
+ posts = fetch_posts_instaloader(reference['username'], limit)
734
850
  return IntegrationResult(bool(posts), 'Posts públicos encontrados.' if posts else 'Não foi possível encontrar posts públicos nesta página do Instagram.', reference | {'posts': posts[:max(1, int(limit))]})
735
851
  api_user = _fetch_web_profile_user(reference['username'])
736
852
  if api_user:
@@ -773,4 +889,4 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
773
889
  return IntegrationResult(bool(posts), 'Posts públicos encontrados.' if posts else 'Não foi possível encontrar posts públicos nesta página do Instagram.', reference | {'posts': posts[:max(1, int(limit))]})
774
890
 
775
891
 
776
- __all__ = ['IntegrationResult', 'extract_public_instagram_country', 'fetch_public_instagram_posts', 'fetch_public_instagram_profile', 'normalize_instagram_bio', 'normalize_instagram_metric', 'normalize_instagram_reference']
892
+ __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']
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.6.52",
3
+ "version": "0.6.53",
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,5 +26,6 @@ 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
29
30
  composio>=0.21,<1
30
31
  justokenmax @ https://github.com/Kalmantic/jusTokenMax/archive/refs/heads/main.zip#subdirectory=python