@danhachuel/thunderbolt 0.7.0 → 0.7.2

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.
Files changed (2) hide show
  1. package/app/main.py +203 -179
  2. package/package.json +1 -1
package/app/main.py CHANGED
@@ -2405,11 +2405,21 @@ def render_tiktok_channels():
2405
2405
  st.rerun()
2406
2406
  st.divider()
2407
2407
  st.subheader(f"Canais TikTok cadastrados ({len(channels)})")
2408
- if channels:
2409
- for channel in channels:
2408
+ tiktok_channel_search = st.text_input(
2409
+ "Pesquisar nomes dos canais TikTok",
2410
+ key="tiktok_registered_channels_search",
2411
+ placeholder="Digite o nome do canal",
2412
+ )
2413
+ visible_tiktok_channels = [
2414
+ channel for channel in channels
2415
+ if not tiktok_channel_search.strip()
2416
+ or tiktok_channel_search.strip().casefold() in str(channel.get("name") or "").casefold()
2417
+ ]
2418
+ if channels and visible_tiktok_channels:
2419
+ for channel in visible_tiktok_channels:
2410
2420
  channel_id = str(channel.get("id") or "")
2411
2421
  with st.container(border=True):
2412
- card_cols = st.columns([0.7, 3.05, 1.05, 1.05, 1.25, 1.7, 1.35], gap="small")
2422
+ card_cols = st.columns([0.7, 3.05, 1.05, 1.05, 1.25, 1.35], gap="small")
2413
2423
  with card_cols[0]:
2414
2424
  avatar_url = _tiktok_avatar_url(channel)
2415
2425
  if avatar_url:
@@ -2428,6 +2438,17 @@ def render_tiktok_channels():
2428
2438
  with card_cols[4]:
2429
2439
  st.metric("Vídeos", format_metric_number(channel.get("video_count")))
2430
2440
  with card_cols[5]:
2441
+ if st.button("↻", key=f"refresh_tiktok_metrics_{channel_id}", help="Actualizar Seguidores, Vídeos e Curtidas", width="stretch"):
2442
+ with st.spinner("A actualizar métricas TikTok…"):
2443
+ refreshed, message = _refresh_tiktok_channel_metrics(channel)
2444
+ (st.success if refreshed else st.warning)(message)
2445
+ if refreshed:
2446
+ st.rerun()
2447
+ active = st.toggle("Activo", value=bool(channel.get("active", True)), key=f"tiktok_import_card_active_{channel_id}")
2448
+ if active != bool(channel.get("active", True)):
2449
+ update_channel(channel_id, {"active": active})
2450
+ st.rerun()
2451
+ with st.expander("Detalhes e configuração do canal", expanded=False):
2431
2452
  prompt_file = TIKTOK_PROMPT_MASTERS / str(channel.get("default_prompt_master") or channel.get("prompt_master") or "")
2432
2453
  prompt_content = load_prompt_master_file(prompt_file) if prompt_file.is_file() else ""
2433
2454
  calculated_time, calculated_source, calculated_words = _channel_average_video_time(channel, prompt_master=prompt_content)
@@ -2439,89 +2460,80 @@ def render_tiktok_channels():
2439
2460
  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)})
2440
2461
  st.success("Tempo médio do canal TikTok guardado.")
2441
2462
  st.rerun()
2442
- with card_cols[6]:
2443
- if st.button("↻", key=f"refresh_tiktok_metrics_{channel_id}", help="Actualizar Seguidores, Vídeos e Curtidas", width="stretch"):
2444
- with st.spinner("A actualizar métricas TikTok…"):
2445
- refreshed, message = _refresh_tiktok_channel_metrics(channel)
2446
- (st.success if refreshed else st.warning)(message)
2447
- if refreshed:
2448
- st.rerun()
2449
- active = st.toggle("Activo", value=bool(channel.get("active", True)), key=f"tiktok_import_card_active_{channel_id}")
2450
- if active != bool(channel.get("active", True)):
2451
- update_channel(channel_id, {"active": active})
2452
- st.rerun()
2453
2463
  if st.button("Editar", key=f"tiktok_import_card_edit_{channel_id}"):
2454
2464
  st.session_state[f"tiktok_edit_{channel_id}"] = not st.session_state.get(f"tiktok_edit_{channel_id}", False)
2455
2465
  st.rerun()
2456
2466
  if st.button("Apagar card", key=f"tiktok_import_card_delete_{channel_id}"):
2457
2467
  st.session_state[f"tiktok_delete_{channel_id}"] = True
2458
2468
  st.rerun()
2459
- summary_cols = st.columns(4, gap="medium")
2460
- with summary_cols[0]:
2461
- st.markdown("**Blueprint Padrão**")
2462
- st.caption(prompt_labels.get(str(channel.get("default_prompt_master") or channel.get("prompt_master") or ""), "Sem Blueprint padrão"))
2463
- with summary_cols[1]:
2464
- st.markdown("**Nicho**")
2465
- st.caption(channel_niche_label(channel))
2466
- with summary_cols[2]:
2467
- st.markdown("**Narrador/Voz Padrão**")
2468
- st.caption(str(channel.get("default_voice") or channel.get("voice") or "Sem voz padrão"))
2469
- with summary_cols[3]:
2470
- st.markdown("**Idioma**")
2471
- st.caption(video_language_label(normalize_video_language(channel.get("language") or "pt")))
2472
- st.caption(f"Fonte do vídeo: {channel_video_source_value(channel.get('style_wide'))} · Proporção: {channel.get('video_aspect_ratio') or 'Portrait 9:16'}")
2473
- if st.session_state.get(f"tiktok_delete_{channel_id}"):
2474
- st.warning("Apagar este canal TikTok e os dados locais associados?")
2475
- confirm_cols = st.columns(2)
2476
- with confirm_cols[0]:
2477
- if st.button("Confirmar apagar", type="primary", key=f"tiktok_confirm_delete_{channel_id}"):
2478
- delete_channel(channel_id)
2479
- st.session_state.pop(f"tiktok_delete_{channel_id}", None)
2480
- st.rerun()
2481
- with confirm_cols[1]:
2482
- if st.button("Cancelar", key=f"tiktok_cancel_delete_{channel_id}"):
2483
- st.session_state.pop(f"tiktok_delete_{channel_id}", None)
2484
- st.rerun()
2485
- if st.session_state.get(f"tiktok_edit_{channel_id}"):
2486
- with st.form(f"tiktok_edit_form_{channel_id}"):
2487
- edit_name = st.text_input("Nome do canal", value=str(channel.get("name") or ""))
2488
- edit_url = st.text_input("URL pública", value=str(channel.get("url") or ""))
2489
- edit_handle = st.text_input("Handle", value=str(channel.get("handle") or ""))
2490
- edit_language = st.selectbox("Idioma do roteiro", VIDEO_LANGUAGE_SELECTION_OPTIONS, index=VIDEO_LANGUAGE_SELECTION_OPTIONS.index(normalize_video_language(channel.get("language") or "pt")) if normalize_video_language(channel.get("language") or "pt") in VIDEO_LANGUAGE_SELECTION_OPTIONS else 0, format_func=video_language_label)
2491
- edit_source = st.selectbox("Fonte do vídeo", WIDE_STYLE_OPTIONS, index=WIDE_STYLE_OPTIONS.index(channel_video_source_value(channel.get("style_wide"))) if channel_video_source_value(channel.get("style_wide")) in WIDE_STYLE_OPTIONS else 0)
2492
- edit_aspect = st.selectbox("Proporção do vídeo", CHANNEL_ASPECT_RATIO_OPTIONS, index=CHANNEL_ASPECT_RATIO_OPTIONS.index(str(channel.get("video_aspect_ratio") or "Portrait 9:16")) if str(channel.get("video_aspect_ratio") or "Portrait 9:16") in CHANNEL_ASPECT_RATIO_OPTIONS else 1)
2493
- edit_niche = st.text_input("Nicho", value=str(channel.get("niche") or ""))
2494
- edit_voice = st.selectbox("Narrador/Voz Padrão", voice_options, index=voice_options.index(str(channel.get("default_voice") or channel.get("voice") or "")) if str(channel.get("default_voice") or channel.get("voice") or "") in voice_options else 0, format_func=lambda item: item or "Sem voz padrão")
2495
- edit_automation = st.toggle("Automação ON", value=bool(channel.get("automation_on", False)), key=f"tiktok_edit_automation_{channel_id}")
2496
- edit_time = st.text_input("Horário diário (HH:MM)", value=str(channel.get("automation_time") or "00:00"))
2497
- st.caption(f"Fonte do vídeo: {channel_video_source_value(channel.get('style_wide'))}")
2498
- edit_description = st.text_area("Descrição", value=str(channel.get("description") or ""))
2499
- if st.form_submit_button("Guardar edição", type="primary"):
2500
- if not valid_hhmm(edit_time):
2501
- st.error("O horário diário deve estar no formato HH:MM.")
2502
- else:
2503
- update_channel(channel_id, {"name": edit_name.strip(), "url": edit_url.strip(), "handle": edit_handle.strip(), "language": edit_language, "niche": edit_niche.strip(), "reference_channels": [edit_niche.strip()] if edit_niche.strip() else [], "default_voice": edit_voice, "voice": edit_voice, "style_wide": channel_video_source_storage(edit_source), "video_aspect_ratio": edit_aspect, "automation_on": edit_automation, "automation_time": edit_time.strip(), "description": edit_description.strip(), "platform": "tiktok"})
2504
- st.session_state.pop(f"tiktok_edit_{channel_id}", None)
2469
+ summary_cols = st.columns(4, gap="medium")
2470
+ with summary_cols[0]:
2471
+ st.markdown("**Blueprint Padrão**")
2472
+ st.caption(prompt_labels.get(str(channel.get("default_prompt_master") or channel.get("prompt_master") or ""), "Sem Blueprint padrão"))
2473
+ with summary_cols[1]:
2474
+ st.markdown("**Nicho**")
2475
+ st.caption(channel_niche_label(channel))
2476
+ with summary_cols[2]:
2477
+ st.markdown("**Narrador/Voz Padrão**")
2478
+ st.caption(str(channel.get("default_voice") or channel.get("voice") or "Sem voz padrão"))
2479
+ with summary_cols[3]:
2480
+ st.markdown("**Idioma**")
2481
+ st.caption(video_language_label(normalize_video_language(channel.get("language") or "pt")))
2482
+ st.caption(f"Fonte do vídeo: {channel_video_source_value(channel.get('style_wide'))} · Proporção: {channel.get('video_aspect_ratio') or 'Portrait 9:16'}")
2483
+ if st.session_state.get(f"tiktok_delete_{channel_id}"):
2484
+ st.warning("Apagar este canal TikTok e os dados locais associados?")
2485
+ confirm_cols = st.columns(2)
2486
+ with confirm_cols[0]:
2487
+ if st.button("Confirmar apagar", type="primary", key=f"tiktok_confirm_delete_{channel_id}"):
2488
+ delete_channel(channel_id)
2489
+ st.session_state.pop(f"tiktok_delete_{channel_id}", None)
2505
2490
  st.rerun()
2506
- render_channel_audio_subtitle_defaults(channel)
2507
- with st.expander("Últimos 10 vídeos publicados", expanded=False):
2508
- recent_videos = channel_videos_for(channel, limit=10)
2509
- if not recent_videos:
2510
- st.info("Ainda não existem vídeos públicos sincronizados para este canal TikTok.")
2511
- for recent_video in recent_videos[:10]:
2512
- video_cols = st.columns([0.7, 3.5, 1.2])
2513
- with video_cols[0]:
2514
- if recent_video.get("thumbnail_url"):
2515
- st.image(recent_video["thumbnail_url"], width=64)
2516
- else:
2517
- st.markdown("### TT")
2518
- with video_cols[1]:
2519
- st.write(f"**{recent_video.get('title') or 'Vídeo sem título'}**")
2520
- st.caption(f"{recent_video.get('published_at') or 'Sem data'} · {recent_video.get('url') or 'Sem URL'}")
2521
- with video_cols[2]:
2522
- st.caption(str(recent_video.get("status") or "publicado").title())
2523
- else:
2491
+ with confirm_cols[1]:
2492
+ if st.button("Cancelar", key=f"tiktok_cancel_delete_{channel_id}"):
2493
+ st.session_state.pop(f"tiktok_delete_{channel_id}", None)
2494
+ st.rerun()
2495
+ if st.session_state.get(f"tiktok_edit_{channel_id}"):
2496
+ with st.form(f"tiktok_edit_form_{channel_id}"):
2497
+ edit_name = st.text_input("Nome do canal", value=str(channel.get("name") or ""))
2498
+ edit_url = st.text_input("URL pública", value=str(channel.get("url") or ""))
2499
+ edit_handle = st.text_input("Handle", value=str(channel.get("handle") or ""))
2500
+ edit_language = st.selectbox("Idioma do roteiro", VIDEO_LANGUAGE_SELECTION_OPTIONS, index=VIDEO_LANGUAGE_SELECTION_OPTIONS.index(normalize_video_language(channel.get("language") or "pt")) if normalize_video_language(channel.get("language") or "pt") in VIDEO_LANGUAGE_SELECTION_OPTIONS else 0, format_func=video_language_label)
2501
+ edit_source = st.selectbox("Fonte do vídeo", WIDE_STYLE_OPTIONS, index=WIDE_STYLE_OPTIONS.index(channel_video_source_value(channel.get("style_wide"))) if channel_video_source_value(channel.get("style_wide")) in WIDE_STYLE_OPTIONS else 0)
2502
+ edit_aspect = st.selectbox("Proporção do vídeo", CHANNEL_ASPECT_RATIO_OPTIONS, index=CHANNEL_ASPECT_RATIO_OPTIONS.index(str(channel.get("video_aspect_ratio") or "Portrait 9:16")) if str(channel.get("video_aspect_ratio") or "Portrait 9:16") in CHANNEL_ASPECT_RATIO_OPTIONS else 1)
2503
+ edit_niche = st.text_input("Nicho", value=str(channel.get("niche") or ""))
2504
+ edit_voice = st.selectbox("Narrador/Voz Padrão", voice_options, index=voice_options.index(str(channel.get("default_voice") or channel.get("voice") or "")) if str(channel.get("default_voice") or channel.get("voice") or "") in voice_options else 0, format_func=lambda item: item or "Sem voz padrão")
2505
+ edit_automation = st.toggle("Automação ON", value=bool(channel.get("automation_on", False)), key=f"tiktok_edit_automation_{channel_id}")
2506
+ edit_time = st.text_input("Horário diário (HH:MM)", value=str(channel.get("automation_time") or "00:00"))
2507
+ st.caption(f"Fonte do vídeo: {channel_video_source_value(channel.get('style_wide'))}")
2508
+ edit_description = st.text_area("Descrição", value=str(channel.get("description") or ""))
2509
+ if st.form_submit_button("Guardar edição", type="primary"):
2510
+ if not valid_hhmm(edit_time):
2511
+ st.error("O horário diário deve estar no formato HH:MM.")
2512
+ else:
2513
+ update_channel(channel_id, {"name": edit_name.strip(), "url": edit_url.strip(), "handle": edit_handle.strip(), "language": edit_language, "niche": edit_niche.strip(), "reference_channels": [edit_niche.strip()] if edit_niche.strip() else [], "default_voice": edit_voice, "voice": edit_voice, "style_wide": channel_video_source_storage(edit_source), "video_aspect_ratio": edit_aspect, "automation_on": edit_automation, "automation_time": edit_time.strip(), "description": edit_description.strip(), "platform": "tiktok"})
2514
+ st.session_state.pop(f"tiktok_edit_{channel_id}", None)
2515
+ st.rerun()
2516
+ render_channel_audio_subtitle_defaults(channel)
2517
+ with st.expander("Últimos 10 vídeos publicados", expanded=False):
2518
+ recent_videos = channel_videos_for(channel, limit=10)
2519
+ if not recent_videos:
2520
+ st.info("Ainda não existem vídeos públicos sincronizados para este canal TikTok.")
2521
+ for recent_video in recent_videos[:10]:
2522
+ video_cols = st.columns([0.7, 3.5, 1.2])
2523
+ with video_cols[0]:
2524
+ if recent_video.get("thumbnail_url"):
2525
+ st.image(recent_video["thumbnail_url"], width=64)
2526
+ else:
2527
+ st.markdown("### TT")
2528
+ with video_cols[1]:
2529
+ st.write(f"**{recent_video.get('title') or 'Vídeo sem título'}**")
2530
+ st.caption(f"{recent_video.get('published_at') or 'Sem data'} · {recent_video.get('url') or 'Sem URL'}")
2531
+ with video_cols[2]:
2532
+ st.caption(str(recent_video.get("status") or "publicado").title())
2533
+ elif not channels:
2524
2534
  st.info("Ainda não existem canais TikTok cadastrados. Use o campo abaixo para pesquisar e cadastrar um perfil público.")
2535
+ else:
2536
+ st.info("Nenhum canal TikTok corresponde à pesquisa.")
2525
2537
  with spreadsheet_tab:
2526
2538
  uploaded = st.file_uploader("Upload da planilha de canais TikTok", type=["xlsx", "xls"], key="tiktok_channel_spreadsheet")
2527
2539
  if uploaded and st.button("Ler e cadastrar planilha TikTok", type="primary", key="tiktok_read_sheet"):
@@ -2900,10 +2912,22 @@ def render_channels():
2900
2912
  st.rerun()
2901
2913
 
2902
2914
  st.divider()
2903
- st.subheader("Canais cadastrados")
2904
2915
  registered_channels = [channel for channel in read_json("channels.json", []) if is_youtube_channel_record(channel)]
2916
+ st.subheader(f"Canais Youtube cadastrados ({len(registered_channels)})")
2917
+ youtube_channel_search = st.text_input(
2918
+ "Pesquisar nomes dos canais YouTube",
2919
+ key="youtube_registered_channels_search",
2920
+ placeholder="Digite o nome do canal",
2921
+ )
2922
+ visible_registered_channels = [
2923
+ channel for channel in registered_channels
2924
+ if not youtube_channel_search.strip()
2925
+ or youtube_channel_search.strip().casefold() in str(channel.get("name") or "").casefold()
2926
+ ]
2905
2927
  if not registered_channels:
2906
2928
  st.info("Nenhum canal cadastrado.")
2929
+ elif not visible_registered_channels:
2930
+ st.info("Nenhum canal YouTube corresponde à pesquisa.")
2907
2931
  else:
2908
2932
  wide_style_labels = {
2909
2933
  "pexels": "Pexels/Pixabay",
@@ -3108,11 +3132,11 @@ def render_channels():
3108
3132
  channels = [channel for channel in read_json("channels.json", []) if is_youtube_channel_record(channel)]
3109
3133
  if not channels:
3110
3134
  return
3111
- for channel in channels:
3135
+ for channel in visible_registered_channels:
3112
3136
  channel_id = str(channel["id"])
3113
3137
  edit_key = f"edit_channel_{channel_id}"
3114
3138
  with st.container(border=True):
3115
- header_cols = st.columns([0.7, 2.75, 1.1, 1.0, 1.35, 1.75, 1.35])
3139
+ header_cols = st.columns([0.7, 2.75, 1.1, 1.0, 1.35, 1.35])
3116
3140
  with header_cols[0]:
3117
3141
  if channel.get("thumbnail_url"):
3118
3142
  st.image(channel["thumbnail_url"], width=64)
@@ -3129,16 +3153,6 @@ def render_channels():
3129
3153
  with header_cols[4]:
3130
3154
  st.metric("Visualizações", _format_channel_count(channel.get("view_count")))
3131
3155
  with header_cols[5]:
3132
- calculated_time, calculated_source, calculated_words = _channel_average_video_time(channel)
3133
- average_video_time = st.text_input("Tempo Medio de Video", value=channel_video_time_value(channel), key=f"youtube_channel_average_video_time_{channel_id}", help=f"Referência: {calculated_words or 0} palavras · {calculated_source}.")
3134
- if st.button("Guardar tempo", key=f"save_youtube_channel_average_video_time_{channel_id}", width="stretch"):
3135
- if not valid_hhmm(average_video_time):
3136
- st.error("Use o formato MM:SS, por exemplo 12:00.")
3137
- else:
3138
- 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)})
3139
- st.success("Tempo médio do canal YouTube guardado.")
3140
- st.rerun()
3141
- with header_cols[6]:
3142
3156
  if st.button("↻", key=f"refresh_youtube_metrics_{channel_id}", help="Actualizar Inscritos, Vídeos e Visualizações", width="stretch"):
3143
3157
  with st.spinner("A actualizar métricas YouTube…"):
3144
3158
  refreshed, message = _refresh_youtube_channel_metrics(channel, youtube)
@@ -3149,106 +3163,116 @@ def render_channels():
3149
3163
  if active != channel.get("active"):
3150
3164
  update_channel(channel_id, {"active": active})
3151
3165
  st.rerun()
3166
+ delete_key = f"delete_pending_{channel_id}"
3167
+ with st.expander("Detalhes e configuração do canal", expanded=False):
3168
+ calculated_time, calculated_source, calculated_words = _channel_average_video_time(channel)
3169
+ average_video_time = st.text_input("Tempo Medio de Video", value=channel_video_time_value(channel), key=f"youtube_channel_average_video_time_{channel_id}", help=f"Referência: {calculated_words or 0} palavras · {calculated_source}.")
3170
+ if st.button("Guardar tempo", key=f"save_youtube_channel_average_video_time_{channel_id}", width="stretch"):
3171
+ if not valid_hhmm(average_video_time):
3172
+ st.error("Use o formato MM:SS, por exemplo 12:00.")
3173
+ else:
3174
+ 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)})
3175
+ st.success("Tempo médio do canal YouTube guardado.")
3176
+ st.rerun()
3152
3177
  action_cols = st.columns(2)
3153
3178
  with action_cols[0]:
3154
3179
  if st.button("Editar", key=f"edit_channel_button_{channel_id}", width="stretch"):
3155
3180
  st.session_state[edit_key] = True
3156
3181
  st.rerun()
3157
3182
  with action_cols[1]:
3158
- delete_key = f"delete_pending_{channel_id}"
3159
3183
  if st.button("Apagar", key=f"delete_{channel_id}", width="stretch"):
3160
3184
  st.session_state[delete_key] = True
3161
3185
  st.rerun()
3162
- if st.session_state.get(delete_key):
3163
- st.warning("Apagar este canal YouTube e os dados locais associados?")
3164
- confirm_cols = st.columns(2)
3165
- with confirm_cols[0]:
3166
- if st.button("Confirmar apagar", type="primary", key=f"confirm_delete_{channel_id}"):
3167
- removed = delete_channel(channel_id)
3168
- st.session_state.pop(delete_key, None)
3169
- if removed is None:
3170
- st.error("O canal já não existe ou não pôde ser removido.")
3171
- else:
3172
- st.success(f"Canal {removed.get('name') or channel_id} apagado.")
3186
+ if st.session_state.get(delete_key):
3187
+ st.warning("Apagar este canal YouTube e os dados locais associados?")
3188
+ confirm_cols = st.columns(2)
3189
+ with confirm_cols[0]:
3190
+ if st.button("Confirmar apagar", type="primary", key=f"confirm_delete_{channel_id}"):
3191
+ removed = delete_channel(channel_id)
3192
+ st.session_state.pop(delete_key, None)
3193
+ if removed is None:
3194
+ st.error("O canal já não existe ou não pôde ser removido.")
3195
+ else:
3196
+ st.success(f"Canal {removed.get('name') or channel_id} apagado.")
3197
+ st.rerun()
3198
+ with confirm_cols[1]:
3199
+ if st.button("Cancelar", key=f"cancel_delete_{channel_id}"):
3200
+ st.session_state.pop(delete_key, None)
3173
3201
  st.rerun()
3174
- with confirm_cols[1]:
3175
- if st.button("Cancelar", key=f"cancel_delete_{channel_id}"):
3176
- st.session_state.pop(delete_key, None)
3177
- st.rerun()
3178
- if st.session_state.get(edit_key):
3179
- render_channel_edit_form(channel, youtube_account_ids, youtube_account_labels, youtube_accounts_by_id)
3180
- else:
3181
- summary = channel_blueprint_summary(channel)
3182
- render_channel_thumbnail_blueprint_panel(channel, compact=True)
3183
- channel_language = language_label(channel.get("language") or "pt")
3184
- block_cols = st.columns(4, gap="small")
3185
- with block_cols[0]:
3186
- st.markdown(f"**Blueprint Padrão**\n\n{summary['name']}")
3187
- if st.button("Editar Blueprint", key=f"edit_prompts_{channel_id}", width="stretch"):
3188
- st.session_state[edit_key] = True
3189
- st.rerun()
3190
- with block_cols[1]:
3191
- st.markdown(f"**Nicho**\n\n{channel_niche_label(channel)}")
3192
- if st.button("Editar Nicho", key=f"edit_niche_{channel_id}", width="stretch"):
3193
- st.session_state[edit_key] = True
3194
- st.rerun()
3195
- with block_cols[2]:
3196
- st.markdown(f"**Narrador/Voz Padrão**\n\n{summary['voice'] or 'Sem voz padrão'}")
3197
- if st.button("Configurar Narrador/Voz", key=f"edit_voice_{channel_id}", width="stretch"):
3198
- st.session_state[edit_key] = True
3199
- st.rerun()
3200
- with block_cols[3]:
3201
- st.markdown(f"**Idioma**\n\n{channel_language}")
3202
-
3203
- channel_account_ids = list(youtube_account_ids)
3204
- current_channel_account_id = str(channel.get("google_account_id", ""))
3205
- if current_channel_account_id and current_channel_account_id not in channel_account_ids:
3206
- channel_account_ids.append(current_channel_account_id)
3207
- youtube_account_labels[current_channel_account_id] = "Conta Google não configurada"
3208
- with st.expander("Upload directo — documento da conta deste canal", expanded=False):
3209
- st.caption("O DELEGATED_SESSION_ID é individual deste canal. Guarde-o aqui; o valor fica no registo local do canal e não é copiado para o documento JSON partilhado da conta Google.")
3210
- with st.form(f"channel_direct_credentials_{channel_id}"):
3211
- channel_account_id = st.selectbox("Conta Google do documento deste canal", channel_account_ids, index=channel_account_ids.index(current_channel_account_id) if current_channel_account_id in channel_account_ids else 0, format_func=lambda item: youtube_account_labels.get(item, item or "Sem conta Google associada"), key=f"channel_account_{channel_id}")
3212
- channel_delegated_session_id = st.text_input(
3213
- "DELEGATED_SESSION_ID deste canal",
3214
- value=str(channel.get("delegated_session_id") or ""),
3215
- type="password",
3216
- key=f"channel_delegated_session_id_{channel_id}",
3217
- help="Identificador individual usado pelo Upload directo deste canal. Não é partilhado com outros canais nem mostrado nos diagnósticos.",
3218
- )
3219
- save_channel_direct_credentials = st.form_submit_button("Guardar conta Google e DELEGATED_SESSION_ID", type="primary", width="stretch")
3220
- selected_channel_account = youtube_accounts_by_id.get(channel_account_id)
3221
- if selected_channel_account:
3222
- selected_account_status = document_status(STORAGE, selected_channel_account, channel, settings, channels)
3223
- if selected_account_status["ready"]:
3224
- st.success("Documento da conta completo e DELEGATED_SESSION_ID deste canal encontrado no documento.")
3225
- elif not selected_account_status["document_exists"]:
3226
- st.warning("A conta seleccionada ainda não tem documento JSON de credenciais.")
3227
- else:
3228
- missing_channel_parts = list(selected_account_status["missing_cookies"])
3229
- if not selected_account_status["has_session_info"]:
3230
- missing_channel_parts.append("sessionInfo")
3231
- if not selected_account_status["has_innertube_api_key"]:
3232
- missing_channel_parts.append("INNERTUBE_API_KEY")
3233
- if not selected_account_status["has_delegated_session_id"]:
3234
- missing_channel_parts.append("DELEGATED_SESSION_ID deste canal")
3235
- st.warning(f"Documento incompleto: {', '.join(missing_channel_parts)}")
3202
+ if st.session_state.get(edit_key):
3203
+ render_channel_edit_form(channel, youtube_account_ids, youtube_account_labels, youtube_accounts_by_id)
3236
3204
  else:
3237
- st.info("Associe este canal a uma conta Google para validar o documento de credenciais.")
3238
- if save_channel_direct_credentials:
3239
- update_channel(
3240
- channel_id,
3241
- {
3242
- "google_account_id": channel_account_id.strip(),
3243
- "google_account_email": str(youtube_accounts_by_id.get(channel_account_id, {}).get("email", "")),
3244
- "delegated_session_id": channel_delegated_session_id.strip(),
3245
- },
3246
- )
3247
- st.success("Conta Google e DELEGATED_SESSION_ID individual do canal guardados.")
3248
- st.rerun()
3205
+ summary = channel_blueprint_summary(channel)
3206
+ render_channel_thumbnail_blueprint_panel(channel, compact=True)
3207
+ channel_language = language_label(channel.get("language") or "pt")
3208
+ block_cols = st.columns(4, gap="small")
3209
+ with block_cols[0]:
3210
+ st.markdown(f"**Blueprint Padrão**\n\n{summary['name']}")
3211
+ if st.button("Editar Blueprint", key=f"edit_prompts_{channel_id}", width="stretch"):
3212
+ st.session_state[edit_key] = True
3213
+ st.rerun()
3214
+ with block_cols[1]:
3215
+ st.markdown(f"**Nicho**\n\n{channel_niche_label(channel)}")
3216
+ if st.button("Editar Nicho", key=f"edit_niche_{channel_id}", width="stretch"):
3217
+ st.session_state[edit_key] = True
3218
+ st.rerun()
3219
+ with block_cols[2]:
3220
+ st.markdown(f"**Narrador/Voz Padrão**\n\n{summary['voice'] or 'Sem voz padrão'}")
3221
+ if st.button("Configurar Narrador/Voz", key=f"edit_voice_{channel_id}", width="stretch"):
3222
+ st.session_state[edit_key] = True
3223
+ st.rerun()
3224
+ with block_cols[3]:
3225
+ st.markdown(f"**Idioma**\n\n{channel_language}")
3226
+
3227
+ channel_account_ids = list(youtube_account_ids)
3228
+ current_channel_account_id = str(channel.get("google_account_id", ""))
3229
+ if current_channel_account_id and current_channel_account_id not in channel_account_ids:
3230
+ channel_account_ids.append(current_channel_account_id)
3231
+ youtube_account_labels[current_channel_account_id] = "Conta Google não configurada"
3232
+ with st.expander("Upload directo — documento da conta deste canal", expanded=False):
3233
+ st.caption("O DELEGATED_SESSION_ID é individual deste canal. Guarde-o aqui; o valor fica no registo local do canal e não é copiado para o documento JSON partilhado da conta Google.")
3234
+ with st.form(f"channel_direct_credentials_{channel_id}"):
3235
+ channel_account_id = st.selectbox("Conta Google do documento deste canal", channel_account_ids, index=channel_account_ids.index(current_channel_account_id) if current_channel_account_id in channel_account_ids else 0, format_func=lambda item: youtube_account_labels.get(item, item or "Sem conta Google associada"), key=f"channel_account_{channel_id}")
3236
+ channel_delegated_session_id = st.text_input(
3237
+ "DELEGATED_SESSION_ID deste canal",
3238
+ value=str(channel.get("delegated_session_id") or ""),
3239
+ type="password",
3240
+ key=f"channel_delegated_session_id_{channel_id}",
3241
+ help="Identificador individual usado pelo Upload directo deste canal. Não é partilhado com outros canais nem mostrado nos diagnósticos.",
3242
+ )
3243
+ save_channel_direct_credentials = st.form_submit_button("Guardar conta Google e DELEGATED_SESSION_ID", type="primary", width="stretch")
3244
+ selected_channel_account = youtube_accounts_by_id.get(channel_account_id)
3245
+ if selected_channel_account:
3246
+ selected_account_status = document_status(STORAGE, selected_channel_account, channel, settings, channels)
3247
+ if selected_account_status["ready"]:
3248
+ st.success("Documento da conta completo e DELEGATED_SESSION_ID deste canal encontrado no documento.")
3249
+ elif not selected_account_status["document_exists"]:
3250
+ st.warning("A conta seleccionada ainda não tem documento JSON de credenciais.")
3251
+ else:
3252
+ missing_channel_parts = list(selected_account_status["missing_cookies"])
3253
+ if not selected_account_status["has_session_info"]:
3254
+ missing_channel_parts.append("sessionInfo")
3255
+ if not selected_account_status["has_innertube_api_key"]:
3256
+ missing_channel_parts.append("INNERTUBE_API_KEY")
3257
+ if not selected_account_status["has_delegated_session_id"]:
3258
+ missing_channel_parts.append("DELEGATED_SESSION_ID deste canal")
3259
+ st.warning(f"Documento incompleto: {', '.join(missing_channel_parts)}")
3260
+ else:
3261
+ st.info("Associe este canal a uma conta Google para validar o documento de credenciais.")
3262
+ if save_channel_direct_credentials:
3263
+ update_channel(
3264
+ channel_id,
3265
+ {
3266
+ "google_account_id": channel_account_id.strip(),
3267
+ "google_account_email": str(youtube_accounts_by_id.get(channel_account_id, {}).get("email", "")),
3268
+ "delegated_session_id": channel_delegated_session_id.strip(),
3269
+ },
3270
+ )
3271
+ st.success("Conta Google e DELEGATED_SESSION_ID individual do canal guardados.")
3272
+ st.rerun()
3249
3273
 
3250
- render_channel_audio_subtitle_defaults(channel)
3251
- render_channel_videos(channel)
3274
+ render_channel_audio_subtitle_defaults(channel)
3275
+ render_channel_videos(channel)
3252
3276
 
3253
3277
 
3254
3278
  def _saved_video_draft_records() -> list[dict[str, Any]]:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.7.00",
3
+ "version": "0.7.02",
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",