@danhachuel/thunderbolt 0.6.52 → 0.6.54

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,6 +25,7 @@ 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
28
29
 
29
30
 
30
31
  ABOUT_ACCOUNT_URL = 'https://i.instagram.com/api/v1/bloks/apps/com.instagram.interactions.about_this_account/'
@@ -124,6 +125,138 @@ def _instagram_headers() -> dict[str, str]:
124
125
  }
125
126
 
126
127
 
128
+ 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()
131
+ return value not in {'0', 'false', 'no', 'off'}
132
+
133
+
134
+ def _save_instagram_debug_html(username: str, document: str) -> None:
135
+ storage_dir = Path(__file__).resolve().parents[1] / 'storage'
136
+ path = Path(os.getenv('INSTAGRAM_DEBUG_PROFILE_HTML', str(storage_dir / 'debug_instagram.html')))
137
+ try:
138
+ path.parent.mkdir(parents=True, exist_ok=True)
139
+ path.write_text(document, encoding='utf-8')
140
+ LOGGER.info('HTML salvo em %s', path)
141
+ except OSError as exc:
142
+ LOGGER.warning('Não foi possível salvar o HTML de diagnóstico Instagram: %s', exc)
143
+
144
+
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
+ return {
195
+ 'id': str(getattr(profile, 'userid', '') or ''),
196
+ 'username': str(getattr(profile, 'username', '') or username),
197
+ 'full_name': str(getattr(profile, 'full_name', '') or ''),
198
+ 'biography': str(getattr(profile, 'biography', '') or ''),
199
+ 'follower_count': getattr(profile, 'follower_count', getattr(profile, 'followers', None)),
200
+ 'following_count': getattr(profile, 'following_count', getattr(profile, 'followees', None)),
201
+ 'edge_owner_to_timeline_media': {'count': getattr(profile, 'mediacount', None)},
202
+ 'is_private': bool(getattr(profile, 'is_private', False)),
203
+ '_instagram_source': 'instaloader',
204
+ }
205
+ except Exception as exc:
206
+ LOGGER.warning('Fallback instaloader falhou para @%s: %s', username, exc, exc_info=True)
207
+ return None
208
+
209
+
210
+ def _fetch_posts_instaloader(username: str, limit: int = 10) -> list[dict[str, Any]]:
211
+ """Fetch public posts anonymously through Instaloader when Playwright fails."""
212
+ instaloader = _instagram_instaloader()
213
+ if instaloader is None:
214
+ return []
215
+ posts: list[dict[str, Any]] = []
216
+ try:
217
+ loader = instaloader.Instaloader(
218
+ quiet=True,
219
+ request_timeout=30,
220
+ max_connection_attempts=1,
221
+ download_pictures=False,
222
+ download_videos=False,
223
+ download_video_thumbnails=False,
224
+ save_metadata=False,
225
+ )
226
+ profile = instaloader.Profile.from_username(loader.context, username)
227
+ for post in profile.get_posts():
228
+ shortcode = str(getattr(post, 'shortcode', '') or '')
229
+ if not shortcode:
230
+ continue
231
+ image_url = str(getattr(post, 'url', '') or '')
232
+ timestamp = getattr(post, 'date_utc', '') or ''
233
+ posts.append({
234
+ 'id': str(getattr(post, 'mediaid', '') or shortcode),
235
+ 'shortcode': shortcode,
236
+ 'url': f'https://www.instagram.com/p/{shortcode}/',
237
+ 'image_url': image_url,
238
+ 'display_url': image_url,
239
+ 'caption': str(getattr(post, 'caption', '') or '').strip(),
240
+ 'published_at': timestamp,
241
+ 'timestamp': timestamp,
242
+ 'is_video': bool(getattr(post, 'is_video', False)),
243
+ })
244
+ if len(posts) >= max(1, int(limit)):
245
+ break
246
+ LOGGER.info('Fallback instaloader carregou %s posts para @%s', len(posts), username)
247
+ except Exception as exc:
248
+ LOGGER.warning('Fallback instaloader falhou nos posts de @%s: %s', username, exc, exc_info=True)
249
+ return posts
250
+
251
+
252
+ def fetch_profile_instaloader(username: str) -> dict[str, Any] | None:
253
+ return _fetch_profile_instaloader(username)
254
+
255
+
256
+ def fetch_posts_instaloader(username: str, limit: int = 10) -> list[dict[str, Any]]:
257
+ return _fetch_posts_instaloader(username, limit)
258
+
259
+
127
260
  @dataclass
128
261
  class IntegrationResult:
129
262
  ok: bool
@@ -291,14 +424,17 @@ def _fetch_profile_with_playwright(username: str, headers: Mapping[str, str] | N
291
424
  try:
292
425
  from playwright.sync_api import TimeoutError as PlaywrightTimeoutError, sync_playwright
293
426
  with sync_playwright() as playwright:
294
- browser = playwright.chromium.launch(headless=True)
427
+ browser = playwright.chromium.launch(headless=_instagram_playwright_headless())
295
428
  try:
296
- page = browser.new_page(
297
- 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'},
299
- )
429
+ 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/'}
430
+ state_path = Path(__file__).resolve().parents[1] / 'storage' / 'instagram_storage_state.json'
431
+ context_kwargs = {'user_agent': headers['User-Agent'], 'extra_http_headers': headers_for_context}
432
+ if state_path.is_file():
433
+ context_kwargs['storage_state'] = str(state_path)
434
+ context = browser.new_context(**context_kwargs)
300
435
  if cookies:
301
- page.context.add_cookies([{'name': name, 'value': value, 'domain': '.instagram.com', 'path': '/'} for name, value in cookies.items()])
436
+ context.add_cookies([{'name': name, 'value': value, 'domain': '.instagram.com', 'path': '/'} for name, value in cookies.items()])
437
+ page = context.new_page()
302
438
 
303
439
  def capture_profile_response(response: Any) -> None:
304
440
  nonlocal captured_user
@@ -314,16 +450,29 @@ def _fetch_profile_with_playwright(username: str, headers: Mapping[str, str] | N
314
450
  LOGGER.debug('Não foi possível interpretar resposta web_profile_info: %s', exc)
315
451
 
316
452
  page.on('response', capture_profile_response)
453
+ try:
454
+ page.goto('https://www.instagram.com/', wait_until='domcontentloaded', timeout=30000)
455
+ state_path.parent.mkdir(parents=True, exist_ok=True)
456
+ context.storage_state(path=str(state_path))
457
+ LOGGER.debug('Instagram Playwright página inicial carregada; storage_state salvo em %s.', state_path)
458
+ except Exception as exc:
459
+ LOGGER.debug('Instagram Playwright não carregou a página inicial: %s', exc)
317
460
  url = f'https://www.instagram.com/{username}/'
318
461
  response = page.goto(url, wait_until='domcontentloaded', timeout=30000)
319
462
  LOGGER.debug('Instagram Playwright profile URL=%s status=%s', url, response.status if response else None)
320
463
  if response is None or response.status >= 400:
321
- return None
464
+ LOGGER.warning('Instagram Playwright perfil HTTP status=%s URL=%s', response.status if response else None, url)
465
+ return captured_user
466
+ try:
467
+ _save_instagram_debug_html(username, page.content())
468
+ except Exception as exc:
469
+ LOGGER.debug('Não foi possível guardar HTML de diagnóstico: %s', exc)
470
+ page.wait_for_timeout(5000)
322
471
  try:
323
472
  page.wait_for_load_state('networkidle', timeout=10000)
324
473
  except (PlaywrightTimeoutError, TimeoutError):
325
474
  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 {}))
475
+ LOGGER.info('Instagram Playwright API profile captured=%s quality=%s URL=%s', bool(captured_user), _profile_quality(captured_user or {}), url)
327
476
  finally:
328
477
  browser.close()
329
478
  except Exception as exc:
@@ -339,7 +488,16 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
339
488
  headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36'
340
489
  if platform.system() == 'Windows':
341
490
  LOGGER.debug('Windows detectado: perfil Instagram exclusivamente via Playwright.')
342
- return _fetch_profile_with_playwright(username, headers, cookies)
491
+ playwright_user = _fetch_profile_with_playwright(username, headers, cookies)
492
+ if playwright_user:
493
+ return playwright_user
494
+ LOGGER.warning('Fallback para instaloader activado no perfil @%s.', username)
495
+ fallback_user = fetch_profile_instaloader(username)
496
+ if fallback_user:
497
+ _streamlit_message('info', 'Dados obtidos via fallback (instaloader). O país pode não estar disponível.', 'profile_instaloader_fallback')
498
+ else:
499
+ _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')
500
+ return fallback_user
343
501
 
344
502
  endpoints = (
345
503
  f'https://www.instagram.com/api/v1/users/web_profile_info/?username={username}',
@@ -570,7 +728,10 @@ def fetch_public_instagram_profile(source: str) -> IntegrationResult:
570
728
  return IntegrationResult(False, str(exc), {})
571
729
  api_user = _fetch_web_profile_user(reference['username'])
572
730
  if api_user:
573
- return IntegrationResult(True, 'Perfil Instagram encontrado publicamente.', _profile_data_from_api(api_user, reference))
731
+ data = _profile_data_from_api(api_user, reference)
732
+ if api_user.get('_instagram_source') == 'instaloader':
733
+ return IntegrationResult(True, 'Perfil encontrado via fallback instaloader. O país pode não estar disponível.', data)
734
+ return IntegrationResult(True, 'Perfil Instagram encontrado publicamente.', data)
574
735
  if platform.system() == 'Windows':
575
736
  return IntegrationResult(False, 'Não foi possível extrair os dados públicos do Instagram com o Playwright.', reference)
576
737
  try:
@@ -674,14 +835,17 @@ def _fetch_posts_with_playwright(reference: Mapping[str, str], limit: int, cooki
674
835
  try:
675
836
  from playwright.sync_api import TimeoutError as PlaywrightTimeoutError, sync_playwright
676
837
  with sync_playwright() as playwright:
677
- browser = playwright.chromium.launch(headless=True)
838
+ browser = playwright.chromium.launch(headless=_instagram_playwright_headless())
678
839
  try:
679
- page = browser.new_page(
680
- 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'},
682
- )
840
+ state_path = Path(__file__).resolve().parents[1] / 'storage' / 'instagram_storage_state.json'
841
+ 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/'}
842
+ 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}
843
+ if state_path.is_file():
844
+ context_kwargs['storage_state'] = str(state_path)
845
+ context = browser.new_context(**context_kwargs)
683
846
  if cookies:
684
- page.context.add_cookies([{'name': name, 'value': value, 'domain': '.instagram.com', 'path': '/'} for name, value in cookies.items()])
847
+ context.add_cookies([{'name': name, 'value': value, 'domain': '.instagram.com', 'path': '/'} for name, value in cookies.items()])
848
+ page = context.new_page()
685
849
 
686
850
  def capture_profile_posts(response: Any) -> None:
687
851
  if '/api/v1/users/web_profile_info/' not in response.url:
@@ -703,10 +867,17 @@ def _fetch_posts_with_playwright(reference: Mapping[str, str], limit: int, cooki
703
867
  LOGGER.debug('Não foi possível interpretar posts web_profile_info: %s', exc)
704
868
 
705
869
  page.on('response', capture_profile_posts)
870
+ try:
871
+ page.goto('https://www.instagram.com/', wait_until='domcontentloaded', timeout=30000)
872
+ state_path.parent.mkdir(parents=True, exist_ok=True)
873
+ context.storage_state(path=str(state_path))
874
+ except Exception as exc:
875
+ LOGGER.debug('Instagram Playwright não carregou a página inicial para posts: %s', exc)
706
876
  response = page.goto(reference['url'], wait_until='domcontentloaded', timeout=30000)
707
877
  LOGGER.debug('Instagram Playwright posts URL=%s status=%s', reference['url'], response.status if response else None)
708
878
  if response is None or response.status >= 400:
709
879
  return []
880
+ page.wait_for_timeout(5000)
710
881
  try:
711
882
  page.wait_for_load_state('networkidle', timeout=10000)
712
883
  except (PlaywrightTimeoutError, TimeoutError):
@@ -731,6 +902,13 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
731
902
  if platform.system() == 'Windows':
732
903
  LOGGER.debug('Windows detectado: posts Instagram exclusivamente via Playwright.')
733
904
  posts = _fetch_posts_with_playwright(reference, limit, _instagram_cookies())
905
+ if not posts:
906
+ LOGGER.warning('Fallback para instaloader activado nos posts de @%s.', reference['username'])
907
+ posts = fetch_posts_instaloader(reference['username'], limit)
908
+ if posts:
909
+ _streamlit_message('info', 'Dados obtidos via fallback (instaloader). O país pode não estar disponível.', 'posts_instaloader_fallback')
910
+ else:
911
+ _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')
734
912
  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
913
  api_user = _fetch_web_profile_user(reference['username'])
736
914
  if api_user:
@@ -773,4 +951,4 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
773
951
  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
952
 
775
953
 
776
- __all__ = ['IntegrationResult', 'extract_public_instagram_country', 'fetch_public_instagram_posts', 'fetch_public_instagram_profile', 'normalize_instagram_bio', 'normalize_instagram_metric', 'normalize_instagram_reference']
954
+ __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.54",
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