@danhachuel/thunderbolt 0.4.63 → 0.4.65

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
@@ -2086,6 +2086,23 @@ def render_tiktok_channels():
2086
2086
  update_channel(channel["id"], {"platform": "tiktok"})
2087
2087
  st.success("Canal TikTok cadastrado.")
2088
2088
  st.rerun()
2089
+ def is_youtube_channel_record(channel: Any) -> bool:
2090
+ """Return whether a persisted channel belongs in the YouTube UI."""
2091
+ if not isinstance(channel, dict):
2092
+ return False
2093
+ platform = str(channel.get("platform") or "").strip().casefold().replace(" ", "")
2094
+ if platform in {"tiktok", "tik-tok"}:
2095
+ return False
2096
+ for field in ("url", "profile_url", "source_url", "import_source", "metrics_source", "provider", "network"):
2097
+ value = str(channel.get(field) or "").strip().casefold()
2098
+ if "tiktok.com" in value or value.startswith("tiktok") or value == "tik-tok":
2099
+ return False
2100
+ for field in ("tiktok_id", "tiktok_username", "tiktok_handle", "tiktok_user_id", "tiktok_open_id"):
2101
+ if str(channel.get(field) or "").strip():
2102
+ return False
2103
+ return True
2104
+
2105
+
2089
2106
  def render_channels():
2090
2107
  st.title("Canais Youtube")
2091
2108
  st.caption("Escolha entre importar dados públicos do YouTube ou preencher o canal manualmente.")
@@ -2342,10 +2359,7 @@ def render_channels():
2342
2359
 
2343
2360
  st.divider()
2344
2361
  st.subheader("Canais cadastrados")
2345
- registered_channels = [
2346
- channel for channel in read_json("channels.json", [])
2347
- if isinstance(channel, dict) and str(channel.get("platform") or "youtube").strip().lower() != "tiktok"
2348
- ]
2362
+ registered_channels = [channel for channel in read_json("channels.json", []) if is_youtube_channel_record(channel)]
2349
2363
  if not registered_channels:
2350
2364
  st.info("Nenhum canal cadastrado.")
2351
2365
  else:
@@ -7206,6 +7220,9 @@ def render_settings():
7206
7220
  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.")
7207
7221
  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")
7208
7222
  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.")
7223
+ 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.")
7224
+ 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.")
7225
+ 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`.")
7209
7226
  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)
7210
7227
  _render_credential_status(composio_api_key)
7211
7228
  composio_action_cols = st.columns(2)
@@ -7214,7 +7231,7 @@ def render_settings():
7214
7231
  with composio_action_cols[1]:
7215
7232
  test_composio = st.button("Testar configuração", use_container_width=True, key="upload_composio_test")
7216
7233
  if save_composio:
7217
- 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 "{}"})
7234
+ 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 "{}"})
7218
7235
  write_json("settings.json", settings)
7219
7236
  st.success("Configuração Composio guardada.")
7220
7237
  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.63",
3
+ "version": "0.4.65",
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",