@danhachuel/thunderbolt 0.6.42 → 0.6.44
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/app/main.py +22 -0
- package/app/social_networks_ui.py +35 -8
- package/integrations/instagram_public.py +140 -6
- package/package.json +1 -1
package/app/main.py
CHANGED
|
@@ -39,6 +39,28 @@ import streamlit as st
|
|
|
39
39
|
ROOT = Path(__file__).resolve().parents[1]
|
|
40
40
|
if str(ROOT) not in sys.path:
|
|
41
41
|
sys.path.insert(0, str(ROOT))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _load_local_env() -> None:
|
|
45
|
+
for env_path in (ROOT / ".env", Path.cwd() / ".env"):
|
|
46
|
+
try:
|
|
47
|
+
if not env_path.is_file():
|
|
48
|
+
continue
|
|
49
|
+
for raw_line in env_path.read_text(encoding="utf-8").splitlines():
|
|
50
|
+
line = raw_line.strip()
|
|
51
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
52
|
+
continue
|
|
53
|
+
key, value = line.split("=", 1)
|
|
54
|
+
key = key.strip()
|
|
55
|
+
value = value.strip().strip("'\"")
|
|
56
|
+
if key:
|
|
57
|
+
os.environ.setdefault(key, value)
|
|
58
|
+
except OSError as exc:
|
|
59
|
+
print(f"DEBUG: não foi possível ler {env_path}: {exc}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
_load_local_env()
|
|
63
|
+
|
|
42
64
|
try:
|
|
43
65
|
APP_VERSION = json.loads((ROOT / "package.json").read_text(encoding="utf-8")).get("version", "")
|
|
44
66
|
except (OSError, json.JSONDecodeError):
|
|
@@ -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,115 @@ 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
|
+
_WINDOWS_COOKIE_WARNING_SHOWN = False
|
|
28
|
+
|
|
16
29
|
|
|
17
30
|
ABOUT_ACCOUNT_URL = 'https://i.instagram.com/api/v1/bloks/apps/com.instagram.interactions.about_this_account/'
|
|
18
31
|
ABOUT_ACCOUNT_BLOKS_VERSION = '8ca96ca267e30c02cf90888d91eeff09627f0e3fd2bd9df472278c9a6c022cbb'
|
|
19
32
|
|
|
20
33
|
|
|
34
|
+
def _load_dotenv_compat() -> None:
|
|
35
|
+
"""Load .env when python-dotenv exists, otherwise parse simple KEY=VALUE lines."""
|
|
36
|
+
try:
|
|
37
|
+
from dotenv import load_dotenv
|
|
38
|
+
load_dotenv()
|
|
39
|
+
return
|
|
40
|
+
except ImportError:
|
|
41
|
+
LOGGER.debug('python-dotenv não está instalado; será usado o parser .env compatível.')
|
|
42
|
+
env_path = Path(__file__).resolve().parents[1] / '.env'
|
|
43
|
+
if not env_path.is_file():
|
|
44
|
+
return
|
|
45
|
+
try:
|
|
46
|
+
for raw_line in env_path.read_text(encoding='utf-8').splitlines():
|
|
47
|
+
line = raw_line.strip()
|
|
48
|
+
if not line or line.startswith('#') or '=' not in line:
|
|
49
|
+
continue
|
|
50
|
+
key, value = line.split('=', 1)
|
|
51
|
+
value = value.strip().strip('"').strip("'")
|
|
52
|
+
os.environ.setdefault(key.strip(), value)
|
|
53
|
+
except OSError as exc:
|
|
54
|
+
LOGGER.warning('Não foi possível ler %s: %s', env_path, exc)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _streamlit_message(level: str, message: str, key: str) -> None:
|
|
58
|
+
"""Show a runtime diagnostic once when Streamlit is available."""
|
|
59
|
+
if st is None:
|
|
60
|
+
return
|
|
61
|
+
try:
|
|
62
|
+
shown_key = f'_instagram_debug_{key}'
|
|
63
|
+
if st.session_state.get(shown_key):
|
|
64
|
+
return
|
|
65
|
+
st.session_state[shown_key] = True
|
|
66
|
+
getattr(st, level)(message)
|
|
67
|
+
except Exception as exc: # Streamlit may be imported outside an active app context.
|
|
68
|
+
LOGGER.debug('Não foi possível mostrar diagnóstico Streamlit: %s', exc)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _ensure_windows_playwright() -> None:
|
|
72
|
+
"""Ensure Chromium exists on Windows before using the Playwright fallback."""
|
|
73
|
+
global _WINDOWS_PLAYWRIGHT_CHECKED, _WINDOWS_COOKIE_WARNING_SHOWN
|
|
74
|
+
if os.name != 'nt' or _WINDOWS_PLAYWRIGHT_CHECKED:
|
|
75
|
+
return
|
|
76
|
+
_WINDOWS_PLAYWRIGHT_CHECKED = True
|
|
77
|
+
_load_dotenv_compat()
|
|
78
|
+
cookie_names = ('INSTAGRAM_SESSIONID', 'IG_SESSIONID')
|
|
79
|
+
if not any(os.getenv(name, '').strip() for name in cookie_names) and not _WINDOWS_COOKIE_WARNING_SHOWN:
|
|
80
|
+
_WINDOWS_COOKIE_WARNING_SHOWN = True
|
|
81
|
+
message = ('Windows: cookies Instagram não configurados. O país da conta pode ficar indisponível; '
|
|
82
|
+
'configure INSTAGRAM_SESSIONID (e, se necessário, INSTAGRAM_CSRFTOKEN, '
|
|
83
|
+
'INSTAGRAM_DS_USER_ID, INSTAGRAM_MID e INSTAGRAM_IG_DID) no ficheiro .env.')
|
|
84
|
+
LOGGER.warning(message)
|
|
85
|
+
_streamlit_message('warning', message, 'windows_cookie_warning')
|
|
86
|
+
try:
|
|
87
|
+
completed = subprocess.run(
|
|
88
|
+
[sys.executable, '-m', 'playwright', 'install', 'chromium'],
|
|
89
|
+
capture_output=True,
|
|
90
|
+
text=True,
|
|
91
|
+
encoding='utf-8',
|
|
92
|
+
errors='replace',
|
|
93
|
+
timeout=180,
|
|
94
|
+
check=False,
|
|
95
|
+
)
|
|
96
|
+
if completed.returncode != 0:
|
|
97
|
+
detail = (completed.stderr or completed.stdout or 'sem detalhes').strip()[-1000:]
|
|
98
|
+
message = f'Não foi possível instalar o Chromium do Playwright no Windows: {detail}'
|
|
99
|
+
LOGGER.warning(message)
|
|
100
|
+
_streamlit_message('error', message + ' Execute: python -m playwright install chromium', 'playwright_install_error')
|
|
101
|
+
else:
|
|
102
|
+
LOGGER.info('Chromium do Playwright verificado/instalado no Windows.')
|
|
103
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
104
|
+
message = f'Falha ao executar a instalação do Chromium no Windows: {exc}'
|
|
105
|
+
LOGGER.warning(message)
|
|
106
|
+
_streamlit_message('error', message + ' Execute: python -m playwright install chromium', 'playwright_install_exception')
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
_load_dotenv_compat()
|
|
110
|
+
if os.name == 'nt':
|
|
111
|
+
_ensure_windows_playwright()
|
|
112
|
+
|
|
113
|
+
|
|
21
114
|
def _instagram_cookies() -> dict[str, str]:
|
|
22
115
|
cookies = {}
|
|
23
116
|
sessionid = os.getenv('INSTAGRAM_SESSIONID') or os.getenv('IG_SESSIONID')
|
|
@@ -130,6 +223,7 @@ def _structured_metric_from_json(document: str, *keys: str) -> int | None:
|
|
|
130
223
|
|
|
131
224
|
|
|
132
225
|
def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
226
|
+
_ensure_windows_playwright()
|
|
133
227
|
headers = _instagram_headers()
|
|
134
228
|
headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36'
|
|
135
229
|
# The www host is the current public web endpoint. The legacy i host is
|
|
@@ -180,10 +274,13 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
|
180
274
|
for endpoint in endpoints:
|
|
181
275
|
try:
|
|
182
276
|
response = requests.get(endpoint, headers=request_header, timeout=15)
|
|
277
|
+
LOGGER.debug('Instagram profile URL=%s status=%s content_type=%s', endpoint, response.status_code, response.headers.get('content-type', ''))
|
|
183
278
|
if response.status_code >= 400:
|
|
184
279
|
continue
|
|
185
280
|
payload = response.json()
|
|
281
|
+
LOGGER.debug('Instagram profile URL=%s response=JSON', endpoint)
|
|
186
282
|
except (requests.RequestException, ValueError, AttributeError, StopIteration):
|
|
283
|
+
LOGGER.warning('Falha na requisição JSON do perfil Instagram URL=%s', endpoint, exc_info=True)
|
|
187
284
|
if best_user:
|
|
188
285
|
return best_user
|
|
189
286
|
continue
|
|
@@ -195,6 +292,7 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
|
195
292
|
|
|
196
293
|
curl = shutil.which('curl')
|
|
197
294
|
if curl:
|
|
295
|
+
LOGGER.debug('Fallback curl disponível em %s', curl)
|
|
198
296
|
for endpoint in endpoints:
|
|
199
297
|
try:
|
|
200
298
|
completed = subprocess.run(
|
|
@@ -207,13 +305,18 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
|
207
305
|
check=False,
|
|
208
306
|
)
|
|
209
307
|
payload = json.loads(completed.stdout) if completed.returncode == 0 and completed.stdout else None
|
|
308
|
+
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
309
|
user = extract_user(payload)
|
|
211
310
|
if isinstance(user, dict):
|
|
212
311
|
best_user = merge_profile(best_user, user)
|
|
213
312
|
if profile_quality(best_user) >= 4:
|
|
214
313
|
return best_user
|
|
215
314
|
except (OSError, subprocess.SubprocessError, ValueError):
|
|
315
|
+
LOGGER.warning('Falha no fallback curl do perfil Instagram URL=%s', endpoint, exc_info=True)
|
|
216
316
|
continue
|
|
317
|
+
else:
|
|
318
|
+
LOGGER.warning('curl não está disponível no PATH; a tentar Playwright.')
|
|
319
|
+
_streamlit_message('warning', 'curl não foi encontrado no PATH do Windows; será tentado o fallback Playwright.', 'curl_missing')
|
|
217
320
|
|
|
218
321
|
# Em algumas instalações Windows o Instagram devolve a página HTML aos
|
|
219
322
|
# clientes HTTP, embora entregue o JSON completo a um navegador Chromium.
|
|
@@ -242,8 +345,9 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
|
242
345
|
continue
|
|
243
346
|
finally:
|
|
244
347
|
browser.close()
|
|
245
|
-
except
|
|
246
|
-
|
|
348
|
+
except Exception as exc:
|
|
349
|
+
LOGGER.warning('Falha no fallback Playwright do perfil Instagram: %s', exc, exc_info=True)
|
|
350
|
+
_streamlit_message('error', f'Fallback Playwright do perfil Instagram falhou: {exc}. Execute: python -m playwright install chromium', 'profile_playwright_error')
|
|
247
351
|
return best_user
|
|
248
352
|
|
|
249
353
|
|
|
@@ -294,13 +398,29 @@ def _fetch_instagram_about_country(user_id: Any) -> str:
|
|
|
294
398
|
}
|
|
295
399
|
try:
|
|
296
400
|
response = requests.post(ABOUT_ACCOUNT_URL, data=payload, headers=_instagram_headers(), cookies=cookies, timeout=20)
|
|
401
|
+
LOGGER.debug('Instagram about-account URL=%s status=%s content_type=%s', ABOUT_ACCOUNT_URL, response.status_code, response.headers.get('content-type', ''))
|
|
297
402
|
if response.status_code >= 400:
|
|
298
403
|
return ''
|
|
299
|
-
|
|
300
|
-
|
|
404
|
+
country = _country_from_bloks(response.json())
|
|
405
|
+
LOGGER.debug('Instagram about-account country_found=%s', bool(country))
|
|
406
|
+
return country
|
|
407
|
+
except (requests.RequestException, ValueError, TypeError) as exc:
|
|
408
|
+
LOGGER.warning('Falha ao consultar país público do Instagram: %s', exc, exc_info=True)
|
|
301
409
|
return ''
|
|
302
410
|
|
|
303
411
|
|
|
412
|
+
def _country_from_bio_fallback(bio: str) -> str:
|
|
413
|
+
patterns = (
|
|
414
|
+
r'\b(?:from|based in|located in|living in)\s+([A-Za-zÀ-ÿ][A-Za-zÀ-ÿ .-]{1,60})',
|
|
415
|
+
r'\b(?:de|baseado em|morando em|localizado em)\s+([A-Za-zÀ-ÿ][A-Za-zÀ-ÿ .-]{1,60})',
|
|
416
|
+
)
|
|
417
|
+
for pattern in patterns:
|
|
418
|
+
match = re.search(pattern, bio or '', flags=re.IGNORECASE)
|
|
419
|
+
if match:
|
|
420
|
+
return match.group(1).strip(' .,!;:')
|
|
421
|
+
return ''
|
|
422
|
+
|
|
423
|
+
|
|
304
424
|
def _profile_data_from_api(user: Mapping[str, Any], reference: Mapping[str, str]) -> dict[str, Any]:
|
|
305
425
|
followers = _profile_metric(user, 'edge_followed_by', 'followers', 'follower_count', 'followerCount')
|
|
306
426
|
following = _profile_metric(user, 'edge_follow', 'follows', 'following', 'following_count', 'followingCount')
|
|
@@ -310,6 +430,8 @@ def _profile_data_from_api(user: Mapping[str, Any], reference: Mapping[str, str]
|
|
|
310
430
|
media = media if media is not None else _profile_metric(user, 'posts', 'post_count', 'media_count')
|
|
311
431
|
biography = str(user.get('biography') or user.get('bio') or user.get('description') or '').strip()
|
|
312
432
|
country = extract_public_instagram_country(user) or _fetch_instagram_about_country(user.get('id') or user.get('pk'))
|
|
433
|
+
country = country or _country_from_bio_fallback(biography)
|
|
434
|
+
LOGGER.debug('Instagram profile extracted bio=%s followers=%s following=%s posts=%s country=%s', bool(biography), followers, following, media, country or '—')
|
|
313
435
|
return {
|
|
314
436
|
'id': f"instagram_{reference['username']}",
|
|
315
437
|
**reference,
|
|
@@ -387,6 +509,7 @@ def _profile_metric(user: Mapping[str, Any], *keys: str) -> int | None:
|
|
|
387
509
|
|
|
388
510
|
|
|
389
511
|
def fetch_public_instagram_profile(source: str) -> IntegrationResult:
|
|
512
|
+
LOGGER.debug('fetch_public_instagram_profile source=%s', source)
|
|
390
513
|
try:
|
|
391
514
|
reference = normalize_instagram_reference(source)
|
|
392
515
|
except ValueError as exc:
|
|
@@ -397,7 +520,9 @@ def fetch_public_instagram_profile(source: str) -> IntegrationResult:
|
|
|
397
520
|
try:
|
|
398
521
|
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
522
|
except requests.RequestException as exc:
|
|
523
|
+
LOGGER.warning('Falha HTTP no perfil Instagram URL=%s: %s', reference['url'], exc, exc_info=True)
|
|
400
524
|
return IntegrationResult(False, f'Não foi possível consultar o perfil público do Instagram: {exc}', reference)
|
|
525
|
+
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
526
|
if response.status_code >= 400:
|
|
402
527
|
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
528
|
title = _meta(response.text, 'og:title') or reference['username']
|
|
@@ -422,6 +547,8 @@ def fetch_public_instagram_profile(source: str) -> IntegrationResult:
|
|
|
422
547
|
embedded_country = extract_public_instagram_country(document)
|
|
423
548
|
if embedded_country:
|
|
424
549
|
break
|
|
550
|
+
embedded_country = embedded_country or _country_from_bio_fallback(description)
|
|
551
|
+
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
552
|
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
553
|
return IntegrationResult(True, 'Perfil Instagram encontrado publicamente.', data)
|
|
427
554
|
|
|
@@ -484,6 +611,8 @@ def _post_from_node(node: dict[str, Any]) -> dict[str, Any] | None:
|
|
|
484
611
|
|
|
485
612
|
|
|
486
613
|
def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationResult:
|
|
614
|
+
LOGGER.debug('fetch_public_instagram_posts source=%s limit=%s', source, limit)
|
|
615
|
+
_ensure_windows_playwright()
|
|
487
616
|
try:
|
|
488
617
|
reference = normalize_instagram_reference(source)
|
|
489
618
|
except ValueError as exc:
|
|
@@ -498,13 +627,16 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
|
|
|
498
627
|
if post:
|
|
499
628
|
api_posts.append(post)
|
|
500
629
|
if api_posts:
|
|
630
|
+
LOGGER.debug('Instagram API posts extracted=%s response=JSON', len(api_posts))
|
|
501
631
|
return IntegrationResult(True, 'Posts públicos encontrados.', reference | {'posts': api_posts[:max(1, int(limit))]})
|
|
502
632
|
if api_user.get('is_private'):
|
|
503
633
|
return IntegrationResult(False, 'Esta conta é privada. O Instagram não disponibiliza posts sem uma sessão autenticada.', reference | {'posts': [], 'is_private': True})
|
|
504
634
|
try:
|
|
505
635
|
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)
|
|
506
636
|
except requests.RequestException as exc:
|
|
637
|
+
LOGGER.warning('Falha HTTP nos posts Instagram URL=%s: %s', reference['url'], exc, exc_info=True)
|
|
507
638
|
return IntegrationResult(False, f'Não foi possível consultar os posts públicos do Instagram: {exc}', reference)
|
|
639
|
+
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
640
|
if response.status_code >= 400:
|
|
509
641
|
return IntegrationResult(False, f'O Instagram devolveu HTTP {response.status_code} ao consultar os posts.', reference | {'status_code': response.status_code})
|
|
510
642
|
posts: list[dict[str, Any]] = []
|
|
@@ -545,8 +677,10 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
|
|
|
545
677
|
break
|
|
546
678
|
finally:
|
|
547
679
|
browser.close()
|
|
548
|
-
except
|
|
549
|
-
|
|
680
|
+
except Exception as exc:
|
|
681
|
+
LOGGER.warning('Falha no fallback Playwright dos posts Instagram: %s', exc, exc_info=True)
|
|
682
|
+
_streamlit_message('error', f'Fallback Playwright dos posts Instagram falhou: {exc}. Execute: python -m playwright install chromium', 'posts_playwright_error')
|
|
683
|
+
LOGGER.debug('Instagram posts extracted=%s limit=%s', len(posts), limit)
|
|
550
684
|
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
685
|
|
|
552
686
|
|
package/package.json
CHANGED