@danhachuel/thunderbolt 0.6.44 → 0.6.46
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 +24 -2
- package/integrations/instagram_public.py +9 -12
- package/package.json +1 -1
package/app/main.py
CHANGED
|
@@ -2352,7 +2352,7 @@ def render_tiktok_channels():
|
|
|
2352
2352
|
for channel in channels:
|
|
2353
2353
|
channel_id = str(channel.get("id") or "")
|
|
2354
2354
|
with st.container(border=True):
|
|
2355
|
-
card_cols = st.columns([0.7, 3.
|
|
2355
|
+
card_cols = st.columns([0.7, 3.05, 1.05, 1.05, 1.25, 1.7, 1.35], gap="small")
|
|
2356
2356
|
with card_cols[0]:
|
|
2357
2357
|
avatar_url = _tiktok_avatar_url(channel)
|
|
2358
2358
|
if avatar_url:
|
|
@@ -2371,6 +2371,18 @@ def render_tiktok_channels():
|
|
|
2371
2371
|
with card_cols[4]:
|
|
2372
2372
|
st.metric("Vídeos", format_metric_number(channel.get("video_count")))
|
|
2373
2373
|
with card_cols[5]:
|
|
2374
|
+
prompt_file = TIKTOK_PROMPT_MASTERS / str(channel.get("default_prompt_master") or channel.get("prompt_master") or "")
|
|
2375
|
+
prompt_content = load_prompt_master_file(prompt_file) if prompt_file.is_file() else ""
|
|
2376
|
+
calculated_time, calculated_source, calculated_words = _channel_average_video_time(channel, prompt_master=prompt_content)
|
|
2377
|
+
average_video_time = st.text_input("Tempo Medio de Video (HH:MM)", value=str(channel.get("average_video_time") or calculated_time), key=f"tiktok_channel_average_video_time_{channel_id}", help=f"Referência: {calculated_words or 0} palavras · {calculated_source}. Use 00:00 para usar o Prompt Master.")
|
|
2378
|
+
if st.button("Guardar tempo", key=f"save_tiktok_channel_average_video_time_{channel_id}", use_container_width=True):
|
|
2379
|
+
if not valid_hhmm(average_video_time):
|
|
2380
|
+
st.error("Use o formato HH:MM, por exemplo 00:02.")
|
|
2381
|
+
else:
|
|
2382
|
+
update_channel(channel_id, {"average_video_time": average_video_time.strip() or "00:00", "average_video_word_count": words_from_channel_time(average_video_time)})
|
|
2383
|
+
st.success("Tempo médio do canal TikTok guardado.")
|
|
2384
|
+
st.rerun()
|
|
2385
|
+
with card_cols[6]:
|
|
2374
2386
|
if st.button("↻", key=f"refresh_tiktok_metrics_{channel_id}", help="Actualizar Seguidores, Vídeos e Curtidas", use_container_width=True):
|
|
2375
2387
|
with st.spinner("A actualizar métricas TikTok…"):
|
|
2376
2388
|
refreshed, message = _refresh_tiktok_channel_metrics(channel)
|
|
@@ -3043,7 +3055,7 @@ def render_channels():
|
|
|
3043
3055
|
channel_id = str(channel["id"])
|
|
3044
3056
|
edit_key = f"edit_channel_{channel_id}"
|
|
3045
3057
|
with st.container(border=True):
|
|
3046
|
-
header_cols = st.columns([0.7,
|
|
3058
|
+
header_cols = st.columns([0.7, 2.75, 1.1, 1.0, 1.35, 1.75, 1.35])
|
|
3047
3059
|
with header_cols[0]:
|
|
3048
3060
|
if channel.get("thumbnail_url"):
|
|
3049
3061
|
st.image(channel["thumbnail_url"], width=64)
|
|
@@ -3060,6 +3072,16 @@ def render_channels():
|
|
|
3060
3072
|
with header_cols[4]:
|
|
3061
3073
|
st.metric("Visualizações", _format_channel_count(channel.get("view_count")))
|
|
3062
3074
|
with header_cols[5]:
|
|
3075
|
+
calculated_time, calculated_source, calculated_words = _channel_average_video_time(channel)
|
|
3076
|
+
average_video_time = st.text_input("Tempo Medio de Video (HH:MM)", value=str(channel.get("average_video_time") or calculated_time), key=f"youtube_channel_average_video_time_{channel_id}", help=f"Referência: {calculated_words or 0} palavras · {calculated_source}. Use 00:00 para usar o Blueprint.")
|
|
3077
|
+
if st.button("Guardar tempo", key=f"save_youtube_channel_average_video_time_{channel_id}", use_container_width=True):
|
|
3078
|
+
if not valid_hhmm(average_video_time):
|
|
3079
|
+
st.error("Use o formato HH:MM, por exemplo 00:02.")
|
|
3080
|
+
else:
|
|
3081
|
+
update_channel(channel_id, {"average_video_time": average_video_time.strip() or "00:00", "average_video_word_count": words_from_channel_time(average_video_time)})
|
|
3082
|
+
st.success("Tempo médio do canal YouTube guardado.")
|
|
3083
|
+
st.rerun()
|
|
3084
|
+
with header_cols[6]:
|
|
3063
3085
|
if st.button("↻", key=f"refresh_youtube_metrics_{channel_id}", help="Actualizar Inscritos, Vídeos e Visualizações", use_container_width=True):
|
|
3064
3086
|
with st.spinner("A actualizar métricas YouTube…"):
|
|
3065
3087
|
refreshed, message = _refresh_youtube_channel_metrics(channel, youtube)
|
|
@@ -24,7 +24,6 @@ except ImportError: # pragma: no cover - Streamlit is a runtime dependency
|
|
|
24
24
|
|
|
25
25
|
LOGGER = logging.getLogger(__name__)
|
|
26
26
|
_WINDOWS_PLAYWRIGHT_CHECKED = False
|
|
27
|
-
_WINDOWS_COOKIE_WARNING_SHOWN = False
|
|
28
27
|
|
|
29
28
|
|
|
30
29
|
ABOUT_ACCOUNT_URL = 'https://i.instagram.com/api/v1/bloks/apps/com.instagram.interactions.about_this_account/'
|
|
@@ -70,19 +69,11 @@ def _streamlit_message(level: str, message: str, key: str) -> None:
|
|
|
70
69
|
|
|
71
70
|
def _ensure_windows_playwright() -> None:
|
|
72
71
|
"""Ensure Chromium exists on Windows before using the Playwright fallback."""
|
|
73
|
-
global _WINDOWS_PLAYWRIGHT_CHECKED
|
|
72
|
+
global _WINDOWS_PLAYWRIGHT_CHECKED
|
|
74
73
|
if os.name != 'nt' or _WINDOWS_PLAYWRIGHT_CHECKED:
|
|
75
74
|
return
|
|
76
75
|
_WINDOWS_PLAYWRIGHT_CHECKED = True
|
|
77
76
|
_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
77
|
try:
|
|
87
78
|
completed = subprocess.run(
|
|
88
79
|
[sys.executable, '-m', 'playwright', 'install', 'chromium'],
|
|
@@ -224,6 +215,7 @@ def _structured_metric_from_json(document: str, *keys: str) -> int | None:
|
|
|
224
215
|
|
|
225
216
|
def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
226
217
|
_ensure_windows_playwright()
|
|
218
|
+
cookies = _instagram_cookies()
|
|
227
219
|
headers = _instagram_headers()
|
|
228
220
|
headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36'
|
|
229
221
|
# The www host is the current public web endpoint. The legacy i host is
|
|
@@ -273,7 +265,7 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
|
273
265
|
for request_header in request_headers:
|
|
274
266
|
for endpoint in endpoints:
|
|
275
267
|
try:
|
|
276
|
-
response = requests.get(endpoint, headers=request_header, timeout=15)
|
|
268
|
+
response = requests.get(endpoint, headers=request_header, cookies=cookies or None, timeout=15)
|
|
277
269
|
LOGGER.debug('Instagram profile URL=%s status=%s content_type=%s', endpoint, response.status_code, response.headers.get('content-type', ''))
|
|
278
270
|
if response.status_code >= 400:
|
|
279
271
|
continue
|
|
@@ -330,6 +322,8 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
|
330
322
|
user_agent=headers['User-Agent'],
|
|
331
323
|
extra_http_headers={'x-ig-app-id': headers['x-ig-app-id'], 'Accept-Language': 'en-US,en;q=0.9'},
|
|
332
324
|
)
|
|
325
|
+
if cookies:
|
|
326
|
+
page.context.add_cookies([{'name': name, 'value': value, 'domain': '.instagram.com', 'path': '/'} for name, value in cookies.items()])
|
|
333
327
|
for endpoint in endpoints:
|
|
334
328
|
try:
|
|
335
329
|
response = page.goto(endpoint, wait_until='domcontentloaded', timeout=30000)
|
|
@@ -632,7 +626,7 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
|
|
|
632
626
|
if api_user.get('is_private'):
|
|
633
627
|
return IntegrationResult(False, 'Esta conta é privada. O Instagram não disponibiliza posts sem uma sessão autenticada.', reference | {'posts': [], 'is_private': True})
|
|
634
628
|
try:
|
|
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)
|
|
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)
|
|
636
630
|
except requests.RequestException as exc:
|
|
637
631
|
LOGGER.warning('Falha HTTP nos posts Instagram URL=%s: %s', reference['url'], exc, exc_info=True)
|
|
638
632
|
return IntegrationResult(False, f'Não foi possível consultar os posts públicos do Instagram: {exc}', reference)
|
|
@@ -662,6 +656,9 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
|
|
|
662
656
|
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36',
|
|
663
657
|
extra_http_headers={'x-ig-app-id': '936619743392459', 'Accept-Language': 'en-US,en;q=0.9'},
|
|
664
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()])
|
|
665
662
|
page.goto(reference['url'], wait_until='domcontentloaded', timeout=30000)
|
|
666
663
|
browser_document = page.content()
|
|
667
664
|
for document in _embedded_json_documents(browser_document):
|
package/package.json
CHANGED