@danhachuel/thunderbolt 0.4.53 → 0.4.55

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 +114 -112
  2. package/package.json +1 -1
package/app/main.py CHANGED
@@ -1883,6 +1883,73 @@ def render_tiktok_channels():
1883
1883
  prompt_ids, prompt_labels = _tiktok_prompt_options()
1884
1884
  with import_tab:
1885
1885
  st.caption("A pesquisa consulta exclusivamente a página pública do TikTok. Não existe método alternativo nem utilização de API.")
1886
+ st.divider()
1887
+ st.subheader("Importar novo canal pela página pública")
1888
+ source = st.text_input("URL pública ou @handle", placeholder="https://www.tiktok.com/@conta", key="tiktok_channel_source")
1889
+ lookup_cols = st.columns([1, 1])
1890
+ with lookup_cols[0]:
1891
+ if st.button("Buscar no Tiktok", type="primary", use_container_width=True, key="tiktok_channel_lookup"):
1892
+ result = fetch_public_tiktok_profile(source)
1893
+ st.session_state["tiktok_channel_import"] = {"ok": result.ok, "message": result.message, "data": result.data}
1894
+ with lookup_cols[1]:
1895
+ if st.button("Limpar importação", use_container_width=True, key="tiktok_channel_clear"):
1896
+ for key in ("tiktok_channel_import", "tiktok_import_language", "tiktok_import_niche", "tiktok_import_prompt", "tiktok_import_description"):
1897
+ st.session_state.pop(key, None)
1898
+ st.rerun()
1899
+ imported_state = st.session_state.get("tiktok_channel_import", {})
1900
+ if imported_state.get("message"):
1901
+ (st.success if imported_state.get("ok") else st.warning)(imported_state["message"])
1902
+ imported = imported_state.get("data") if imported_state.get("ok") else {}
1903
+ if imported:
1904
+ st.caption("Dados encontrados na página pública. Reveja os cartões e edite os campos antes de guardar.")
1905
+ with st.container(border=True):
1906
+ profile_cols = st.columns([0.8, 2.2, 1, 1, 1])
1907
+ with profile_cols[0]:
1908
+ avatar = _tiktok_avatar_url(imported)
1909
+ if avatar:
1910
+ st.image(avatar, width=82)
1911
+ else:
1912
+ st.markdown("### TT")
1913
+ with profile_cols[1]:
1914
+ st.markdown(f"### {imported.get('name') or imported.get('username') or 'Perfil TikTok'}")
1915
+ st.write(imported.get("handle") or f"@{imported.get('username', '')}")
1916
+ st.caption(imported.get("public_url") or imported.get("url") or "Página pública")
1917
+ with profile_cols[2]:
1918
+ st.metric("Seguidores", format_metric_number(imported.get("subscriber_count")))
1919
+ with profile_cols[3]:
1920
+ st.metric("Curtidas", format_metric_number(imported.get("likes_count")))
1921
+ with profile_cols[4]:
1922
+ st.metric("Vídeos", format_metric_number(imported.get("video_count")))
1923
+ if imported.get("bio"):
1924
+ st.caption(f"Descrição pública: {imported['bio']}")
1925
+ with st.container(border=True):
1926
+ st.markdown("**Dados do canal a cadastrar**")
1927
+ data_cols = st.columns(3)
1928
+ with data_cols[0]:
1929
+ st.write(f"**Nome:** {imported.get('name') or imported.get('username') or '—'}")
1930
+ st.write(f"**Handle:** {imported.get('handle') or '—'}")
1931
+ with data_cols[1]:
1932
+ st.write(f"**URL:** {imported.get('public_url') or imported.get('url') or '—'}")
1933
+ st.write(f"**Origem:** {imported.get('metrics_source') or 'tiktok_public_page'}")
1934
+ with data_cols[2]:
1935
+ st.write("**Formato:** Portrait 9:16")
1936
+ st.write("**Consulta:** Página pública")
1937
+ with st.form("tiktok_channel_import_form"):
1938
+ name = st.text_input("Nome canal", value=str(imported.get("name") or imported.get("username") or ""))
1939
+ url = st.text_input("URL canal", value=str(imported.get("public_url") or imported.get("url") or ""))
1940
+ handle = st.text_input("Handle canal", value=str(imported.get("handle") or ""))
1941
+ language = st.selectbox("Idioma", list(LANGUAGE_CODES), format_func=language_label, key="tiktok_import_language")
1942
+ niche = st.text_input("Nicho", key="tiktok_import_niche")
1943
+ prompt = st.selectbox("Prompt Master padrão", prompt_ids, format_func=lambda item: prompt_labels.get(item, item), key="tiktok_import_prompt")
1944
+ description = st.text_area("Descrição", value=str(imported.get("bio") or ""), key="tiktok_import_description")
1945
+ if st.form_submit_button("Cadastrar canal TikTok", type="primary"):
1946
+ channel = create_channel(name.strip(), url.strip(), {"platform": "tiktok", "handle": handle.strip(), "language": language, "niche": niche.strip(), "description": description.strip(), "default_prompt_master": prompt, "prompt_master": prompt, "style_wide": "portrait", "video_aspect_ratio": "Portrait 9:16", "metrics_source": "tiktok_public_page", "subscriber_count": imported.get("subscriber_count"), "video_count": imported.get("video_count")})
1947
+ avatar_url = _tiktok_avatar_url(imported)
1948
+ update_channel(channel["id"], {"platform": "tiktok", "default_prompt_master": prompt, "prompt_master": prompt, "avatar_url": avatar_url, "thumbnail_url": avatar_url})
1949
+ st.session_state.pop("tiktok_channel_import", None)
1950
+ st.success(f"Canal {channel['name']} cadastrado.")
1951
+ st.rerun()
1952
+ st.divider()
1886
1953
  st.subheader(f"Canais TikTok cadastrados ({len(channels)})")
1887
1954
  if channels:
1888
1955
  for channel in channels:
@@ -1975,72 +2042,6 @@ def render_tiktok_channels():
1975
2042
  st.caption(str(recent_video.get("status") or "publicado").title())
1976
2043
  else:
1977
2044
  st.info("Ainda não existem canais TikTok cadastrados. Use o campo abaixo para pesquisar e cadastrar um perfil público.")
1978
- st.divider()
1979
- st.subheader("Importar novo canal pela página pública")
1980
- source = st.text_input("URL pública ou @handle", placeholder="https://www.tiktok.com/@conta", key="tiktok_channel_source")
1981
- lookup_cols = st.columns([1, 1])
1982
- with lookup_cols[0]:
1983
- if st.button("Buscar no Tiktok", type="primary", use_container_width=True, key="tiktok_channel_lookup"):
1984
- result = fetch_public_tiktok_profile(source)
1985
- st.session_state["tiktok_channel_import"] = {"ok": result.ok, "message": result.message, "data": result.data}
1986
- with lookup_cols[1]:
1987
- if st.button("Limpar importação", use_container_width=True, key="tiktok_channel_clear"):
1988
- for key in ("tiktok_channel_import", "tiktok_import_language", "tiktok_import_niche", "tiktok_import_prompt", "tiktok_import_description"):
1989
- st.session_state.pop(key, None)
1990
- st.rerun()
1991
- imported_state = st.session_state.get("tiktok_channel_import", {})
1992
- if imported_state.get("message"):
1993
- (st.success if imported_state.get("ok") else st.warning)(imported_state["message"])
1994
- imported = imported_state.get("data") if imported_state.get("ok") else {}
1995
- if imported:
1996
- st.caption("Dados encontrados na página pública. Reveja os cartões e edite os campos antes de guardar.")
1997
- with st.container(border=True):
1998
- profile_cols = st.columns([0.8, 2.2, 1, 1, 1])
1999
- with profile_cols[0]:
2000
- avatar = _tiktok_avatar_url(imported)
2001
- if avatar:
2002
- st.image(avatar, width=82)
2003
- else:
2004
- st.markdown("### TT")
2005
- with profile_cols[1]:
2006
- st.markdown(f"### {imported.get('name') or imported.get('username') or 'Perfil TikTok'}")
2007
- st.write(imported.get("handle") or f"@{imported.get('username', '')}")
2008
- st.caption(imported.get("public_url") or imported.get("url") or "Página pública")
2009
- with profile_cols[2]:
2010
- st.metric("Seguidores", format_metric_number(imported.get("subscriber_count")))
2011
- with profile_cols[3]:
2012
- st.metric("Curtidas", format_metric_number(imported.get("likes_count")))
2013
- with profile_cols[4]:
2014
- st.metric("Vídeos", format_metric_number(imported.get("video_count")))
2015
- if imported.get("bio"):
2016
- st.caption(f"Descrição pública: {imported['bio']}")
2017
- with st.container(border=True):
2018
- st.markdown("**Dados do canal a cadastrar**")
2019
- data_cols = st.columns(3)
2020
- with data_cols[0]:
2021
- st.write(f"**Nome:** {imported.get('name') or imported.get('username') or '—'}")
2022
- st.write(f"**Handle:** {imported.get('handle') or '—'}")
2023
- with data_cols[1]:
2024
- st.write(f"**URL:** {imported.get('public_url') or imported.get('url') or '—'}")
2025
- st.write(f"**Origem:** {imported.get('metrics_source') or 'tiktok_public_page'}")
2026
- with data_cols[2]:
2027
- st.write("**Formato:** Portrait 9:16")
2028
- st.write("**Consulta:** Página pública")
2029
- with st.form("tiktok_channel_import_form"):
2030
- name = st.text_input("Nome canal", value=str(imported.get("name") or imported.get("username") or ""))
2031
- url = st.text_input("URL canal", value=str(imported.get("public_url") or imported.get("url") or ""))
2032
- handle = st.text_input("Handle canal", value=str(imported.get("handle") or ""))
2033
- language = st.selectbox("Idioma", list(LANGUAGE_CODES), format_func=language_label, key="tiktok_import_language")
2034
- niche = st.text_input("Nicho", key="tiktok_import_niche")
2035
- prompt = st.selectbox("Prompt Master padrão", prompt_ids, format_func=lambda item: prompt_labels.get(item, item), key="tiktok_import_prompt")
2036
- description = st.text_area("Descrição", value=str(imported.get("bio") or ""), key="tiktok_import_description")
2037
- if st.form_submit_button("Cadastrar canal TikTok", type="primary"):
2038
- channel = create_channel(name.strip(), url.strip(), {"platform": "tiktok", "handle": handle.strip(), "language": language, "niche": niche.strip(), "description": description.strip(), "default_prompt_master": prompt, "prompt_master": prompt, "style_wide": "portrait", "video_aspect_ratio": "Portrait 9:16", "metrics_source": "tiktok_public_page", "subscriber_count": imported.get("subscriber_count"), "video_count": imported.get("video_count")})
2039
- avatar_url = _tiktok_avatar_url(imported)
2040
- update_channel(channel["id"], {"platform": "tiktok", "default_prompt_master": prompt, "prompt_master": prompt, "avatar_url": avatar_url, "thumbnail_url": avatar_url})
2041
- st.session_state.pop("tiktok_channel_import", None)
2042
- st.success(f"Canal {channel['name']} cadastrado.")
2043
- st.rerun()
2044
2045
  with spreadsheet_tab:
2045
2046
  uploaded = st.file_uploader("Upload da planilha de canais TikTok", type=["xlsx", "xls"], key="tiktok_channel_spreadsheet")
2046
2047
  if uploaded and st.button("Ler e cadastrar planilha TikTok", type="primary", key="tiktok_read_sheet"):
@@ -5899,7 +5900,7 @@ def render_bilibili_api_cards(settings: dict[str, Any]) -> None:
5899
5900
  st.success("Novo card Bilibili criado.")
5900
5901
  st.rerun()
5901
5902
 
5902
- def render_google_accounts():
5903
+ def render_google_accounts(*, include_innertube: bool = True):
5903
5904
  st.title("Contas Google")
5904
5905
  st.caption("Gestão das contas Google/YouTube, documentos de credenciais, canais em lote e credenciais globais do YouTube.")
5905
5906
  settings = read_json("settings.json", {})
@@ -6065,57 +6066,58 @@ def render_google_accounts():
6065
6066
  if youtube_accounts_missing_document:
6066
6067
  st.info("Contas que ainda precisam de dados no documento: " + ", ".join(youtube_accounts_missing_document))
6067
6068
 
6068
- st.divider()
6069
- st.markdown("### INNERTUBE_API_KEY")
6070
- st.caption("Esta é uma chave API global do YouTube: aplica-se a todas as contas Google/YouTube e a todo o sistema. Não é associada a uma conta específica, não faz parte dos documentos de cookies/credenciais e não é editada no separador API Keys.")
6071
- configured_global_innertube_api_key = str(settings.get("direct_innertube_api_key") or settings.get("INNERTUBE_API_KEY") or "").strip()
6072
- current_innertube_api_key = configured_global_innertube_api_key
6073
- if not current_innertube_api_key:
6074
- # Migrate values created by older releases, which incorrectly stored
6075
- # the key inside a selected Google account. The first legacy value is
6076
- # retained as the single global value and all account copies are removed.
6077
- for legacy_account in batch_accounts:
6078
- current_innertube_api_key = str(legacy_account.get("innertube_api_key") or legacy_account.get("INNERTUBE_API_KEY") or "").strip()
6069
+ if include_innertube:
6070
+ st.divider()
6071
+ st.markdown("### INNERTUBE_API_KEY")
6072
+ st.caption("Esta é uma chave API global do YouTube: aplica-se a todas as contas Google/YouTube e a todo o sistema. Não é associada a uma conta específica, não faz parte dos documentos de cookies/credenciais e não é editada no separador API Keys.")
6073
+ configured_global_innertube_api_key = str(settings.get("direct_innertube_api_key") or settings.get("INNERTUBE_API_KEY") or "").strip()
6074
+ current_innertube_api_key = configured_global_innertube_api_key
6075
+ if not current_innertube_api_key:
6076
+ # Migrate values created by older releases, which incorrectly stored
6077
+ # the key inside a selected Google account. The first legacy value is
6078
+ # retained as the single global value and all account copies are removed.
6079
+ for legacy_account in batch_accounts:
6080
+ current_innertube_api_key = str(legacy_account.get("innertube_api_key") or legacy_account.get("INNERTUBE_API_KEY") or "").strip()
6081
+ if current_innertube_api_key:
6082
+ break
6079
6083
  if current_innertube_api_key:
6080
- break
6081
- if current_innertube_api_key:
6082
- settings["direct_innertube_api_key"] = current_innertube_api_key
6084
+ settings["direct_innertube_api_key"] = current_innertube_api_key
6085
+ settings.pop("INNERTUBE_API_KEY", None)
6086
+ for legacy_account in batch_accounts:
6087
+ legacy_account.pop("innertube_api_key", None)
6088
+ legacy_account.pop("INNERTUBE_API_KEY", None)
6089
+ settings["youtube_batch_accounts"] = batch_accounts
6090
+ write_json("settings.json", settings)
6091
+ innertube_status_cols = st.columns([3.2, 1.2])
6092
+ with innertube_status_cols[0]:
6093
+ st.caption("Estado da chave global")
6094
+ with innertube_status_cols[1]:
6095
+ _render_credential_status(current_innertube_api_key)
6096
+ with st.form("innertube_api_key_form"):
6097
+ innertube_api_key_value = st.text_input(
6098
+ "INNERTUBE_API_KEY",
6099
+ value=current_innertube_api_key,
6100
+ type="password",
6101
+ key="global_innertube_api_key",
6102
+ help="Chave global usada pelo Upload directo para todas as contas Google/YouTube. Guarde-a na configuração global, separada dos documentos de cookies.",
6103
+ )
6104
+ _render_api_test_control(
6105
+ settings,
6106
+ "innertube",
6107
+ lambda: test_innertube_api_key(innertube_api_key_value),
6108
+ widget_key="api_test_innertube",
6109
+ )
6110
+ save_innertube_api_key = st.form_submit_button("Guardar INNERTUBE_API_KEY global", type="primary", use_container_width=True)
6111
+ if save_innertube_api_key:
6112
+ settings["direct_innertube_api_key"] = innertube_api_key_value.strip()
6083
6113
  settings.pop("INNERTUBE_API_KEY", None)
6084
6114
  for legacy_account in batch_accounts:
6085
6115
  legacy_account.pop("innertube_api_key", None)
6086
6116
  legacy_account.pop("INNERTUBE_API_KEY", None)
6087
6117
  settings["youtube_batch_accounts"] = batch_accounts
6088
6118
  write_json("settings.json", settings)
6089
- innertube_status_cols = st.columns([3.2, 1.2])
6090
- with innertube_status_cols[0]:
6091
- st.caption("Estado da chave global")
6092
- with innertube_status_cols[1]:
6093
- _render_credential_status(current_innertube_api_key)
6094
- with st.form("innertube_api_key_form"):
6095
- innertube_api_key_value = st.text_input(
6096
- "INNERTUBE_API_KEY",
6097
- value=current_innertube_api_key,
6098
- type="password",
6099
- key="global_innertube_api_key",
6100
- help="Chave global usada pelo Upload directo para todas as contas Google/YouTube. Guarde-a na configuração global, separada dos documentos de cookies.",
6101
- )
6102
- _render_api_test_control(
6103
- settings,
6104
- "innertube",
6105
- lambda: test_innertube_api_key(innertube_api_key_value),
6106
- widget_key="api_test_innertube",
6107
- )
6108
- save_innertube_api_key = st.form_submit_button("Guardar INNERTUBE_API_KEY global", type="primary", use_container_width=True)
6109
- if save_innertube_api_key:
6110
- settings["direct_innertube_api_key"] = innertube_api_key_value.strip()
6111
- settings.pop("INNERTUBE_API_KEY", None)
6112
- for legacy_account in batch_accounts:
6113
- legacy_account.pop("innertube_api_key", None)
6114
- legacy_account.pop("INNERTUBE_API_KEY", None)
6115
- settings["youtube_batch_accounts"] = batch_accounts
6116
- write_json("settings.json", settings)
6117
- st.success("INNERTUBE_API_KEY global guardada para todas as contas Google/YouTube e para todo o sistema.")
6118
- st.rerun()
6119
+ st.success("INNERTUBE_API_KEY global guardada para todas as contas Google/YouTube e para todo o sistema.")
6120
+ st.rerun()
6119
6121
 
6120
6122
  st.divider()
6121
6123
  st.markdown("### Adicionar outra conta Gmail")
@@ -7044,7 +7046,7 @@ def render_settings():
7044
7046
  st.subheader("API Keys Upload")
7045
7047
  st.caption("Credenciais e integrações utilizadas pelos fluxos de publicação. Cada grupo fica separado para evitar misturar chaves de geração com chaves de upload.")
7046
7048
  with st.expander("Contas Google", expanded=False):
7047
- render_google_accounts()
7049
+ render_google_accounts(include_innertube=False)
7048
7050
  with st.expander("API Innertube", expanded=False):
7049
7051
  st.caption("INNERTUBE_API_KEY global usada pelo Upload directo para todas as contas Google/YouTube.")
7050
7052
  upload_innertube = st.text_input("INNERTUBE_API_KEY", value=str(settings.get("direct_innertube_api_key") or settings.get("INNERTUBE_API_KEY") or ""), type="password", key="upload_api_innertube_key")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.4.53",
3
+ "version": "0.4.55",
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",