@danhachuel/thunderbolt 0.6.53 → 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 +88 -26
- package/package.json +1 -1
|
@@ -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/'
|
|
@@ -131,8 +132,10 @@ def _instagram_playwright_headless() -> bool:
|
|
|
131
132
|
|
|
132
133
|
|
|
133
134
|
def _save_instagram_debug_html(username: str, document: str) -> None:
|
|
134
|
-
|
|
135
|
+
storage_dir = Path(__file__).resolve().parents[1] / 'storage'
|
|
136
|
+
path = Path(os.getenv('INSTAGRAM_DEBUG_PROFILE_HTML', str(storage_dir / 'debug_instagram.html')))
|
|
135
137
|
try:
|
|
138
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
136
139
|
path.write_text(document, encoding='utf-8')
|
|
137
140
|
LOGGER.info('HTML salvo em %s', path)
|
|
138
141
|
except OSError as exc:
|
|
@@ -140,16 +143,38 @@ def _save_instagram_debug_html(username: str, document: str) -> None:
|
|
|
140
143
|
|
|
141
144
|
|
|
142
145
|
def _instagram_instaloader():
|
|
143
|
-
"""Load the
|
|
146
|
+
"""Load the anonymous fallback, installing it once when the requirement is missing."""
|
|
147
|
+
global _INSTALOADER_INSTALL_ATTEMPTED
|
|
144
148
|
try:
|
|
145
149
|
import instaloader
|
|
150
|
+
return instaloader
|
|
146
151
|
except ImportError:
|
|
147
|
-
|
|
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')
|
|
148
174
|
return None
|
|
149
|
-
return instaloader
|
|
150
175
|
|
|
151
176
|
|
|
152
|
-
def
|
|
177
|
+
def _fetch_profile_instaloader(username: str) -> dict[str, Any] | None:
|
|
153
178
|
"""Fetch a public profile anonymously through Instaloader when Playwright fails."""
|
|
154
179
|
instaloader = _instagram_instaloader()
|
|
155
180
|
if instaloader is None:
|
|
@@ -157,6 +182,8 @@ def fetch_profile_instaloader(username: str) -> dict[str, Any] | None:
|
|
|
157
182
|
try:
|
|
158
183
|
loader = instaloader.Instaloader(
|
|
159
184
|
quiet=True,
|
|
185
|
+
request_timeout=30,
|
|
186
|
+
max_connection_attempts=1,
|
|
160
187
|
download_pictures=False,
|
|
161
188
|
download_videos=False,
|
|
162
189
|
download_video_thumbnails=False,
|
|
@@ -169,8 +196,8 @@ def fetch_profile_instaloader(username: str) -> dict[str, Any] | None:
|
|
|
169
196
|
'username': str(getattr(profile, 'username', '') or username),
|
|
170
197
|
'full_name': str(getattr(profile, 'full_name', '') or ''),
|
|
171
198
|
'biography': str(getattr(profile, 'biography', '') or ''),
|
|
172
|
-
'follower_count': getattr(profile, 'followers', None),
|
|
173
|
-
'following_count': getattr(profile, 'followees', None),
|
|
199
|
+
'follower_count': getattr(profile, 'follower_count', getattr(profile, 'followers', None)),
|
|
200
|
+
'following_count': getattr(profile, 'following_count', getattr(profile, 'followees', None)),
|
|
174
201
|
'edge_owner_to_timeline_media': {'count': getattr(profile, 'mediacount', None)},
|
|
175
202
|
'is_private': bool(getattr(profile, 'is_private', False)),
|
|
176
203
|
'_instagram_source': 'instaloader',
|
|
@@ -180,7 +207,7 @@ def fetch_profile_instaloader(username: str) -> dict[str, Any] | None:
|
|
|
180
207
|
return None
|
|
181
208
|
|
|
182
209
|
|
|
183
|
-
def
|
|
210
|
+
def _fetch_posts_instaloader(username: str, limit: int = 10) -> list[dict[str, Any]]:
|
|
184
211
|
"""Fetch public posts anonymously through Instaloader when Playwright fails."""
|
|
185
212
|
instaloader = _instagram_instaloader()
|
|
186
213
|
if instaloader is None:
|
|
@@ -189,6 +216,8 @@ def fetch_posts_instaloader(username: str, limit: int = 10) -> list[dict[str, An
|
|
|
189
216
|
try:
|
|
190
217
|
loader = instaloader.Instaloader(
|
|
191
218
|
quiet=True,
|
|
219
|
+
request_timeout=30,
|
|
220
|
+
max_connection_attempts=1,
|
|
192
221
|
download_pictures=False,
|
|
193
222
|
download_videos=False,
|
|
194
223
|
download_video_thumbnails=False,
|
|
@@ -199,13 +228,17 @@ def fetch_posts_instaloader(username: str, limit: int = 10) -> list[dict[str, An
|
|
|
199
228
|
shortcode = str(getattr(post, 'shortcode', '') or '')
|
|
200
229
|
if not shortcode:
|
|
201
230
|
continue
|
|
231
|
+
image_url = str(getattr(post, 'url', '') or '')
|
|
232
|
+
timestamp = getattr(post, 'date_utc', '') or ''
|
|
202
233
|
posts.append({
|
|
203
234
|
'id': str(getattr(post, 'mediaid', '') or shortcode),
|
|
204
235
|
'shortcode': shortcode,
|
|
205
236
|
'url': f'https://www.instagram.com/p/{shortcode}/',
|
|
206
|
-
'image_url':
|
|
237
|
+
'image_url': image_url,
|
|
238
|
+
'display_url': image_url,
|
|
207
239
|
'caption': str(getattr(post, 'caption', '') or '').strip(),
|
|
208
|
-
'published_at':
|
|
240
|
+
'published_at': timestamp,
|
|
241
|
+
'timestamp': timestamp,
|
|
209
242
|
'is_video': bool(getattr(post, 'is_video', False)),
|
|
210
243
|
})
|
|
211
244
|
if len(posts) >= max(1, int(limit)):
|
|
@@ -216,6 +249,14 @@ def fetch_posts_instaloader(username: str, limit: int = 10) -> list[dict[str, An
|
|
|
216
249
|
return posts
|
|
217
250
|
|
|
218
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
|
+
|
|
219
260
|
@dataclass
|
|
220
261
|
class IntegrationResult:
|
|
221
262
|
ok: bool
|
|
@@ -385,12 +426,15 @@ def _fetch_profile_with_playwright(username: str, headers: Mapping[str, str] | N
|
|
|
385
426
|
with sync_playwright() as playwright:
|
|
386
427
|
browser = playwright.chromium.launch(headless=_instagram_playwright_headless())
|
|
387
428
|
try:
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
)
|
|
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)
|
|
392
435
|
if cookies:
|
|
393
|
-
|
|
436
|
+
context.add_cookies([{'name': name, 'value': value, 'domain': '.instagram.com', 'path': '/'} for name, value in cookies.items()])
|
|
437
|
+
page = context.new_page()
|
|
394
438
|
|
|
395
439
|
def capture_profile_response(response: Any) -> None:
|
|
396
440
|
nonlocal captured_user
|
|
@@ -407,12 +451,14 @@ def _fetch_profile_with_playwright(username: str, headers: Mapping[str, str] | N
|
|
|
407
451
|
|
|
408
452
|
page.on('response', capture_profile_response)
|
|
409
453
|
try:
|
|
410
|
-
page.goto('https://www.instagram.com/', wait_until='domcontentloaded', timeout=
|
|
411
|
-
|
|
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)
|
|
412
458
|
except Exception as exc:
|
|
413
459
|
LOGGER.debug('Instagram Playwright não carregou a página inicial: %s', exc)
|
|
414
460
|
url = f'https://www.instagram.com/{username}/'
|
|
415
|
-
response = page.goto(url, wait_until='domcontentloaded', timeout=
|
|
461
|
+
response = page.goto(url, wait_until='domcontentloaded', timeout=30000)
|
|
416
462
|
LOGGER.debug('Instagram Playwright profile URL=%s status=%s', url, response.status if response else None)
|
|
417
463
|
if response is None or response.status >= 400:
|
|
418
464
|
LOGGER.warning('Instagram Playwright perfil HTTP status=%s URL=%s', response.status if response else None, url)
|
|
@@ -421,6 +467,7 @@ def _fetch_profile_with_playwright(username: str, headers: Mapping[str, str] | N
|
|
|
421
467
|
_save_instagram_debug_html(username, page.content())
|
|
422
468
|
except Exception as exc:
|
|
423
469
|
LOGGER.debug('Não foi possível guardar HTML de diagnóstico: %s', exc)
|
|
470
|
+
page.wait_for_timeout(5000)
|
|
424
471
|
try:
|
|
425
472
|
page.wait_for_load_state('networkidle', timeout=10000)
|
|
426
473
|
except (PlaywrightTimeoutError, TimeoutError):
|
|
@@ -445,7 +492,12 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
|
445
492
|
if playwright_user:
|
|
446
493
|
return playwright_user
|
|
447
494
|
LOGGER.warning('Fallback para instaloader activado no perfil @%s.', username)
|
|
448
|
-
|
|
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
|
|
449
501
|
|
|
450
502
|
endpoints = (
|
|
451
503
|
f'https://www.instagram.com/api/v1/users/web_profile_info/?username={username}',
|
|
@@ -785,12 +837,15 @@ def _fetch_posts_with_playwright(reference: Mapping[str, str], limit: int, cooki
|
|
|
785
837
|
with sync_playwright() as playwright:
|
|
786
838
|
browser = playwright.chromium.launch(headless=_instagram_playwright_headless())
|
|
787
839
|
try:
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
)
|
|
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)
|
|
792
846
|
if cookies:
|
|
793
|
-
|
|
847
|
+
context.add_cookies([{'name': name, 'value': value, 'domain': '.instagram.com', 'path': '/'} for name, value in cookies.items()])
|
|
848
|
+
page = context.new_page()
|
|
794
849
|
|
|
795
850
|
def capture_profile_posts(response: Any) -> None:
|
|
796
851
|
if '/api/v1/users/web_profile_info/' not in response.url:
|
|
@@ -813,13 +868,16 @@ def _fetch_posts_with_playwright(reference: Mapping[str, str], limit: int, cooki
|
|
|
813
868
|
|
|
814
869
|
page.on('response', capture_profile_posts)
|
|
815
870
|
try:
|
|
816
|
-
page.goto('https://www.instagram.com/', wait_until='domcontentloaded', timeout=
|
|
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))
|
|
817
874
|
except Exception as exc:
|
|
818
875
|
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=
|
|
876
|
+
response = page.goto(reference['url'], wait_until='domcontentloaded', timeout=30000)
|
|
820
877
|
LOGGER.debug('Instagram Playwright posts URL=%s status=%s', reference['url'], response.status if response else None)
|
|
821
878
|
if response is None or response.status >= 400:
|
|
822
879
|
return []
|
|
880
|
+
page.wait_for_timeout(5000)
|
|
823
881
|
try:
|
|
824
882
|
page.wait_for_load_state('networkidle', timeout=10000)
|
|
825
883
|
except (PlaywrightTimeoutError, TimeoutError):
|
|
@@ -847,6 +905,10 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
|
|
|
847
905
|
if not posts:
|
|
848
906
|
LOGGER.warning('Fallback para instaloader activado nos posts de @%s.', reference['username'])
|
|
849
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')
|
|
850
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))]})
|
|
851
913
|
api_user = _fetch_web_profile_user(reference['username'])
|
|
852
914
|
if api_user:
|
package/package.json
CHANGED