@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.
- package/integrations/instagram_public.py +127 -11
- 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
|
|
@@ -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=
|
|
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=
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
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
package/requirements.txt
CHANGED