@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.
- package/integrations/instagram_public.py +195 -17
- package/package.json +1 -1
- package/requirements.txt +1 -0
|
@@ -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=
|
|
427
|
+
browser = playwright.chromium.launch(headless=_instagram_playwright_headless())
|
|
295
428
|
try:
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
838
|
+
browser = playwright.chromium.launch(headless=_instagram_playwright_headless())
|
|
678
839
|
try:
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
-
|
|
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
package/requirements.txt
CHANGED