@danhachuel/thunderbolt 0.2.56 → 0.2.59

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 +88 -3
  2. package/package.json +1 -1
package/app/main.py CHANGED
@@ -2223,6 +2223,68 @@ def render_upload_postiz():
2223
2223
  (st.success if result.ok else st.error)(result.message)
2224
2224
 
2225
2225
 
2226
+ UPLOAD_DESTINATION_TARGET_KEYS = {
2227
+ "TikTok": "tiktok_profiles",
2228
+ "Instagram": "instagram_profiles",
2229
+ "Facebook Pages": "facebook_pages",
2230
+ }
2231
+
2232
+
2233
+ def upload_target_label(target: Any) -> str:
2234
+ if isinstance(target, dict):
2235
+ name = str(target.get("name") or target.get("label") or target.get("title") or target.get("username") or target.get("id") or "Sem nome")
2236
+ handle = str(target.get("handle") or target.get("username") or target.get("url") or "")
2237
+ return f"{name} — {handle}" if handle and handle not in name else name
2238
+ return str(target)
2239
+
2240
+
2241
+ def upload_target_reference(target: Any) -> dict[str, str] | str | None:
2242
+ if target is None:
2243
+ return None
2244
+ if isinstance(target, dict):
2245
+ public_fields = ("id", "name", "label", "handle", "username", "url")
2246
+ return {field: str(target[field]) for field in public_fields if target.get(field)}
2247
+ return str(target)
2248
+
2249
+
2250
+ def upload_targets_for_destination(destination: str, channels: list[dict[str, Any]], settings: dict[str, Any]) -> list[Any]:
2251
+ if destination == "YouTube":
2252
+ return [channel for channel in channels if isinstance(channel, dict) and channel.get("id") and channel.get("active", True)]
2253
+ setting_key = UPLOAD_DESTINATION_TARGET_KEYS.get(destination)
2254
+ if not setting_key:
2255
+ return []
2256
+ configured_targets = settings.get(setting_key, [])
2257
+ if not isinstance(configured_targets, list):
2258
+ return []
2259
+ targets: list[Any] = []
2260
+ for target in configured_targets:
2261
+ if isinstance(target, dict) and target.get("id"):
2262
+ targets.append(target)
2263
+ elif isinstance(target, str) and target.strip():
2264
+ targets.append(target.strip())
2265
+ return targets
2266
+
2267
+
2268
+ def render_upload_destination_target(destination: str, channels: list[dict[str, Any]], settings: dict[str, Any]) -> Any | None:
2269
+ options = upload_targets_for_destination(destination, channels, settings)
2270
+ destination_key = re.sub(r"[^a-z0-9]+", "_", destination.lower()).strip("_")
2271
+ select_label = "Canal" if destination == "YouTube" else "Perfil / página"
2272
+ empty_label = "Nenhum canal YouTube cadastrado" if destination == "YouTube" else f"Nenhum {destination} configurado"
2273
+ if not options:
2274
+ st.selectbox(select_label, [empty_label], disabled=True, key=f"upload_target_{destination_key}")
2275
+ if destination == "YouTube":
2276
+ st.caption("Cadastre ou liste pelo menos um canal YouTube antes de escolher o destino de envio.")
2277
+ else:
2278
+ st.caption(f"A lista de {destination} será ligada numa etapa própria de credenciais/API.")
2279
+ return None
2280
+ return st.selectbox(
2281
+ select_label,
2282
+ options,
2283
+ format_func=upload_target_label,
2284
+ key=f"upload_target_{destination_key}",
2285
+ )
2286
+
2287
+
2226
2288
  def render_upload_conventional():
2227
2289
  st.title("Upload")
2228
2290
  settings = read_json("settings.json", {})
@@ -2233,6 +2295,12 @@ def render_upload_conventional():
2233
2295
  postiz = PostizAdapter(settings)
2234
2296
  tasks = [t for t in read_json("tasks.json", []) if t.get("state") == "done" or t.get("artifacts", {}).get("video")]
2235
2297
  destination = st.multiselect("Destinos", ["YouTube", "TikTok", "Instagram", "Facebook Pages"], default=["YouTube"], key="upload_destinations", placeholder="Seleccione os destinos")
2298
+ upload_targets: dict[str, Any | None] = {}
2299
+ if destination:
2300
+ st.markdown("**Onde enviar**")
2301
+ for target_destination in destination:
2302
+ with st.container(border=True):
2303
+ upload_targets[target_destination] = render_upload_destination_target(target_destination, channels, settings)
2236
2304
 
2237
2305
  if "Instagram" in destination:
2238
2306
  st.info("Instagram está disponível no front end. A publicação real será ligada numa etapa de credenciais/API própria.")
@@ -2277,7 +2345,8 @@ def render_upload_conventional():
2277
2345
  thumbnail_path = artifacts.get("thumbnail") or artifacts.get("cover", "")
2278
2346
  captions_path = artifacts.get("captions") or artifacts.get("subtitle", "")
2279
2347
  st.caption(video_path or "Sem caminho de vídeo registado")
2280
- channel = channel_map.get(str(task.get("channel_id")), {})
2348
+ selected_youtube_channel = upload_targets.get("YouTube") if "YouTube" in destination else None
2349
+ channel = selected_youtube_channel or channel_map.get(str(task.get("channel_id")), {})
2281
2350
  account = direct_accounts.get(str(channel.get("google_account_id", "")))
2282
2351
  if "YouTube" in destination:
2283
2352
  title = st.text_input("Título", value=task.get("title") or task.get("topic", "Vídeo Thunderbolt"), key=f"yt_title_{task['id']}")
@@ -2292,7 +2361,9 @@ def render_upload_conventional():
2292
2361
  language = st.text_input("Idioma", value="pt-BR", key=f"yt_language_{task['id']}")
2293
2362
  quota_count = official_upload_count(channel, account)
2294
2363
  st.caption(f"API Oficial hoje: {quota_count}/{OFFICIAL_DAILY_LIMIT} envios nesta conta Gmail.")
2295
- if st.button("Enviar pelo fluxo recomendado", type="primary", key=f"upload_youtube_{task['id']}"):
2364
+ if not selected_youtube_channel:
2365
+ st.caption("Seleccione primeiro um canal YouTube no selector acima para activar este envio.")
2366
+ if st.button("Enviar pelo fluxo recomendado", type="primary", key=f"upload_youtube_{task['id']}", disabled=not selected_youtube_channel, help="Escolha o canal YouTube no selector acima." if not selected_youtube_channel else None):
2296
2367
  tags = [tag.strip() for tag in tags_raw.split(",") if tag.strip()]
2297
2368
  result = upload_with_default_route(
2298
2369
  settings,
@@ -2312,6 +2383,7 @@ def render_upload_conventional():
2312
2383
  record = {
2313
2384
  "task_id": task.get("id"),
2314
2385
  "destination": "YouTube",
2386
+ "target": upload_target_reference(channel),
2315
2387
  "status": "published" if result.ok else "failed",
2316
2388
  "message": result.message,
2317
2389
  "data": result.data,
@@ -2324,8 +2396,21 @@ def render_upload_conventional():
2324
2396
  if result.data.get("attempts"):
2325
2397
  with st.expander("Detalhes dos mecanismos de upload"):
2326
2398
  st.json(result.data["attempts"])
2327
- if "TikTok" in destination and st.button("Enviar para TikTok", key=f"upload_tiktok_{task['id']}"):
2399
+ tiktok_target = upload_targets.get("TikTok") if "TikTok" in destination else None
2400
+ if "TikTok" in destination and st.button("Enviar para TikTok", key=f"upload_tiktok_{task['id']}", disabled=not tiktok_target, help="Escolha o perfil TikTok no selector acima." if not tiktok_target else None):
2328
2401
  result = TikTokAdapter(settings).upload_video(video_path, task.get("title") or task.get("topic", ""))
2402
+ record = {
2403
+ "task_id": task.get("id"),
2404
+ "destination": "TikTok",
2405
+ "target": upload_target_reference(tiktok_target),
2406
+ "status": "published" if result.ok else "failed",
2407
+ "message": result.message,
2408
+ "data": result.data,
2409
+ "created_at": now(),
2410
+ }
2411
+ uploads = read_json("uploads.json", [])
2412
+ uploads.append(record)
2413
+ write_json("uploads.json", uploads)
2329
2414
  (st.success if result.ok else st.warning)(result.message)
2330
2415
  if "Instagram" in destination:
2331
2416
  st.button("Preparar Instagram", key=f"upload_instagram_{task['id']}", disabled=True, help="UI preparada; publicação Instagram ainda não está activa.")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danhachuel/thunderbolt",
3
- "version": "0.2.56",
3
+ "version": "0.2.59",
4
4
  "description": "Thunderbolt — interface local para operação de canais faceless e motor MoneyPrinterTurbo",
5
5
  "main": "scripts/cli.mjs",
6
6
  "type": "module",