@danhachuel/thunderbolt 0.7.13 → 0.7.15

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
@@ -2419,7 +2419,7 @@ def render_tiktok_channels():
2419
2419
  for channel in visible_tiktok_channels:
2420
2420
  channel_id = str(channel.get("id") or "")
2421
2421
  with st.container(border=True):
2422
- card_cols = st.columns([0.7, 3.05, 1.05, 1.05, 1.25, 1.35], gap="small")
2422
+ card_cols = st.columns([0.7, 2.55, 1.15, 1.05, 1.05, 1.25, 1.35], gap="small")
2423
2423
  with card_cols[0]:
2424
2424
  avatar_url = _tiktok_avatar_url(channel)
2425
2425
  if avatar_url:
@@ -2432,12 +2432,16 @@ def render_tiktok_channels():
2432
2432
  st.caption(channel.get("description") or "Perfil carregado da página pública do TikTok.")
2433
2433
  st.caption(f"Nicho: {channel.get('niche') or 'não configurado'}")
2434
2434
  with card_cols[2]:
2435
- st.metric("Seguidores", format_metric_number(channel.get("subscriber_count")))
2435
+ tiktok_url = _tiktok_channel_url(channel)
2436
+ if tiktok_url:
2437
+ st.link_button("Abrir canal", tiktok_url, type="primary", width="content")
2436
2438
  with card_cols[3]:
2437
- st.metric("Curtidas", format_metric_number(channel.get("likes_count")))
2439
+ st.metric("Seguidores", format_metric_number(channel.get("subscriber_count")))
2438
2440
  with card_cols[4]:
2439
- st.metric("Vídeos", format_metric_number(channel.get("video_count")))
2441
+ st.metric("Curtidas", format_metric_number(channel.get("likes_count")))
2440
2442
  with card_cols[5]:
2443
+ st.metric("Vídeos", format_metric_number(channel.get("video_count")))
2444
+ with card_cols[6]:
2441
2445
  if st.button("↻", key=f"refresh_tiktok_metrics_{channel_id}", help="Actualizar Seguidores, Vídeos e Curtidas", width="stretch"):
2442
2446
  with st.spinner("A actualizar métricas TikTok…"):
2443
2447
  refreshed, message = _refresh_tiktok_channel_metrics(channel)
@@ -2651,20 +2655,37 @@ def _refresh_youtube_channel_metrics(channel: dict[str, Any], youtube: YouTubeAd
2651
2655
 
2652
2656
 
2653
2657
  def _refresh_tiktok_channel_metrics(channel: dict[str, Any]) -> tuple[bool, str]:
2654
- result = fetch_public_tiktok_profile(str(channel.get("url") or channel.get("handle") or ""))
2658
+ channel_ref = str(channel.get("url") or channel.get("handle") or "").strip()
2659
+ if not channel_ref:
2660
+ return False, "Este canal não tem URL ou handle TikTok para actualizar as métricas."
2661
+ try:
2662
+ result = fetch_public_tiktok_profile(channel_ref)
2663
+ except Exception as exc:
2664
+ return False, f"Não foi possível actualizar as métricas TikTok ({type(exc).__name__}). Tente novamente."
2655
2665
  if not result.ok or not isinstance(result.data, dict):
2656
2666
  return False, result.message
2657
2667
  data = result.data
2658
- update_channel(str(channel["id"]), {
2659
- "subscriber_count": data.get("subscriber_count"),
2660
- "likes_count": data.get("likes_count"),
2661
- "video_count": data.get("video_count"),
2668
+ updates = {
2662
2669
  "metrics_source": data.get("metrics_source", "tiktok_public_page"),
2663
2670
  "last_public_lookup_at": data.get("last_public_lookup_at", now()),
2664
- })
2671
+ }
2672
+ for field in ("subscriber_count", "likes_count", "video_count", "view_count", "name", "handle", "url", "avatar_url", "thumbnail_url"):
2673
+ if data.get(field) is not None and data.get(field) != "":
2674
+ updates[field] = data[field]
2675
+ update_channel(str(channel["id"]), updates)
2665
2676
  return True, "Métricas TikTok actualizadas."
2666
2677
 
2667
2678
 
2679
+ def _tiktok_channel_url(channel: dict[str, Any]) -> str:
2680
+ source = str(channel.get("url") or channel.get("handle") or channel.get("username") or "").strip()
2681
+ if not source:
2682
+ return ""
2683
+ try:
2684
+ return str(normalize_tiktok_reference(source).get("url") or "").strip()
2685
+ except ValueError:
2686
+ return source if source.startswith(("http://", "https://")) else ""
2687
+
2688
+
2668
2689
  def render_channels():
2669
2690
  st.title("Canais Youtube")
2670
2691
  st.caption("Escolha entre importar dados públicos do YouTube ou preencher o canal manualmente.")
@@ -3096,7 +3117,7 @@ def render_channels():
3096
3117
  channel_id = str(channel["id"])
3097
3118
  edit_key = f"edit_channel_{channel_id}"
3098
3119
  with st.container(border=True):
3099
- header_cols = st.columns([0.7, 2.75, 1.1, 1.0, 1.35, 1.35])
3120
+ header_cols = st.columns([0.7, 2.75, 1.15, 1.1, 1.0, 1.35, 1.35])
3100
3121
  channel_url = str(channel.get("url") or "").strip()
3101
3122
  if not channel_url:
3102
3123
  channel_id_or_handle = str(channel.get("youtube_channel_id") or channel.get("youtube_id") or channel.get("handle") or "").strip()
@@ -3119,14 +3140,15 @@ def render_channels():
3119
3140
  st.caption(channel_niche_label(channel))
3120
3141
  st.caption(f"{channel.get('handle') or channel.get('url') or 'sem URL'} · {channel.get('metrics_source', 'manual')}")
3121
3142
  with header_cols[2]:
3122
- st.metric("Inscritos", _format_channel_count(channel.get("subscriber_count")))
3143
+ if channel_url:
3144
+ st.link_button("Abrir canal", channel_url, type="primary", width="content")
3123
3145
  with header_cols[3]:
3124
- st.metric("Vídeos", _format_channel_count(channel.get("video_count")))
3146
+ st.metric("Inscritos", _format_channel_count(channel.get("subscriber_count")))
3125
3147
  with header_cols[4]:
3126
- st.metric("Visualizações", _format_channel_count(channel.get("view_count")))
3148
+ st.metric("Vídeos", _format_channel_count(channel.get("video_count")))
3127
3149
  with header_cols[5]:
3128
- if channel_url:
3129
- st.link_button("Abrir canal", channel_url, type="primary", width="content")
3150
+ st.metric("Visualizações", _format_channel_count(channel.get("view_count")))
3151
+ with header_cols[6]:
3130
3152
  if st.button("↻", key=f"refresh_youtube_metrics_{channel_id}", help="Actualizar Inscritos, Vídeos e Visualizações", width="stretch"):
3131
3153
  with st.spinner("A actualizar métricas YouTube…"):
3132
3154
  refreshed, message = _refresh_youtube_channel_metrics(channel, youtube)
@@ -5589,6 +5611,11 @@ def _render_tiktok_automation_cards():
5589
5611
  st.subheader("Vídeos cadastrados TikTok")
5590
5612
  st.caption("Esta fila mostra exclusivamente tarefas associadas a canais TikTok.")
5591
5613
  tiktok_tasks = load_automation_tasks_for_platform("tiktok")
5614
+ tiktok_channels_by_id = {
5615
+ str(channel.get("id")): channel
5616
+ for channel in read_json("channels.json", [])
5617
+ if isinstance(channel, dict) and classify_channel_platform(channel) == "tiktok"
5618
+ }
5592
5619
  if not tiktok_tasks:
5593
5620
  st.info("Ainda não existem vídeos TikTok cadastrados.")
5594
5621
  for task in tiktok_tasks:
@@ -5607,6 +5634,15 @@ def _render_tiktok_automation_cards():
5607
5634
  st.caption("Thumbnail ainda não pronta")
5608
5635
  st.write(f"**{task.get('title') or task.get('topic') or 'Vídeo TikTok'}**")
5609
5636
  st.caption(f"{task.get('channel_name') or 'Canal TikTok'} · {task_id}")
5637
+ task_channel = tiktok_channels_by_id.get(str(task.get("channel_id") or ""))
5638
+ if task_channel is None:
5639
+ task_channel = next(
5640
+ (channel for channel in tiktok_channels_by_id.values() if str(channel.get("name") or "").strip() == str(task.get("channel_name") or "").strip()),
5641
+ None,
5642
+ )
5643
+ task_channel_url = _tiktok_channel_url(task_channel or {})
5644
+ if task_channel_url:
5645
+ st.link_button("Abrir canal", task_channel_url, type="primary", width="content")
5610
5646
  thumbnail_download_col, prompt_download_col = st.columns(2, gap="small")
5611
5647
  with thumbnail_download_col:
5612
5648
  st.download_button(
@@ -212,7 +212,7 @@ def fetch_public_tiktok_profile(source: str) -> IntegrationResult:
212
212
  "Accept": "text/html,application/xhtml+xml;q=0.9,*/*;q=0.8",
213
213
  }
214
214
  try:
215
- response = requests.get(reference["url"], headers=headers, timeout=12, allow_redirects=True)
215
+ response = requests.get(reference["url"], headers=headers, timeout=8, allow_redirects=True)
216
216
  except requests.RequestException as exc:
217
217
  return IntegrationResult(False, f"Não foi possível consultar o perfil público do TikTok: {exc}", reference)
218
218
  if response.status_code in {401, 403, 429}:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.7.13",
3
+ "version": "0.7.15",
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",