@danhachuel/thunderbolt 0.6.47 → 0.6.49
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 +11 -11
- package/hermes_ui/video_length.py +4 -1
- package/integrations/instagram_public.py +125 -102
- package/package.json +1 -1
package/app/main.py
CHANGED
|
@@ -116,7 +116,7 @@ from hermes_ui.update_manager import check_version, restart_current_process, upd
|
|
|
116
116
|
|
|
117
117
|
from hermes_ui.script_documents import list_script_documents, read_script_document, save_script_document, script_storage_path
|
|
118
118
|
from hermes_ui.script_generation import generate_script_document
|
|
119
|
-
from hermes_ui.video_length import channel_video_length, length_generation_settings, words_from_channel_time
|
|
119
|
+
from hermes_ui.video_length import DEFAULT_AVERAGE_VIDEO_TIME, channel_video_length, length_generation_settings, words_from_channel_time
|
|
120
120
|
from hermes_ui.voice_preview import DEFAULT_SAMPLE, load_preview_file, synthesize_preview
|
|
121
121
|
from hermes_ui.elevenlabs_voices import ElevenLabsVoicesError, cached_personal_voices, fetch_personal_voices, personal_voice_options
|
|
122
122
|
from hermes_ui.thumbnail_generation import ThumbnailGenerationError, generate_thumbnail_image
|
|
@@ -2374,12 +2374,12 @@ def render_tiktok_channels():
|
|
|
2374
2374
|
prompt_file = TIKTOK_PROMPT_MASTERS / str(channel.get("default_prompt_master") or channel.get("prompt_master") or "")
|
|
2375
2375
|
prompt_content = load_prompt_master_file(prompt_file) if prompt_file.is_file() else ""
|
|
2376
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
|
|
2377
|
+
average_video_time = st.text_input("Tempo Medio de Video (HH:MM)", value=str(channel.get("average_video_time") or DEFAULT_AVERAGE_VIDEO_TIME), key=f"tiktok_channel_average_video_time_{channel_id}", help=f"Referência: {calculated_words or 0} palavras · {calculated_source}.")
|
|
2378
2378
|
if st.button("Guardar tempo", key=f"save_tiktok_channel_average_video_time_{channel_id}", use_container_width=True):
|
|
2379
2379
|
if not valid_hhmm(average_video_time):
|
|
2380
2380
|
st.error("Use o formato HH:MM, por exemplo 00:02.")
|
|
2381
2381
|
else:
|
|
2382
|
-
update_channel(channel_id, {"average_video_time": average_video_time.strip() or
|
|
2382
|
+
update_channel(channel_id, {"average_video_time": average_video_time.strip() or DEFAULT_AVERAGE_VIDEO_TIME, "average_video_word_count": words_from_channel_time(average_video_time)})
|
|
2383
2383
|
st.success("Tempo médio do canal TikTok guardado.")
|
|
2384
2384
|
st.rerun()
|
|
2385
2385
|
with card_cols[6]:
|
|
@@ -3073,12 +3073,12 @@ def render_channels():
|
|
|
3073
3073
|
st.metric("Visualizações", _format_channel_count(channel.get("view_count")))
|
|
3074
3074
|
with header_cols[5]:
|
|
3075
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
|
|
3076
|
+
average_video_time = st.text_input("Tempo Medio de Video (HH:MM)", value=str(channel.get("average_video_time") or DEFAULT_AVERAGE_VIDEO_TIME), key=f"youtube_channel_average_video_time_{channel_id}", help=f"Referência: {calculated_words or 0} palavras · {calculated_source}.")
|
|
3077
3077
|
if st.button("Guardar tempo", key=f"save_youtube_channel_average_video_time_{channel_id}", use_container_width=True):
|
|
3078
3078
|
if not valid_hhmm(average_video_time):
|
|
3079
3079
|
st.error("Use o formato HH:MM, por exemplo 00:02.")
|
|
3080
3080
|
else:
|
|
3081
|
-
update_channel(channel_id, {"average_video_time": average_video_time.strip() or
|
|
3081
|
+
update_channel(channel_id, {"average_video_time": average_video_time.strip() or DEFAULT_AVERAGE_VIDEO_TIME, "average_video_word_count": words_from_channel_time(average_video_time)})
|
|
3082
3082
|
st.success("Tempo médio do canal YouTube guardado.")
|
|
3083
3083
|
st.rerun()
|
|
3084
3084
|
with header_cols[6]:
|
|
@@ -5447,7 +5447,7 @@ def render_tiktok_automation():
|
|
|
5447
5447
|
for channel in channels:
|
|
5448
5448
|
channel_id = str(channel["id"])
|
|
5449
5449
|
with st.container(border=True):
|
|
5450
|
-
header_cols = st.columns([0.55, 2.
|
|
5450
|
+
header_cols = st.columns([0.55, 2.35, 1.25, 0.82, 0.92, 1.2])
|
|
5451
5451
|
with header_cols[0]:
|
|
5452
5452
|
profile_image = _tiktok_avatar_url(channel)
|
|
5453
5453
|
if profile_image:
|
|
@@ -5465,7 +5465,7 @@ def render_tiktok_automation():
|
|
|
5465
5465
|
prompt_file = TIKTOK_PROMPT_MASTERS / str(channel.get("default_prompt_master") or channel.get("prompt_master") or "")
|
|
5466
5466
|
prompt_content = load_prompt_master_file(prompt_file) if prompt_file.is_file() else ""
|
|
5467
5467
|
calculated_time, calculated_source, calculated_words = _channel_average_video_time(channel, prompt_master=prompt_content)
|
|
5468
|
-
average_video_time = st.text_input("Tempo Medio de Video (HH:MM)", value=str(channel.get("average_video_time") or
|
|
5468
|
+
average_video_time = st.text_input("Tempo Medio de Video (HH:MM)", value=str(channel.get("average_video_time") or DEFAULT_AVERAGE_VIDEO_TIME), key=f"tiktok_average_video_time_{channel_id}", help=f"Referência média: {calculated_words or 0} palavras · fonte: {calculated_source}.")
|
|
5469
5469
|
default_cols = st.columns([1.0, 1.0, 1.35, 1.45, 1.55, 1.0], gap="small")
|
|
5470
5470
|
with default_cols[0]:
|
|
5471
5471
|
st.markdown("**Idioma do roteiro**")
|
|
@@ -5492,7 +5492,7 @@ def render_tiktok_automation():
|
|
|
5492
5492
|
st.error("O Tempo Medio de Video deve estar no formato HH:MM, por exemplo 00:02.")
|
|
5493
5493
|
else:
|
|
5494
5494
|
avatar_url = _tiktok_avatar_url(channel)
|
|
5495
|
-
update_channel(channel_id, {"automation_on": bool(enabled), "automation_time": schedule_time.strip(), "average_video_time": average_video_time.strip() or
|
|
5495
|
+
update_channel(channel_id, {"automation_on": bool(enabled), "automation_time": schedule_time.strip(), "average_video_time": average_video_time.strip() or DEFAULT_AVERAGE_VIDEO_TIME, "average_video_word_count": words_from_channel_time(average_video_time), "default_prompt_master": prompt, "prompt_master": prompt, "platform": "tiktok", "format": automation_format, "video_aspect_ratio": "Portrait 9:16", "style_wide": "portrait", "avatar_url": avatar_url, "thumbnail_url": avatar_url})
|
|
5496
5496
|
st.success("Automação TikTok guardada.")
|
|
5497
5497
|
st.rerun()
|
|
5498
5498
|
_render_tiktok_automation_cards()
|
|
@@ -5626,7 +5626,7 @@ def render_automation():
|
|
|
5626
5626
|
automation_voice = current_voice
|
|
5627
5627
|
paired_thumbnail = thumbnail_blueprint_for_blueprint(automation_blueprint)
|
|
5628
5628
|
automation_format = str(channel.get("format") or "wide")
|
|
5629
|
-
header_cols = st.columns([0.58,
|
|
5629
|
+
header_cols = st.columns([0.58, 2.15, 1.4, 1.15, 1.15, 1.25, 0.82, 0.92], gap="small")
|
|
5630
5630
|
with header_cols[0]:
|
|
5631
5631
|
if channel.get("thumbnail_url"):
|
|
5632
5632
|
st.image(channel["thumbnail_url"], width=48)
|
|
@@ -5653,7 +5653,7 @@ def render_automation():
|
|
|
5653
5653
|
schedule_time = st.text_input("Horário (HH:MM)", value=channel.get("automation_time", "00:00"), key=f"automation_time_{channel_id}")
|
|
5654
5654
|
with header_cols[7]:
|
|
5655
5655
|
calculated_time, calculated_source, calculated_words = _channel_average_video_time(channel)
|
|
5656
|
-
average_video_time = st.text_input("Tempo Medio de Video (HH:MM)", value=str(channel.get("average_video_time") or
|
|
5656
|
+
average_video_time = st.text_input("Tempo Medio de Video (HH:MM)", value=str(channel.get("average_video_time") or DEFAULT_AVERAGE_VIDEO_TIME), key=f"average_video_time_{channel_id}", help=f"Referência média: {calculated_words or 0} palavras · fonte: {calculated_source}.")
|
|
5657
5657
|
|
|
5658
5658
|
control_cols = st.columns([1.8, 1.8, 1.35, 1.2], gap="small")
|
|
5659
5659
|
with control_cols[0]:
|
|
@@ -5685,7 +5685,7 @@ def render_automation():
|
|
|
5685
5685
|
update_channel(channel_id, {
|
|
5686
5686
|
"automation_on": bool(enabled),
|
|
5687
5687
|
"automation_time": schedule_time.strip(),
|
|
5688
|
-
"average_video_time": average_video_time.strip() or
|
|
5688
|
+
"average_video_time": average_video_time.strip() or DEFAULT_AVERAGE_VIDEO_TIME,
|
|
5689
5689
|
"average_video_word_count": words_from_channel_time(average_video_time),
|
|
5690
5690
|
"format": automation_format,
|
|
5691
5691
|
})
|
|
@@ -6,6 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
8
|
WORDS_PER_MINUTE = 150
|
|
9
|
+
DEFAULT_AVERAGE_VIDEO_TIME = "00:12"
|
|
9
10
|
AVERAGE_VIDEO_TIME_KEY = "average_video_time"
|
|
10
11
|
AVERAGE_VIDEO_WORD_COUNT_KEY = "average_video_word_count"
|
|
11
12
|
|
|
@@ -100,7 +101,8 @@ def channel_video_length(channel: dict[str, Any], blueprint: Any = None, prompt_
|
|
|
100
101
|
words = explicit_word_count(blueprint, prompt_master)
|
|
101
102
|
if words:
|
|
102
103
|
return words, hhmm_from_minutes(round(words / WORDS_PER_MINUTE)), "Blueprint/Prompt Master"
|
|
103
|
-
|
|
104
|
+
default_minutes = minutes_from_hhmm(DEFAULT_AVERAGE_VIDEO_TIME)
|
|
105
|
+
return default_minutes * WORDS_PER_MINUTE, DEFAULT_AVERAGE_VIDEO_TIME, "padrão do canal"
|
|
104
106
|
|
|
105
107
|
|
|
106
108
|
def words_from_channel_time(value: Any) -> int:
|
|
@@ -121,6 +123,7 @@ def length_generation_settings(channel: dict[str, Any], blueprint: Any = None, p
|
|
|
121
123
|
__all__ = [
|
|
122
124
|
"AVERAGE_VIDEO_TIME_KEY",
|
|
123
125
|
"AVERAGE_VIDEO_WORD_COUNT_KEY",
|
|
126
|
+
"DEFAULT_AVERAGE_VIDEO_TIME",
|
|
124
127
|
"WORDS_PER_MINUTE",
|
|
125
128
|
"channel_video_length",
|
|
126
129
|
"explicit_word_count",
|
|
@@ -4,6 +4,7 @@ import re
|
|
|
4
4
|
import json
|
|
5
5
|
import logging
|
|
6
6
|
import os
|
|
7
|
+
import platform
|
|
7
8
|
import sys
|
|
8
9
|
import shutil
|
|
9
10
|
import subprocess
|
|
@@ -213,48 +214,91 @@ def _structured_metric_from_json(document: str, *keys: str) -> int | None:
|
|
|
213
214
|
return None
|
|
214
215
|
|
|
215
216
|
|
|
217
|
+
def _profile_quality(user: Mapping[str, Any]) -> int:
|
|
218
|
+
groups = (
|
|
219
|
+
('biography', 'bio', 'description'),
|
|
220
|
+
('edge_follow', 'following', 'following_count', 'followingCount', 'follows'),
|
|
221
|
+
('edge_followed_by', 'followers', 'follower_count', 'followers_count', 'followerCount'),
|
|
222
|
+
('edge_owner_to_timeline_media', 'posts', 'post_count', 'media_count'),
|
|
223
|
+
('username', 'user_name', 'handle'),
|
|
224
|
+
)
|
|
225
|
+
return sum(any(user.get(key) not in (None, '', {}, []) for key in group) for group in groups)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _merge_profile(base: dict[str, Any] | None, candidate: Mapping[str, Any]) -> dict[str, Any]:
|
|
229
|
+
merged = dict(base or {})
|
|
230
|
+
for key, value in candidate.items():
|
|
231
|
+
if value not in (None, '', [], {}):
|
|
232
|
+
if isinstance(value, Mapping) and isinstance(merged.get(key), Mapping):
|
|
233
|
+
merged[key] = {**merged[key], **value}
|
|
234
|
+
else:
|
|
235
|
+
merged[key] = value
|
|
236
|
+
return merged
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _extract_profile_user(payload: Any) -> Mapping[str, Any] | None:
|
|
240
|
+
if not isinstance(payload, Mapping):
|
|
241
|
+
return None
|
|
242
|
+
candidates = [
|
|
243
|
+
payload.get('user'),
|
|
244
|
+
(payload.get('data') or {}).get('user') if isinstance(payload.get('data'), Mapping) else None,
|
|
245
|
+
(payload.get('graphql') or {}).get('user') if isinstance(payload.get('graphql'), Mapping) else None,
|
|
246
|
+
(payload.get('data') or {}).get('profile') if isinstance(payload.get('data'), Mapping) else None,
|
|
247
|
+
]
|
|
248
|
+
return next((candidate for candidate in candidates if isinstance(candidate, Mapping)), None)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _fetch_profile_with_playwright(username: str, headers: Mapping[str, str], cookies: Mapping[str, str]) -> dict[str, Any] | None:
|
|
252
|
+
endpoints = (
|
|
253
|
+
f'https://www.instagram.com/api/v1/users/web_profile_info/?username={username}',
|
|
254
|
+
f'https://i.instagram.com/api/v1/users/web_profile_info/?username={username}',
|
|
255
|
+
)
|
|
256
|
+
best_user: dict[str, Any] | None = None
|
|
257
|
+
try:
|
|
258
|
+
from playwright.sync_api import sync_playwright
|
|
259
|
+
with sync_playwright() as playwright:
|
|
260
|
+
browser = playwright.chromium.launch(headless=True)
|
|
261
|
+
try:
|
|
262
|
+
page = browser.new_page(
|
|
263
|
+
user_agent=headers['User-Agent'],
|
|
264
|
+
extra_http_headers={'x-ig-app-id': headers['x-ig-app-id'], 'Accept-Language': 'en-US,en;q=0.9'},
|
|
265
|
+
)
|
|
266
|
+
if cookies:
|
|
267
|
+
page.context.add_cookies([{'name': name, 'value': value, 'domain': '.instagram.com', 'path': '/'} for name, value in cookies.items()])
|
|
268
|
+
for endpoint in endpoints:
|
|
269
|
+
try:
|
|
270
|
+
response = page.goto(endpoint, wait_until='domcontentloaded', timeout=30000)
|
|
271
|
+
LOGGER.debug('Instagram Playwright profile URL=%s status=%s', endpoint, response.status if response else None)
|
|
272
|
+
if response is None or response.status >= 400:
|
|
273
|
+
continue
|
|
274
|
+
payload = json.loads(page.locator('body').inner_text(timeout=5000))
|
|
275
|
+
user = _extract_profile_user(payload)
|
|
276
|
+
if isinstance(user, dict):
|
|
277
|
+
best_user = _merge_profile(best_user, user)
|
|
278
|
+
if _profile_quality(best_user) >= 4:
|
|
279
|
+
return best_user
|
|
280
|
+
except (ValueError, TimeoutError) as exc:
|
|
281
|
+
LOGGER.warning('Falha ao interpretar resposta Playwright do perfil URL=%s: %s', endpoint, exc)
|
|
282
|
+
finally:
|
|
283
|
+
browser.close()
|
|
284
|
+
except Exception as exc:
|
|
285
|
+
LOGGER.warning('Falha no Playwright do perfil Instagram: %s', exc, exc_info=True)
|
|
286
|
+
_streamlit_message('error', f'Playwright não conseguiu consultar o perfil Instagram: {exc}', 'profile_playwright_error')
|
|
287
|
+
return best_user
|
|
288
|
+
|
|
289
|
+
|
|
216
290
|
def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
217
291
|
_ensure_windows_playwright()
|
|
218
292
|
cookies = _instagram_cookies()
|
|
219
293
|
headers = _instagram_headers()
|
|
220
294
|
headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36'
|
|
221
|
-
# The www host is the current public web endpoint. The legacy i host is
|
|
222
|
-
# retained as a fallback because Instagram rate-limits the two hosts
|
|
223
|
-
# independently and their availability can vary by region.
|
|
224
295
|
endpoints = (
|
|
225
296
|
f'https://www.instagram.com/api/v1/users/web_profile_info/?username={username}',
|
|
226
297
|
f'https://i.instagram.com/api/v1/users/web_profile_info/?username={username}',
|
|
227
298
|
)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
('edge_follow', 'following', 'following_count', 'followingCount', 'follows'),
|
|
232
|
-
('edge_followed_by', 'followers', 'follower_count', 'followers_count', 'followerCount'),
|
|
233
|
-
('edge_owner_to_timeline_media', 'posts', 'post_count', 'media_count'),
|
|
234
|
-
('username', 'user_name', 'handle'),
|
|
235
|
-
)
|
|
236
|
-
return sum(any(user.get(key) not in (None, '', {}, []) for key in group) for group in groups)
|
|
237
|
-
|
|
238
|
-
def merge_profile(base: dict[str, Any] | None, candidate: Mapping[str, Any]) -> dict[str, Any]:
|
|
239
|
-
merged = dict(base or {})
|
|
240
|
-
for key, value in candidate.items():
|
|
241
|
-
if value not in (None, '', [], {}):
|
|
242
|
-
if isinstance(value, Mapping) and isinstance(merged.get(key), Mapping):
|
|
243
|
-
merged[key] = {**merged[key], **value}
|
|
244
|
-
else:
|
|
245
|
-
merged[key] = value
|
|
246
|
-
return merged
|
|
247
|
-
|
|
248
|
-
def extract_user(payload: Any) -> Mapping[str, Any] | None:
|
|
249
|
-
if not isinstance(payload, Mapping):
|
|
250
|
-
return None
|
|
251
|
-
candidates = [
|
|
252
|
-
payload.get('user'),
|
|
253
|
-
(payload.get('data') or {}).get('user') if isinstance(payload.get('data'), Mapping) else None,
|
|
254
|
-
(payload.get('graphql') or {}).get('user') if isinstance(payload.get('graphql'), Mapping) else None,
|
|
255
|
-
(payload.get('data') or {}).get('profile') if isinstance(payload.get('data'), Mapping) else None,
|
|
256
|
-
]
|
|
257
|
-
return next((candidate for candidate in candidates if isinstance(candidate, Mapping)), None)
|
|
299
|
+
if platform.system() == 'Windows':
|
|
300
|
+
LOGGER.debug('Windows detectado: perfil Instagram exclusivamente via Playwright.')
|
|
301
|
+
return _fetch_profile_with_playwright(username, headers, cookies)
|
|
258
302
|
|
|
259
303
|
request_headers = [
|
|
260
304
|
headers,
|
|
@@ -276,10 +320,10 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
|
276
320
|
if best_user:
|
|
277
321
|
return best_user
|
|
278
322
|
continue
|
|
279
|
-
user =
|
|
323
|
+
user = _extract_profile_user(payload)
|
|
280
324
|
if isinstance(user, dict):
|
|
281
|
-
best_user =
|
|
282
|
-
if
|
|
325
|
+
best_user = _merge_profile(best_user, user)
|
|
326
|
+
if _profile_quality(best_user) >= 4:
|
|
283
327
|
return best_user
|
|
284
328
|
|
|
285
329
|
curl = shutil.which('curl')
|
|
@@ -298,10 +342,10 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
|
298
342
|
)
|
|
299
343
|
payload = json.loads(completed.stdout) if completed.returncode == 0 and completed.stdout else None
|
|
300
344
|
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:])
|
|
301
|
-
user =
|
|
345
|
+
user = _extract_profile_user(payload)
|
|
302
346
|
if isinstance(user, dict):
|
|
303
|
-
best_user =
|
|
304
|
-
if
|
|
347
|
+
best_user = _merge_profile(best_user, user)
|
|
348
|
+
if _profile_quality(best_user) >= 4:
|
|
305
349
|
return best_user
|
|
306
350
|
except (OSError, subprocess.SubprocessError, ValueError):
|
|
307
351
|
LOGGER.warning('Falha no fallback curl do perfil Instagram URL=%s', endpoint, exc_info=True)
|
|
@@ -310,39 +354,7 @@ def _fetch_web_profile_user(username: str) -> dict[str, Any] | None:
|
|
|
310
354
|
LOGGER.warning('curl não está disponível no PATH; a tentar Playwright.')
|
|
311
355
|
_streamlit_message('warning', 'curl não foi encontrado no PATH do Windows; será tentado o fallback Playwright.', 'curl_missing')
|
|
312
356
|
|
|
313
|
-
|
|
314
|
-
# clientes HTTP, embora entregue o JSON completo a um navegador Chromium.
|
|
315
|
-
# O fallback é opcional: a aplicação continua funcional sem Playwright.
|
|
316
|
-
try:
|
|
317
|
-
from playwright.sync_api import sync_playwright
|
|
318
|
-
with sync_playwright() as playwright:
|
|
319
|
-
browser = playwright.chromium.launch(headless=True)
|
|
320
|
-
try:
|
|
321
|
-
page = browser.new_page(
|
|
322
|
-
user_agent=headers['User-Agent'],
|
|
323
|
-
extra_http_headers={'x-ig-app-id': headers['x-ig-app-id'], 'Accept-Language': 'en-US,en;q=0.9'},
|
|
324
|
-
)
|
|
325
|
-
if cookies:
|
|
326
|
-
page.context.add_cookies([{'name': name, 'value': value, 'domain': '.instagram.com', 'path': '/'} for name, value in cookies.items()])
|
|
327
|
-
for endpoint in endpoints:
|
|
328
|
-
try:
|
|
329
|
-
response = page.goto(endpoint, wait_until='domcontentloaded', timeout=30000)
|
|
330
|
-
if response is None or response.status >= 400:
|
|
331
|
-
continue
|
|
332
|
-
payload = json.loads(page.locator('body').inner_text(timeout=5000))
|
|
333
|
-
user = extract_user(payload)
|
|
334
|
-
if isinstance(user, dict):
|
|
335
|
-
best_user = merge_profile(best_user, user)
|
|
336
|
-
if profile_quality(best_user) >= 4:
|
|
337
|
-
return best_user
|
|
338
|
-
except (ValueError, TimeoutError):
|
|
339
|
-
continue
|
|
340
|
-
finally:
|
|
341
|
-
browser.close()
|
|
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')
|
|
345
|
-
return best_user
|
|
357
|
+
return _fetch_profile_with_playwright(username, headers, cookies) or best_user
|
|
346
358
|
|
|
347
359
|
|
|
348
360
|
def _country_from_bloks(value: Any) -> str:
|
|
@@ -604,6 +616,42 @@ def _post_from_node(node: dict[str, Any]) -> dict[str, Any] | None:
|
|
|
604
616
|
}
|
|
605
617
|
|
|
606
618
|
|
|
619
|
+
def _fetch_posts_with_playwright(reference: Mapping[str, str], limit: int, cookies: Mapping[str, str]) -> list[dict[str, Any]]:
|
|
620
|
+
posts: list[dict[str, Any]] = []
|
|
621
|
+
seen: set[str] = set()
|
|
622
|
+
try:
|
|
623
|
+
from playwright.sync_api import sync_playwright
|
|
624
|
+
with sync_playwright() as playwright:
|
|
625
|
+
browser = playwright.chromium.launch(headless=True)
|
|
626
|
+
try:
|
|
627
|
+
page = browser.new_page(
|
|
628
|
+
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36',
|
|
629
|
+
extra_http_headers={'x-ig-app-id': '936619743392459', 'Accept-Language': 'en-US,en;q=0.9'},
|
|
630
|
+
)
|
|
631
|
+
if cookies:
|
|
632
|
+
page.context.add_cookies([{'name': name, 'value': value, 'domain': '.instagram.com', 'path': '/'} for name, value in cookies.items()])
|
|
633
|
+
response = page.goto(reference['url'], wait_until='domcontentloaded', timeout=30000)
|
|
634
|
+
LOGGER.debug('Instagram Playwright posts URL=%s status=%s', reference['url'], response.status if response else None)
|
|
635
|
+
if response is None or response.status >= 400:
|
|
636
|
+
return []
|
|
637
|
+
browser_document = page.content()
|
|
638
|
+
for document in _embedded_json_documents(browser_document):
|
|
639
|
+
for node in _walk_json(document):
|
|
640
|
+
post = _post_from_node(node)
|
|
641
|
+
if not post or post['id'] in seen:
|
|
642
|
+
continue
|
|
643
|
+
seen.add(post['id'])
|
|
644
|
+
posts.append(post)
|
|
645
|
+
if len(posts) >= max(1, int(limit)):
|
|
646
|
+
return posts
|
|
647
|
+
finally:
|
|
648
|
+
browser.close()
|
|
649
|
+
except Exception as exc:
|
|
650
|
+
LOGGER.warning('Falha no Playwright dos posts Instagram: %s', exc, exc_info=True)
|
|
651
|
+
_streamlit_message('error', f'Playwright não conseguiu consultar os posts Instagram: {exc}', 'posts_playwright_error')
|
|
652
|
+
return posts
|
|
653
|
+
|
|
654
|
+
|
|
607
655
|
def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationResult:
|
|
608
656
|
LOGGER.debug('fetch_public_instagram_posts source=%s limit=%s', source, limit)
|
|
609
657
|
_ensure_windows_playwright()
|
|
@@ -611,6 +659,10 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
|
|
|
611
659
|
reference = normalize_instagram_reference(source)
|
|
612
660
|
except ValueError as exc:
|
|
613
661
|
return IntegrationResult(False, str(exc), {})
|
|
662
|
+
if platform.system() == 'Windows':
|
|
663
|
+
LOGGER.debug('Windows detectado: posts Instagram exclusivamente via Playwright.')
|
|
664
|
+
posts = _fetch_posts_with_playwright(reference, limit, _instagram_cookies())
|
|
665
|
+
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))]})
|
|
614
666
|
api_user = _fetch_web_profile_user(reference['username'])
|
|
615
667
|
if api_user:
|
|
616
668
|
api_posts: list[dict[str, Any]] = []
|
|
@@ -647,36 +699,7 @@ def fetch_public_instagram_posts(source: str, limit: int = 10) -> IntegrationRes
|
|
|
647
699
|
if len(posts) >= max(1, int(limit)):
|
|
648
700
|
break
|
|
649
701
|
if not posts:
|
|
650
|
-
|
|
651
|
-
from playwright.sync_api import sync_playwright
|
|
652
|
-
with sync_playwright() as playwright:
|
|
653
|
-
browser = playwright.chromium.launch(headless=True)
|
|
654
|
-
try:
|
|
655
|
-
page = browser.new_page(
|
|
656
|
-
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36',
|
|
657
|
-
extra_http_headers={'x-ig-app-id': '936619743392459', 'Accept-Language': 'en-US,en;q=0.9'},
|
|
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()])
|
|
662
|
-
page.goto(reference['url'], wait_until='domcontentloaded', timeout=30000)
|
|
663
|
-
browser_document = page.content()
|
|
664
|
-
for document in _embedded_json_documents(browser_document):
|
|
665
|
-
for node in _walk_json(document):
|
|
666
|
-
post = _post_from_node(node)
|
|
667
|
-
if not post or post['id'] in seen:
|
|
668
|
-
continue
|
|
669
|
-
seen.add(post['id'])
|
|
670
|
-
posts.append(post)
|
|
671
|
-
if len(posts) >= max(1, int(limit)):
|
|
672
|
-
break
|
|
673
|
-
if len(posts) >= max(1, int(limit)):
|
|
674
|
-
break
|
|
675
|
-
finally:
|
|
676
|
-
browser.close()
|
|
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')
|
|
702
|
+
posts = _fetch_posts_with_playwright(reference, limit, _instagram_cookies())
|
|
680
703
|
LOGGER.debug('Instagram posts extracted=%s limit=%s', len(posts), limit)
|
|
681
704
|
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))]})
|
|
682
705
|
|
package/package.json
CHANGED