@danhachuel/thunderbolt 0.6.43 → 0.6.45
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.
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import io
|
|
4
|
+
import logging
|
|
4
5
|
import re
|
|
5
6
|
import uuid
|
|
6
7
|
import zipfile
|
|
@@ -20,6 +21,9 @@ from integrations.instagram_public import fetch_public_instagram_posts, fetch_pu
|
|
|
20
21
|
from integrations.meta_social import test_facebook_pages_api_card, test_instagram_api_card
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
LOGGER = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
23
27
|
def _clean(value: Any) -> str:
|
|
24
28
|
return str(value or "").strip()
|
|
25
29
|
|
|
@@ -190,8 +194,8 @@ def _instagram_posts_archive(posts: list[dict[str, Any]]) -> bytes:
|
|
|
190
194
|
elif "webp" in content_type:
|
|
191
195
|
extension = ".webp"
|
|
192
196
|
bundle.writestr(f"post-{index:02d}{extension}", response.content)
|
|
193
|
-
except requests.RequestException:
|
|
194
|
-
|
|
197
|
+
except requests.RequestException as exc:
|
|
198
|
+
LOGGER.warning("Não foi possível descarregar a imagem do post Instagram %s: %s", url, exc)
|
|
195
199
|
manifest.append(f"{index}. {_clean(post.get('url'))}\n{_clean(post.get('caption'))}\n")
|
|
196
200
|
bundle.writestr("posts.txt", "\n".join(manifest))
|
|
197
201
|
return archive.getvalue()
|
|
@@ -212,6 +216,8 @@ def _render_instagram_posts(profile: Mapping[str, Any]) -> None:
|
|
|
212
216
|
if load_clicked or refresh_clicked:
|
|
213
217
|
target = max(10, len(posts)) if refresh_clicked else 10
|
|
214
218
|
posts_ok, posts_message, loaded_posts = _load_instagram_posts(profile, limit=target)
|
|
219
|
+
# Debug temporário removido após validação; manter para reactivar se necessário:
|
|
220
|
+
# st.write({"posts_ok": posts_ok, "posts_message": posts_message, "posts_count": len(loaded_posts)})
|
|
215
221
|
if posts_ok:
|
|
216
222
|
posts = loaded_posts
|
|
217
223
|
_persist_instagram_posts(profile, posts)
|
|
@@ -386,7 +392,8 @@ def _characters(settings: Mapping[str, Any]) -> tuple[list[dict[str, Any]], Any
|
|
|
386
392
|
repository = get_repository(settings)
|
|
387
393
|
items = [item for item in repository.list_influencers() if _clean(item.get("id")) != STANDALONE_CONTENT_INFLUENCER_ID]
|
|
388
394
|
return items, repository
|
|
389
|
-
except
|
|
395
|
+
except Exception as exc:
|
|
396
|
+
LOGGER.warning("Não foi possível carregar as personagens: %s", exc, exc_info=True)
|
|
390
397
|
return [], None
|
|
391
398
|
|
|
392
399
|
|
|
@@ -498,19 +505,39 @@ def _render_instagram_card(profile: dict[str, Any], characters: list[dict[str, A
|
|
|
498
505
|
refresh_col, edit_col = st.columns(2)
|
|
499
506
|
with refresh_col:
|
|
500
507
|
if st.button("↻", help="Actualizar posts, seguidores e seguindo", key=f"refresh_instagram_{profile_id}"):
|
|
501
|
-
|
|
508
|
+
try:
|
|
509
|
+
refreshed_ok, refreshed_message, refreshed = _refresh_instagram_profile(profile)
|
|
510
|
+
except Exception as exc:
|
|
511
|
+
st.error(f"Falha ao actualizar o perfil Instagram: {exc}")
|
|
512
|
+
refreshed_ok, refreshed_message, refreshed = False, str(exc), {}
|
|
502
513
|
if refreshed_ok:
|
|
503
514
|
current_posts = _stored_instagram_posts(profile)
|
|
504
|
-
|
|
515
|
+
try:
|
|
516
|
+
posts_ok, posts_message, refreshed_posts = _load_instagram_posts(refreshed, limit=max(10, len(current_posts)))
|
|
517
|
+
except Exception as exc:
|
|
518
|
+
posts_ok, posts_message, refreshed_posts = False, str(exc), []
|
|
505
519
|
if posts_ok:
|
|
506
520
|
_persist_instagram_posts(profile, refreshed_posts)
|
|
507
|
-
|
|
521
|
+
else:
|
|
522
|
+
st.warning(f"Perfil actualizado, mas os posts não foram actualizados: {posts_message}")
|
|
523
|
+
# Debug temporário removido após validação; manter para reactivar se necessário:
|
|
524
|
+
# st.write({"profile_ok": refreshed_ok, "profile_message": refreshed_message, "profile_data": refreshed})
|
|
525
|
+
# st.write({"posts_ok": posts_ok, "posts_message": posts_message, "posts_count": len(refreshed_posts)})
|
|
526
|
+
try:
|
|
527
|
+
updated = update_channel(profile_id, {**refreshed, "id": profile_id, "country": _profile_country(refreshed) or _profile_country(profile), "language": profile.get("language", ""), "character_id": profile.get("character_id", "")})
|
|
528
|
+
except (OSError, PermissionError) as exc:
|
|
529
|
+
st.error(f"Não foi possível guardar os dados no Windows: {exc}")
|
|
530
|
+
return
|
|
531
|
+
except Exception as exc:
|
|
532
|
+
st.error(f"Falha ao guardar os dados da conta Instagram: {exc}")
|
|
533
|
+
return
|
|
508
534
|
if updated is None:
|
|
509
|
-
st.error("A conta Instagram já não existe no armazenamento local.")
|
|
535
|
+
st.error("A conta Instagram já não existe no armazenamento local ou não foi possível escrever o ficheiro.")
|
|
510
536
|
return
|
|
511
537
|
st.success("Métricas Instagram actualizadas.")
|
|
512
538
|
st.rerun()
|
|
513
|
-
|
|
539
|
+
else:
|
|
540
|
+
st.error(f"Não foi possível actualizar o perfil Instagram: {refreshed_message}")
|
|
514
541
|
with edit_col:
|
|
515
542
|
if st.button("Editar", key=f"edit_instagram_button_{profile_id}", use_container_width=True):
|
|
516
543
|
for field in ("name", "handle", "bio", "country", "language", "posts", "followers", "following"):
|
|
@@ -2,22 +2,106 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
import json
|
|
5
|
+
import logging
|
|
5
6
|
import os
|
|
7
|
+
import sys
|
|
6
8
|
import shutil
|
|
7
9
|
import subprocess
|
|
8
10
|
from dataclasses import dataclass
|
|
9
11
|
from datetime import datetime, timezone
|
|
10
12
|
from html import unescape
|
|
13
|
+
from pathlib import Path
|
|
11
14
|
from typing import Any, Mapping
|
|
12
15
|
from urllib.parse import urlparse
|
|
13
16
|
|
|
14
17
|
import requests
|
|
15
18
|
|
|
19
|
+
try:
|
|
20
|
+
import streamlit as st
|
|
21
|
+
except ImportError: # pragma: no cover - Streamlit is a runtime dependency
|
|
22
|
+
st = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
LOGGER = logging.getLogger(__name__)
|
|
26
|
+
_WINDOWS_PLAYWRIGHT_CHECKED = False
|
|
27
|
+
|
|
16
28
|
|
|
17
29
|
ABOUT_ACCOUNT_URL = 'https://i.instagram.com/api/v1/bloks/apps/com.instagram.interactions.about_this_account/'
|
|
18
30
|
ABOUT_ACCOUNT_BLOKS_VERSION = '8ca96ca267e30c02cf90888d91eeff09627f0e3fd2bd9df472278c9a6c022cbb'
|
|
19
31
|
|
|
20
32
|
|
|
33
|
+
def _load_dotenv_compat() -> None:
|
|
34
|
+
"""Load .env when python-dotenv exists, otherwise parse simple KEY=VALUE lines."""
|
|
35
|
+
try:
|
|
36
|
+
from dotenv import load_dotenv
|
|
37
|
+
load_dotenv()
|
|
38
|
+
return
|
|
39
|
+
except ImportError:
|
|
40
|
+
LOGGER.debug('python-dotenv não está instalado; será usado o parser .env compatível.')
|
|
41
|
+
env_path = Path(__file__).resolve().parents[1] / '.env'
|
|
42
|
+
if not env_path.is_file():
|
|
43
|
+
return
|
|
44
|
+
try:
|
|
45
|
+
for raw_line in env_path.read_text(encoding='utf-8').splitlines():
|
|
46
|
+
line = raw_line.strip()
|
|
47
|
+
if not line or line.startswith('#') or '=' not in line:
|
|
48
|
+
continue
|
|
49
|
+
key, value = line.split('=', 1)
|
|
50
|
+
value = value.strip().strip('"').strip("'")
|
|
51
|
+
os.environ.setdefault(key.strip(), value)
|
|
52
|
+
except OSError as exc:
|
|
53
|
+
LOGGER.warning('Não foi possível ler %s: %s', env_path, exc)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _streamlit_message(level: str, message: str, key: str) -> None:
|
|
57
|
+
"""Show a runtime diagnostic once when Streamlit is available."""
|
|
58
|
+
if st is None:
|
|
59
|
+
return
|
|
60
|
+
try:
|
|
61
|
+
shown_key = f'_instagram_debug_{key}'
|
|
62
|
+
if st.session_state.get(shown_key):
|
|
63
|
+
return
|
|
64
|
+
st.session_state[shown_key] = True
|
|
65
|
+
getattr(st, level)(message)
|
|
66
|
+
except Exception as exc: # Streamlit may be imported outside an active app context.
|
|
67
|
+
LOGGER.debug('Não foi possível mostrar diagnóstico Streamlit: %s', exc)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _ensure_windows_playwright() -> None:
|
|
71
|
+
"""Ensure Chromium exists on Windows before using the Playwright fallback."""
|
|
72
|
+
global _WINDOWS_PLAYWRIGHT_CHECKED
|
|
73
|
+
if os.name != 'nt' or _WINDOWS_PLAYWRIGHT_CHECKED:
|
|
74
|
+
return
|
|
75
|
+
_WINDOWS_PLAYWRIGHT_CHECKED = True
|
|
76
|
+
_load_dotenv_compat()
|
|
77
|
+
try:
|
|
78
|
+
completed = subprocess.run(
|
|
79
|
+
[sys.executable, '-m', 'playwright', 'install', 'chromium'],
|
|
80
|
+
capture_output=True,
|
|
81
|
+
text=True,
|
|
82
|
+
encoding='utf-8',
|
|
83
|
+
errors='replace',
|
|
84
|
+
timeout=180,
|
|
85
|
+
check=False,
|
|
86
|
+
)
|
|
87
|
+
if completed.returncode != 0:
|
|
88
|
+
detail = (completed.stderr or completed.stdout or 'sem detalhes').strip()[-1000:]
|
|
89
|
+
message = f'Não foi possível instalar o Chromium do Playwright no Windows: {detail}'
|
|
90
|
+
LOGGER.warning(message)
|
|
91
|
+
_streamlit_message('error', message + ' Execute: python -m playwright install chromium', 'playwright_install_error')
|
|
92
|
+
else:
|
|
93
|
+
LOGGER.info('Chromium do Playwright verificado/instalado no Windows.')
|
|
94
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
95
|
+
message = f'Falha ao executar a instalação do Chromium no Windows: {exc}'
|
|
96
|
+
LOGGER.warning(message)
|
|
97
|
+
_streamlit_message('error', message + ' Execute: python -m playwright install chromium', 'playwright_install_exception')
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
_load_dotenv_compat()
|
|
101
|
+
if os.name == 'nt':
|
|
102
|
+
_ensure_windows_playwright()
|
|
103
|
+
|
|
104
|
+
|
|
21
105
|
def _instagram_cookies() -> dict[str, str]:
|
|
22
106
|
cookies = {}
|
|
23
107
|
sessionid = os.getenv('INSTAGRAM_SESSIONID') or os.getenv('IG_SESSIONID')
|
|
@@ -130,6 +214,8 @@ def _structured_metric_from_json(document: str, *keys: str) -> int | None:
|
|
|
130
214
|
|
|
131
215
|
|
|
132
216
|
def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
217
|
+
_ensure_windows_playwright()
|
|
218
|
+
cookies = _instagram_cookies()
|
|
133
219
|
headers = _instagram_headers()
|
|
134
220
|
headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36'
|
|
135
221
|
# The www host is the current public web endpoint. The legacy i host is
|
|
@@ -179,11 +265,14 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
|
179
265
|
for request_header in request_headers:
|
|
180
266
|
for endpoint in endpoints:
|
|
181
267
|
try:
|
|
182
|
-
response = requests.get(endpoint, headers=request_header, timeout=15)
|
|
268
|
+
response = requests.get(endpoint, headers=request_header, cookies=cookies or None, timeout=15)
|
|
269
|
+
LOGGER.debug('Instagram profile URL=%s status=%s content_type=%s', endpoint, response.status_code, response.headers.get('content-type', ''))
|
|
183
270
|
if response.status_code >= 400:
|
|
184
271
|
continue
|
|
185
272
|
payload = response.json()
|
|
273
|
+
LOGGER.debug('Instagram profile URL=%s response=JSON', endpoint)
|
|
186
274
|
except (requests.RequestException, ValueError, AttributeError, StopIteration):
|
|
275
|
+
LOGGER.warning('Falha na requisição JSON do perfil Instagram URL=%s', endpoint, exc_info=True)
|
|
187
276
|
if best_user:
|
|
188
277
|
return best_user
|
|
189
278
|
continue
|
|
@@ -195,6 +284,7 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
|
195
284
|
|
|
196
285
|
curl = shutil.which('curl')
|
|
197
286
|
if curl:
|
|
287
|
+
LOGGER.debug('Fallback curl disponível em %s', curl)
|
|
198
288
|
for endpoint in endpoints:
|
|
199
289
|
try:
|
|
200
290
|
completed = subprocess.run(
|
|
@@ -207,13 +297,18 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
|
207
297
|
check=False,
|
|
208
298
|
)
|
|
209
299
|
payload = json.loads(completed.stdout) if completed.returncode == 0 and completed.stdout else None
|
|
300
|
+
LOGGER.debug('Instagram curl URL=%s returncode=%s stdout_bytes=%s stderr=%s', endpoint, completed.returncode, len(completed.stdout or ''), (completed.stderr or '').strip()[-500:])
|
|
210
301
|
user = extract_user(payload)
|
|
211
302
|
if isinstance(user, dict):
|
|
212
303
|
best_user = merge_profile(best_user, user)
|
|
213
304
|
if profile_quality(best_user) >= 4:
|
|
214
305
|
return best_user
|
|
215
306
|
except (OSError, subprocess.SubprocessError, ValueError):
|
|
307
|
+
LOGGER.warning('Falha no fallback curl do perfil Instagram URL=%s', endpoint, exc_info=True)
|
|
216
308
|
continue
|
|
309
|
+
else:
|
|
310
|
+
LOGGER.warning('curl não está disponível no PATH; a tentar Playwright.')
|
|
311
|
+
_streamlit_message('warning', 'curl não foi encontrado no PATH do Windows; será tentado o fallback Playwright.', 'curl_missing')
|
|
217
312
|
|
|
218
313
|
# Em algumas instalações Windows o Instagram devolve a página HTML aos
|
|
219
314
|
# clientes HTTP, embora entregue o JSON completo a um navegador Chromium.
|
|
@@ -227,6 +322,8 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
|
227
322
|
user_agent=headers['User-Agent'],
|
|
228
323
|
extra_http_headers={'x-ig-app-id': headers['x-ig-app-id'], 'Accept-Language': 'en-US,en;q=0.9'},
|
|
229
324
|
)
|
|
325
|
+
if cookies:
|
|
326
|
+
page.context.add_cookies([{'name': name, 'value': value, 'domain': '.instagram.com', 'path': '/'} for name, value in cookies.items()])
|
|
230
327
|
for endpoint in endpoints:
|
|
231
328
|
try:
|
|
232
329
|
response = page.goto(endpoint, wait_until='domcontentloaded', timeout=30000)
|
|
@@ -242,8 +339,9 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
|
242
339
|
continue
|
|
243
340
|
finally:
|
|
244
341
|
browser.close()
|
|
245
|
-
except
|
|
246
|
-
|
|
342
|
+
except Exception as exc:
|
|
343
|
+
LOGGER.warning('Falha no fallback Playwright do perfil Instagram: %s', exc, exc_info=True)
|
|
344
|
+
_streamlit_message('error', f'Fallback Playwright do perfil Instagram falhou: {exc}. Execute: python -m playwright install chromium', 'profile_playwright_error')
|
|
247
345
|
return best_user
|
|
248
346
|
|
|
249
347
|
|
|
@@ -294,13 +392,29 @@ def _fetch_instagram_about_country(user_id: Any) -> str:
|
|
|
294
392
|
}
|
|
295
393
|
try:
|
|
296
394
|
response = requests.post(ABOUT_ACCOUNT_URL, data=payload, headers=_instagram_headers(), cookies=cookies, timeout=20)
|
|
395
|
+
LOGGER.debug('Instagram about-account URL=%s status=%s content_type=%s', ABOUT_ACCOUNT_URL, response.status_code, response.headers.get('content-type', ''))
|
|
297
396
|
if response.status_code >= 400:
|
|
298
397
|
return ''
|
|
299
|
-
|
|
300
|
-
|
|
398
|
+
country = _country_from_bloks(response.json())
|
|
399
|
+
LOGGER.debug('Instagram about-account country_found=%s', bool(country))
|
|
400
|
+
return country
|
|
401
|
+
except (requests.RequestException, ValueError, TypeError) as exc:
|
|
402
|
+
LOGGER.warning('Falha ao consultar país público do Instagram: %s', exc, exc_info=True)
|
|
301
403
|
return ''
|
|
302
404
|
|
|
303
405
|
|
|
406
|
+
def _country_from_bio_fallback(bio: str) -> str:
|
|
407
|
+
patterns = (
|
|
408
|
+
r'\b(?:from|based in|located in|living in)\s+([A-Za-zÀ-ÿ][A-Za-zÀ-ÿ .-]{1,60})',
|
|
409
|
+
r'\b(?:de|baseado em|morando em|localizado em)\s+([A-Za-zÀ-ÿ][A-Za-zÀ-ÿ .-]{1,60})',
|
|
410
|
+
)
|
|
411
|
+
for pattern in patterns:
|
|
412
|
+
match = re.search(pattern, bio or '', flags=re.IGNORECASE)
|
|
413
|
+
if match:
|
|
414
|
+
return match.group(1).strip(' .,!;:')
|
|
415
|
+
return ''
|
|
416
|
+
|
|
417
|
+
|
|
304
418
|
def _profile_data_from_api(user: Mapping[str, Any], reference: Mapping[str, str]) -> dict[str, Any]:
|
|
305
419
|
followers = _profile_metric(user, 'edge_followed_by', 'followers', 'follower_count', 'followerCount')
|
|
306
420
|
following = _profile_metric(user, 'edge_follow', 'follows', 'following', 'following_count', 'followingCount')
|
|
@@ -310,6 +424,8 @@ def _profile_data_from_api(user: Mapping[str, Any], reference: Mapping[str, str]
|
|
|
310
424
|
media = media if media is not None else _profile_metric(user, 'posts', 'post_count', 'media_count')
|
|
311
425
|
biography = str(user.get('biography') or user.get('bio') or user.get('description') or '').strip()
|
|
312
426
|
country = extract_public_instagram_country(user) or _fetch_instagram_about_country(user.get('id') or user.get('pk'))
|
|
427
|
+
country = country or _country_from_bio_fallback(biography)
|
|
428
|
+
LOGGER.debug('Instagram profile extracted bio=%s followers=%s following=%s posts=%s country=%s', bool(biography), followers, following, media, country or '—')
|
|
313
429
|
return {
|
|
314
430
|
'id': f"instagram_{reference['username']}",
|
|
315
431
|
**reference,
|
|
@@ -387,6 +503,7 @@ def _profile_metric(user: Mapping[str, Any], *keys: str) -> int | None:
|
|
|
387
503
|
|
|
388
504
|
|
|
389
505
|
def fetch_public_instagram_profile(source: str) -> IntegrationResult:
|
|
506
|
+
LOGGER.debug('fetch_public_instagram_profile source=%s', source)
|
|
390
507
|
try:
|
|
391
508
|
reference = normalize_instagram_reference(source)
|
|
392
509
|
except ValueError as exc:
|
|
@@ -397,7 +514,9 @@ def fetch_public_instagram_profile(source: str) -> IntegrationResult:
|
|
|
397
514
|
try:
|
|
398
515
|
response = requests.get(reference['url'], headers={'User-Agent': 'Mozilla/5.0', 'Accept-Language': 'pt-BR,pt;q=0.9,en;q=0.8'}, timeout=12)
|
|
399
516
|
except requests.RequestException as exc:
|
|
517
|
+
LOGGER.warning('Falha HTTP no perfil Instagram URL=%s: %s', reference['url'], exc, exc_info=True)
|
|
400
518
|
return IntegrationResult(False, f'Não foi possível consultar o perfil público do Instagram: {exc}', reference)
|
|
519
|
+
LOGGER.debug('Instagram HTML profile URL=%s status=%s content_type=%s response=HTML', reference['url'], response.status_code, response.headers.get('content-type', ''))
|
|
401
520
|
if response.status_code >= 400:
|
|
402
521
|
return IntegrationResult(False, f'O Instagram devolveu HTTP {response.status_code}. Confirme o @handle ou use o cadastro manual.', reference | {'status_code': response.status_code})
|
|
403
522
|
title = _meta(response.text, 'og:title') or reference['username']
|
|
@@ -422,6 +541,8 @@ def fetch_public_instagram_profile(source: str) -> IntegrationResult:
|
|
|
422
541
|
embedded_country = extract_public_instagram_country(document)
|
|
423
542
|
if embedded_country:
|
|
424
543
|
break
|
|
544
|
+
embedded_country = embedded_country or _country_from_bio_fallback(description)
|
|
545
|
+
LOGGER.debug('Instagram HTML profile extracted bio=%s followers=%s following=%s posts=%s country=%s', bool(description), followers, following, posts, embedded_country or '—')
|
|
425
546
|
data = {'id': f"instagram_{reference['username']}", **reference, 'name': title.split('(')[0].strip() or reference['username'], 'bio': normalize_instagram_bio(description), 'bio_raw': description, 'country': embedded_country, 'avatar_url': avatar_url, 'subscriber_count': followers, 'following_count': following, 'post_count': posts, 'public_lookup': True, 'metrics_source': 'instagram_public_page', 'last_public_lookup_at': datetime.now(timezone.utc).isoformat()}
|
|
426
547
|
return IntegrationResult(True, 'Perfil Instagram encontrado publicamente.', data)
|
|
427
548
|
|
|
@@ -484,6 +605,8 @@ def _post_from_node(node: dict[str, Any]) -> dict[str, Any] | None:
|
|
|
484
605
|
|
|
485
606
|
|
|
486
607
|
def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationResult:
|
|
608
|
+
LOGGER.debug('fetch_public_instagram_posts source=%s limit=%s', source, limit)
|
|
609
|
+
_ensure_windows_playwright()
|
|
487
610
|
try:
|
|
488
611
|
reference = normalize_instagram_reference(source)
|
|
489
612
|
except ValueError as exc:
|
|
@@ -498,13 +621,16 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
|
|
|
498
621
|
if post:
|
|
499
622
|
api_posts.append(post)
|
|
500
623
|
if api_posts:
|
|
624
|
+
LOGGER.debug('Instagram API posts extracted=%s response=JSON', len(api_posts))
|
|
501
625
|
return IntegrationResult(True, 'Posts públicos encontrados.', reference | {'posts': api_posts[:max(1, int(limit))]})
|
|
502
626
|
if api_user.get('is_private'):
|
|
503
627
|
return IntegrationResult(False, 'Esta conta é privada. O Instagram não disponibiliza posts sem uma sessão autenticada.', reference | {'posts': [], 'is_private': True})
|
|
504
628
|
try:
|
|
505
|
-
response = requests.get(reference['url'], headers={'User-Agent': 'Mozilla/5.0', 'Accept-Language': 'pt-BR,pt;q=0.9,en;q=0.8'}, timeout=15)
|
|
629
|
+
response = requests.get(reference['url'], headers={'User-Agent': 'Mozilla/5.0', 'Accept-Language': 'pt-BR,pt;q=0.9,en;q=0.8'}, cookies=_instagram_cookies() or None, timeout=15)
|
|
506
630
|
except requests.RequestException as exc:
|
|
631
|
+
LOGGER.warning('Falha HTTP nos posts Instagram URL=%s: %s', reference['url'], exc, exc_info=True)
|
|
507
632
|
return IntegrationResult(False, f'Não foi possível consultar os posts públicos do Instagram: {exc}', reference)
|
|
633
|
+
LOGGER.debug('Instagram HTML posts URL=%s status=%s content_type=%s response=HTML', reference['url'], response.status_code, response.headers.get('content-type', ''))
|
|
508
634
|
if response.status_code >= 400:
|
|
509
635
|
return IntegrationResult(False, f'O Instagram devolveu HTTP {response.status_code} ao consultar os posts.', reference | {'status_code': response.status_code})
|
|
510
636
|
posts: list[dict[str, Any]] = []
|
|
@@ -530,6 +656,9 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
|
|
|
530
656
|
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36',
|
|
531
657
|
extra_http_headers={'x-ig-app-id': '936619743392459', 'Accept-Language': 'en-US,en;q=0.9'},
|
|
532
658
|
)
|
|
659
|
+
cookies = _instagram_cookies()
|
|
660
|
+
if cookies:
|
|
661
|
+
page.context.add_cookies([{'name': name, 'value': value, 'domain': '.instagram.com', 'path': '/'} for name, value in cookies.items()])
|
|
533
662
|
page.goto(reference['url'], wait_until='domcontentloaded', timeout=30000)
|
|
534
663
|
browser_document = page.content()
|
|
535
664
|
for document in _embedded_json_documents(browser_document):
|
|
@@ -545,8 +674,10 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
|
|
|
545
674
|
break
|
|
546
675
|
finally:
|
|
547
676
|
browser.close()
|
|
548
|
-
except
|
|
549
|
-
|
|
677
|
+
except Exception as exc:
|
|
678
|
+
LOGGER.warning('Falha no fallback Playwright dos posts Instagram: %s', exc, exc_info=True)
|
|
679
|
+
_streamlit_message('error', f'Fallback Playwright dos posts Instagram falhou: {exc}. Execute: python -m playwright install chromium', 'posts_playwright_error')
|
|
680
|
+
LOGGER.debug('Instagram posts extracted=%s limit=%s', len(posts), limit)
|
|
550
681
|
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))]})
|
|
551
682
|
|
|
552
683
|
|
package/package.json
CHANGED