@danhachuel/thunderbolt 0.4.54 → 0.4.56
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 +99 -70
- 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"):
|
|
@@ -7047,13 +7048,41 @@ def render_settings():
|
|
|
7047
7048
|
with st.expander("Contas Google", expanded=False):
|
|
7048
7049
|
render_google_accounts(include_innertube=False)
|
|
7049
7050
|
with st.expander("API Innertube", expanded=False):
|
|
7050
|
-
st.
|
|
7051
|
-
|
|
7052
|
-
|
|
7051
|
+
st.markdown("### INNERTUBE_API_KEY")
|
|
7052
|
+
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.")
|
|
7053
|
+
upload_batch_accounts = [item for item in settings.get("youtube_batch_accounts", []) if isinstance(item, dict) and item.get("id")]
|
|
7054
|
+
current_upload_innertube = str(settings.get("direct_innertube_api_key") or settings.get("INNERTUBE_API_KEY") or "").strip()
|
|
7055
|
+
if not current_upload_innertube:
|
|
7056
|
+
for legacy_account in upload_batch_accounts:
|
|
7057
|
+
current_upload_innertube = str(legacy_account.get("innertube_api_key") or legacy_account.get("INNERTUBE_API_KEY") or "").strip()
|
|
7058
|
+
if current_upload_innertube:
|
|
7059
|
+
break
|
|
7060
|
+
if current_upload_innertube:
|
|
7061
|
+
settings["direct_innertube_api_key"] = current_upload_innertube
|
|
7062
|
+
settings.pop("INNERTUBE_API_KEY", None)
|
|
7063
|
+
for legacy_account in upload_batch_accounts:
|
|
7064
|
+
legacy_account.pop("innertube_api_key", None)
|
|
7065
|
+
legacy_account.pop("INNERTUBE_API_KEY", None)
|
|
7066
|
+
settings["youtube_batch_accounts"] = upload_batch_accounts
|
|
7067
|
+
write_json("settings.json", settings)
|
|
7068
|
+
innertube_upload_status = st.columns([3.2, 1.2])
|
|
7069
|
+
with innertube_upload_status[0]:
|
|
7070
|
+
st.caption("Estado da chave global")
|
|
7071
|
+
with innertube_upload_status[1]:
|
|
7072
|
+
_render_credential_status(current_upload_innertube)
|
|
7073
|
+
with st.form("upload_innertube_api_key_form"):
|
|
7074
|
+
upload_innertube = st.text_input("INNERTUBE_API_KEY", value=current_upload_innertube, type="password", key="upload_api_innertube_key", help="Chave global usada pelo Upload directo para todas as contas Google/YouTube. Guarde-a na configuração global, separada dos documentos de cookies.")
|
|
7075
|
+
_render_api_test_control(settings, "innertube_upload", lambda: test_innertube_api_key(upload_innertube), widget_key="api_test_innertube_upload")
|
|
7076
|
+
save_upload_innertube = st.form_submit_button("Guardar INNERTUBE_API_KEY global", type="primary", use_container_width=True)
|
|
7077
|
+
if save_upload_innertube:
|
|
7053
7078
|
settings["direct_innertube_api_key"] = upload_innertube.strip()
|
|
7054
7079
|
settings.pop("INNERTUBE_API_KEY", None)
|
|
7080
|
+
for legacy_account in upload_batch_accounts:
|
|
7081
|
+
legacy_account.pop("innertube_api_key", None)
|
|
7082
|
+
legacy_account.pop("INNERTUBE_API_KEY", None)
|
|
7083
|
+
settings["youtube_batch_accounts"] = upload_batch_accounts
|
|
7055
7084
|
write_json("settings.json", settings)
|
|
7056
|
-
st.success("INNERTUBE_API_KEY guardada.")
|
|
7085
|
+
st.success("INNERTUBE_API_KEY global guardada para todas as contas Google/YouTube e para todo o sistema.")
|
|
7057
7086
|
st.rerun()
|
|
7058
7087
|
with st.expander("API Tiktok", expanded=False):
|
|
7059
7088
|
render_tiktok_api_cards(settings)
|
package/package.json
CHANGED