@danhachuel/thunderbolt 0.4.62 → 0.4.64

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.
@@ -839,3 +839,5 @@ Antes de clicar em **Enviar vídeo via Composio**, confirme o vídeo, o toolkit,
839
839
  Na **Automação Youtube**, Composio é a rota padrão quando a integração está activa e tem API key, slug da ferramenta e campo do ficheiro configurados. A ordem automática passa a ser **Composio → API Oficial → Upload directo → Postiz**. Se Composio falhar ou não estiver configurado, o worker tenta os fallbacks disponíveis e mantém o comportamento de publicação existente.
840
840
 
841
841
  Para garantir o canal correcto, configure também o **Campo do canal na ferramenta**. O worker lê o `youtube_channel_id` do canal associado à tarefa e substitui explicitamente esse campo nos argumentos enviados ao Composio. Se a tarefa não tiver um YouTube channel ID sincronizado, ou se os argumentos configurarem outro ID, o envio Composio é bloqueado e os fallbacks podem actuar; a publicação nunca é enviada silenciosamente para um canal diferente.
842
+
843
+ Na Automação Youtube, o Composio recebe ainda os mesmos parâmetros do upload oficial: privacidade `unlisted` (Não listado), categoria `22` e o locale do idioma do vídeo/canal, como `pt-BR`. Os nomes dos campos podem ser ajustados no painel, mas valores divergentes nos argumentos JSON são bloqueados.
package/app/main.py CHANGED
@@ -2109,15 +2109,29 @@ def render_channels():
2109
2109
  col1, col2 = st.columns([1, 1])
2110
2110
  with col1:
2111
2111
  if st.button("Buscar no YouTube", type="primary", use_container_width=True, key="youtube_channel_lookup"):
2112
- if lookup_mode.startswith("Página pública"):
2113
- result = youtube.fetch_channel_public(source)
2114
- elif not youtube.api_key:
2115
- result = IntegrationResult(False, "A YouTube Data API Key não está configurada. Escolha a opção Página pública — sem API Key ou configure a chave em Configurações > Contas Google.", {"status": "api_key_not_configured"})
2112
+ search_source = str(source or "").strip()
2113
+ st.session_state.pop("yt_import", None)
2114
+ st.session_state.pop("yt_message", None)
2115
+ st.session_state.pop("yt_ok", None)
2116
+ if not search_source:
2117
+ st.session_state["yt_message"] = "Introduza um URL youtube.com, um handle @nome ou um ID de canal UC... antes de pesquisar."
2118
+ st.session_state["yt_ok"] = False
2116
2119
  else:
2117
- result = youtube.fetch_channel(source)
2118
- st.session_state["yt_import"] = result.data
2119
- st.session_state["yt_message"] = result.message
2120
- st.session_state["yt_ok"] = result.ok
2120
+ try:
2121
+ with st.spinner("A pesquisar o canal no YouTube…"):
2122
+ if lookup_mode.startswith("Página pública"):
2123
+ result = youtube.fetch_channel_public(search_source)
2124
+ elif not youtube.api_key:
2125
+ result = IntegrationResult(False, "A YouTube Data API Key não está configurada. Escolha a opção Página pública — sem API Key ou configure a chave em Configurações > Contas Google.", {"status": "api_key_not_configured"})
2126
+ else:
2127
+ result = youtube.fetch_channel(search_source)
2128
+ st.session_state["yt_import"] = result.data if isinstance(result.data, dict) else {}
2129
+ st.session_state["yt_message"] = result.message
2130
+ st.session_state["yt_ok"] = result.ok
2131
+ except Exception as exc:
2132
+ st.session_state["yt_import"] = {}
2133
+ st.session_state["yt_message"] = f"A pesquisa do YouTube falhou ({type(exc).__name__}). Confirme o URL/handle e tente novamente. Detalhe: {str(exc)[:240]}"
2134
+ st.session_state["yt_ok"] = False
2121
2135
  with col2:
2122
2136
  if st.button("Limpar importação", use_container_width=True, key="youtube_channel_clear"):
2123
2137
  for key in ("yt_import", "yt_message", "yt_ok"):
@@ -7192,6 +7206,9 @@ def render_settings():
7192
7206
  composio_tool_slug = st.text_input("Slug da ferramenta para Automação Youtube", value=str(settings.get("composio_tool_slug") or ""), key="upload_composio_tool_slug", help="Depois de descobrir uma ferramenta em Upload via Composio, copie aqui o slug para a Automação Youtube.")
7193
7207
  composio_file_field = st.text_input("Campo do ficheiro na ferramenta", value=str(settings.get("composio_file_field") or "file"), key="upload_composio_file_field")
7194
7208
  composio_channel_field = st.text_input("Campo do canal na ferramenta", value=str(settings.get("composio_channel_field") or "channel_id"), key="upload_composio_channel_field", help="O worker substitui este campo pelo YouTube channel ID do canal da tarefa, impedindo o envio para outro canal.")
7209
+ composio_privacy_field = st.text_input("Campo de privacidade", value=str(settings.get("composio_privacy_field") or "privacy_status"), key="upload_composio_privacy_field", help="Recebe sempre `unlisted` (Não listado), como no upload oficial.")
7210
+ composio_category_field = st.text_input("Campo de categoria", value=str(settings.get("composio_category_field") or "category_id"), key="upload_composio_category_field", help="Recebe sempre `22`, categoria Pessoas e blogs, como no upload oficial.")
7211
+ composio_language_field = st.text_input("Campo de idioma", value=str(settings.get("composio_language_field") or "language"), key="upload_composio_language_field", help="Recebe o locale do idioma do vídeo/canal, por exemplo `pt-BR`.")
7195
7212
  composio_arguments_json = st.text_area("Argumentos JSON da Automação Youtube", value=str(settings.get("composio_arguments_json") or "{}"), key="upload_composio_arguments_json", height=120)
7196
7213
  _render_credential_status(composio_api_key)
7197
7214
  composio_action_cols = st.columns(2)
@@ -7200,7 +7217,7 @@ def render_settings():
7200
7217
  with composio_action_cols[1]:
7201
7218
  test_composio = st.button("Testar configuração", use_container_width=True, key="upload_composio_test")
7202
7219
  if save_composio:
7203
- settings.update({"composio_enabled": bool(composio_enabled), "composio_auto_upload": bool(composio_auto_upload), "composio_api_key": composio_api_key.strip(), "composio_user_id": composio_user_id.strip() or "thunderbolt-local", "composio_toolkit": composio_toolkit.strip(), "composio_tool_slug": composio_tool_slug.strip(), "composio_file_field": composio_file_field.strip() or "file", "composio_channel_field": composio_channel_field.strip() or "channel_id", "composio_arguments_json": composio_arguments_json.strip() or "{}"})
7220
+ settings.update({"composio_enabled": bool(composio_enabled), "composio_auto_upload": bool(composio_auto_upload), "composio_api_key": composio_api_key.strip(), "composio_user_id": composio_user_id.strip() or "thunderbolt-local", "composio_toolkit": composio_toolkit.strip(), "composio_tool_slug": composio_tool_slug.strip(), "composio_file_field": composio_file_field.strip() or "file", "composio_channel_field": composio_channel_field.strip() or "channel_id", "composio_privacy_field": composio_privacy_field.strip() or "privacy_status", "composio_category_field": composio_category_field.strip() or "category_id", "composio_language_field": composio_language_field.strip() or "language", "composio_arguments_json": composio_arguments_json.strip() or "{}"})
7204
7221
  write_json("settings.json", settings)
7205
7222
  st.success("Configuração Composio guardada.")
7206
7223
  st.rerun()
@@ -295,6 +295,9 @@ DEFAULTS: dict[str, Any] = {
295
295
  "composio_tool_slug": "",
296
296
  "composio_file_field": "file",
297
297
  "composio_channel_field": "channel_id",
298
+ "composio_privacy_field": "privacy_status",
299
+ "composio_category_field": "category_id",
300
+ "composio_language_field": "language",
298
301
  "composio_arguments_json": "{}",
299
302
  "postiz_enabled": False,
300
303
  "postiz_api_key": "",
@@ -20,6 +20,7 @@ from integrations.youtube_direct_credentials import document_status
20
20
  from integrations.youtube_direct_upload import YouTubeDirectUploader
21
21
  from integrations.composio_upload import ComposioUploadError, execute_upload
22
22
  from hermes_ui.storage import read_json, write_json
23
+ from hermes_ui.languages import language_locale
23
24
 
24
25
  OFFICIAL_DAILY_LIMIT = 5
25
26
  QUOTA_FILENAME = "official_upload_quota.json"
@@ -108,6 +109,9 @@ def upload_with_default_route(
108
109
  settings,
109
110
  channel=channel,
110
111
  video_path=video_path,
112
+ category_id=category_id,
113
+ language=language or channel.get("language") or "pt-BR",
114
+ privacy_status=privacy_status or "unlisted",
111
115
  title=title,
112
116
  description=description,
113
117
  tags=tags or [],
@@ -226,10 +230,21 @@ def _composio_upload(settings: dict[str, Any], *, channel: dict[str, Any], **kwa
226
230
  parsed_arguments = json.loads(arguments)
227
231
  if not isinstance(parsed_arguments, dict):
228
232
  return IntegrationResult(False, "Composio não foi executado: os argumentos JSON devem ser um objecto.", {})
229
- configured_channel = parsed_arguments.get(channel_field)
230
- if configured_channel not in (None, "", channel_id):
231
- return IntegrationResult(False, f"Composio bloqueado: o campo `{channel_field}` aponta para outro canal.", {"expected_channel_id": channel_id})
232
- parsed_arguments[channel_field] = channel_id
233
+ locked_values = {
234
+ channel_field: channel_id,
235
+ str(settings.get("composio_privacy_field") or "privacy_status").strip(): str(kwargs.get("privacy_status") or "unlisted"),
236
+ str(settings.get("composio_category_field") or "category_id").strip(): str(kwargs.get("category_id") or "22"),
237
+ str(settings.get("composio_language_field") or "language").strip(): language_locale(kwargs.get("language") or channel.get("language") or "pt"),
238
+ }
239
+ for field, expected in locked_values.items():
240
+ if not field:
241
+ return IntegrationResult(False, "Composio não foi executado: existe um campo obrigatório vazio na configuração.", {})
242
+ configured_value = parsed_arguments.get(field)
243
+ if configured_value not in (None, "", expected):
244
+ if field == channel_field:
245
+ return IntegrationResult(False, f"Composio bloqueado: o campo `{field}` aponta para outro canal.", {"field": field, "expected": expected})
246
+ return IntegrationResult(False, f"Composio bloqueado: o campo `{field}` tem um valor diferente do upload oficial ({expected}).", {"field": field, "expected": expected})
247
+ parsed_arguments[field] = expected
233
248
  result = execute_upload(
234
249
  str(settings.get("composio_api_key") or ""),
235
250
  str(settings.get("composio_user_id") or ""),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.4.62",
3
+ "version": "0.4.64",
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",