@danhachuel/thunderbolt 0.6.51 → 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.
- package/integrations/instagram_public.py +179 -33
- package/package.json +1 -1
- package/requirements.txt +1 -0
|
@@ -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
|
|
@@ -284,36 +376,62 @@ def _profile_users_from_document(document: str) -> list[Mapping[str, Any]]:
|
|
|
284
376
|
|
|
285
377
|
|
|
286
378
|
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
|
|
379
|
+
"""Fetch the structured profile JSON requested by the public Instagram page."""
|
|
288
380
|
headers = headers or _instagram_headers()
|
|
289
381
|
cookies = cookies or _instagram_cookies()
|
|
290
|
-
|
|
382
|
+
captured_user: dict[str, Any] | None = None
|
|
291
383
|
try:
|
|
292
|
-
from playwright.sync_api import sync_playwright
|
|
384
|
+
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError, sync_playwright
|
|
293
385
|
with sync_playwright() as playwright:
|
|
294
|
-
browser = playwright.chromium.launch(headless=
|
|
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()])
|
|
394
|
+
|
|
395
|
+
def capture_profile_response(response: Any) -> None:
|
|
396
|
+
nonlocal captured_user
|
|
397
|
+
if '/api/v1/users/web_profile_info/' not in response.url:
|
|
398
|
+
return
|
|
399
|
+
try:
|
|
400
|
+
payload = response.json()
|
|
401
|
+
user = ((payload.get('data') or {}).get('user') if isinstance(payload, Mapping) else None)
|
|
402
|
+
if isinstance(user, Mapping):
|
|
403
|
+
captured_user = _merge_profile(captured_user, user)
|
|
404
|
+
LOGGER.debug('Instagram Playwright API profile captured user=%s', username)
|
|
405
|
+
except (ValueError, TypeError, AttributeError) as exc:
|
|
406
|
+
LOGGER.debug('Não foi possível interpretar resposta web_profile_info: %s', exc)
|
|
407
|
+
|
|
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)
|
|
302
414
|
url = f'https://www.instagram.com/{username}/'
|
|
303
|
-
response = page.goto(url, wait_until='domcontentloaded', timeout=
|
|
415
|
+
response = page.goto(url, wait_until='domcontentloaded', timeout=10000)
|
|
304
416
|
LOGGER.debug('Instagram Playwright profile URL=%s status=%s', url, response.status if response else None)
|
|
305
417
|
if response is None or response.status >= 400:
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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)
|
|
424
|
+
try:
|
|
425
|
+
page.wait_for_load_state('networkidle', timeout=10000)
|
|
426
|
+
except (PlaywrightTimeoutError, TimeoutError):
|
|
427
|
+
LOGGER.debug('Instagram Playwright networkidle excedido; usando resposta API já capturada.')
|
|
428
|
+
LOGGER.info('Instagram Playwright API profile captured=%s quality=%s URL=%s', bool(captured_user), _profile_quality(captured_user or {}), url)
|
|
311
429
|
finally:
|
|
312
430
|
browser.close()
|
|
313
431
|
except Exception as exc:
|
|
314
432
|
LOGGER.warning('Falha no Playwright do perfil Instagram: %s', exc, exc_info=True)
|
|
315
433
|
_streamlit_message('error', f'Playwright não conseguiu consultar o perfil Instagram: {exc}', 'profile_playwright_error')
|
|
316
|
-
return
|
|
434
|
+
return captured_user
|
|
317
435
|
|
|
318
436
|
|
|
319
437
|
def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
@@ -323,7 +441,11 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
|
323
441
|
headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36'
|
|
324
442
|
if platform.system() == 'Windows':
|
|
325
443
|
LOGGER.debug('Windows detectado: perfil Instagram exclusivamente via Playwright.')
|
|
326
|
-
|
|
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)
|
|
327
449
|
|
|
328
450
|
endpoints = (
|
|
329
451
|
f'https://www.instagram.com/api/v1/users/web_profile_info/?username={username}',
|
|
@@ -554,7 +676,10 @@ def fetch_public_instagram_profile(source: str) -> IntegrationResult:
|
|
|
554
676
|
return IntegrationResult(False, str(exc), {})
|
|
555
677
|
api_user = _fetch_web_profile_user(reference['username'])
|
|
556
678
|
if api_user:
|
|
557
|
-
|
|
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)
|
|
558
683
|
if platform.system() == 'Windows':
|
|
559
684
|
return IntegrationResult(False, 'Não foi possível extrair os dados públicos do Instagram com o Playwright.', reference)
|
|
560
685
|
try:
|
|
@@ -651,38 +776,56 @@ def _post_from_node(node: dict[str, Any]) -> dict[str, Any] | None:
|
|
|
651
776
|
|
|
652
777
|
|
|
653
778
|
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
|
|
779
|
+
# CORREÇÃO WINDOWS: posts são extraídos exclusivamente da resposta API capturada pelo Playwright.
|
|
655
780
|
posts: list[dict[str, Any]] = []
|
|
656
781
|
seen: set[str] = set()
|
|
657
782
|
cookies = cookies or _instagram_cookies()
|
|
658
783
|
try:
|
|
659
|
-
from playwright.sync_api import sync_playwright
|
|
784
|
+
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError, sync_playwright
|
|
660
785
|
with sync_playwright() as playwright:
|
|
661
|
-
browser = playwright.chromium.launch(headless=
|
|
786
|
+
browser = playwright.chromium.launch(headless=_instagram_playwright_headless())
|
|
662
787
|
try:
|
|
663
788
|
page = browser.new_page(
|
|
664
789
|
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36',
|
|
665
|
-
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/'},
|
|
666
791
|
)
|
|
667
792
|
if cookies:
|
|
668
793
|
page.context.add_cookies([{'name': name, 'value': value, 'domain': '.instagram.com', 'path': '/'} for name, value in cookies.items()])
|
|
669
|
-
|
|
794
|
+
|
|
795
|
+
def capture_profile_posts(response: Any) -> None:
|
|
796
|
+
if '/api/v1/users/web_profile_info/' not in response.url:
|
|
797
|
+
return
|
|
798
|
+
try:
|
|
799
|
+
payload = response.json()
|
|
800
|
+
user = ((payload.get('data') or {}).get('user') if isinstance(payload, Mapping) else None)
|
|
801
|
+
media = user.get('edge_owner_to_timeline_media') if isinstance(user, Mapping) else None
|
|
802
|
+
edges = media.get('edges') if isinstance(media, Mapping) else []
|
|
803
|
+
for edge in edges or []:
|
|
804
|
+
node = edge.get('node') if isinstance(edge, Mapping) else None
|
|
805
|
+
if not isinstance(node, dict):
|
|
806
|
+
continue
|
|
807
|
+
post = _post_from_node(node)
|
|
808
|
+
if post and post['id'] not in seen:
|
|
809
|
+
seen.add(post['id'])
|
|
810
|
+
posts.append(post)
|
|
811
|
+
except (ValueError, TypeError, AttributeError) as exc:
|
|
812
|
+
LOGGER.debug('Não foi possível interpretar posts web_profile_info: %s', exc)
|
|
813
|
+
|
|
814
|
+
page.on('response', capture_profile_posts)
|
|
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)
|
|
670
820
|
LOGGER.debug('Instagram Playwright posts URL=%s status=%s', reference['url'], response.status if response else None)
|
|
671
821
|
if response is None or response.status >= 400:
|
|
672
822
|
return []
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
|
823
|
+
try:
|
|
824
|
+
page.wait_for_load_state('networkidle', timeout=10000)
|
|
825
|
+
except (PlaywrightTimeoutError, TimeoutError):
|
|
826
|
+
LOGGER.debug('Instagram Playwright networkidle excedido; usando posts API já capturados.')
|
|
827
|
+
LOGGER.debug('Instagram Playwright API posts extracted=%s limit=%s', len(posts), limit)
|
|
828
|
+
return posts[:max(1, int(limit))]
|
|
686
829
|
finally:
|
|
687
830
|
browser.close()
|
|
688
831
|
except Exception as exc:
|
|
@@ -701,6 +844,9 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
|
|
|
701
844
|
if platform.system() == 'Windows':
|
|
702
845
|
LOGGER.debug('Windows detectado: posts Instagram exclusivamente via Playwright.')
|
|
703
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)
|
|
704
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))]})
|
|
705
851
|
api_user = _fetch_web_profile_user(reference['username'])
|
|
706
852
|
if api_user:
|
|
@@ -743,4 +889,4 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
|
|
|
743
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))]})
|
|
744
890
|
|
|
745
891
|
|
|
746
|
-
__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
package/requirements.txt
CHANGED