@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 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.45, 1.15, 1.15, 1.45, 1.55], gap="small")
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, 3.1, 1.25, 1.15, 1.55, 1.5])
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, _WINDOWS_COOKIE_WARNING_SHOWN
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.6.44",
3
+ "version": "0.6.46",
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",