@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.
- package/app/main.py +203 -179
- 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
|
-
|
|
2409
|
-
|
|
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.
|
|
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
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
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
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
else
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
st.
|
|
2520
|
-
st.
|
|
2521
|
-
|
|
2522
|
-
st.caption(
|
|
2523
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
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
|
-
|
|
3175
|
-
|
|
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
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
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
|
-
|
|
3251
|
-
|
|
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