@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 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 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.")
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 "00:00", "average_video_word_count": words_from_channel_time(average_video_time)})
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 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.")
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 "00:00", "average_video_word_count": words_from_channel_time(average_video_time)})
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.15, 1.25, 1.35, 1.55, 1.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 calculated_time), key=f"tiktok_average_video_time_{channel_id}", help=f"Referência média: {calculated_words or 0} palavras · fonte: {calculated_source}. Use 00:00 para voltar ao Prompt Master.")
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 "00:00", "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})
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, 1.95, 1.4, 1.15, 1.15, 1.25, 1.25, 1.45], gap="small")
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 calculated_time), key=f"average_video_time_{channel_id}", help=f"Referência média: {calculated_words or 0} palavras · fonte: {calculated_source}. Use 00:00 para voltar ao Blueprint.")
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 "00:00",
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
- return 0, "00:00", "sem referência"
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
- def profile_quality(user: Mapping[str, Any]) -> int:
229
- groups = (
230
- ('biography', 'bio', 'description'),
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 = extract_user(payload)
323
+ user = _extract_profile_user(payload)
280
324
  if isinstance(user, dict):
281
- best_user = merge_profile(best_user, user)
282
- if profile_quality(best_user) >= 4:
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 = extract_user(payload)
345
+ user = _extract_profile_user(payload)
302
346
  if isinstance(user, dict):
303
- best_user = merge_profile(best_user, user)
304
- if profile_quality(best_user) >= 4:
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
- # Em algumas instalações Windows o Instagram devolve a página HTML aos
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
- try:
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.6.47",
3
+ "version": "0.6.49",
4
4
  "description": "Thunderbolt — interface local para operação de canais faceless e motor MoneyPrinterTurbo",
5
5
  "license": "MIT",
6
6
  "main": "scripts/cli.mjs",