This commit is contained in:
fengchuanhn@gmail.com
2026-05-21 00:19:08 +08:00
parent 3215d39a32
commit 3b0a8d777c
63 changed files with 12483 additions and 1114 deletions

View File

@@ -3,6 +3,8 @@
use base64::{engine::general_purpose::STANDARD, Engine as _};
use std::path::{Component, Path, PathBuf};
use crate::ffmpeg;
fn normalize_path(path: &str) -> PathBuf {
Path::new(path)
.components()
@@ -16,6 +18,15 @@ fn is_allowed_local_media(path: &Path) -> bool {
if lower.contains("aiclient-voice-preview-cache")
|| lower.contains("aiclient-node-pipeline")
|| lower.contains("aiclient-generated-speech")
|| lower.contains("aiclient-runninghub")
|| lower.contains("aiclient-digital-human")
|| lower.contains("generated_audios")
|| lower.contains("generated_videos")
|| lower.contains("aiclient-video-edit")
|| lower.contains("aiclient-subtitle-bgm")
|| lower.contains("aiclient-cover")
|| lower.contains("aiclient-cover-previews")
|| lower.contains("avatars")
{
return true;
}
@@ -45,3 +56,21 @@ pub async fn read_local_file_base64(path: String) -> Result<String, String> {
.map_err(|e| format!("读取文件失败: {e}"))?;
Ok(STANDARD.encode(bytes))
}
/// 获取本地音视频时长(秒),供口播视频云端任务对齐音频长度。
#[tauri::command]
pub async fn get_media_duration_seconds(path: String) -> Result<f64, String> {
let normalized = normalize_path(path.trim());
if normalized.as_os_str().is_empty() {
return Err("路径为空".into());
}
if !normalized.is_file() {
return Err(format!("文件不存在: {}", normalized.display()));
}
if !is_allowed_local_media(&normalized) {
return Err("不允许读取该路径下的文件".into());
}
ffmpeg::probe_media_duration(&normalized)
.await
.map_err(|e| e.to_string())
}

View File

@@ -6,4 +6,5 @@ pub mod avatar;
pub mod video;
pub mod fs_util;
pub mod nodejs;
pub mod publish;
pub mod quickjs;

View File

@@ -63,7 +63,7 @@ fn log_node_event(script_name: &str, level: &str, msg: &str, fields: &Option<Val
}
}
fn spawn_event_pump(
pub(crate) fn spawn_event_pump(
app: AppHandle,
window: Window,
script_name: String,

View File

@@ -0,0 +1,276 @@
//! 视频发布:账号管理与 Node 发布脚本桥接
use serde::Deserialize;
use serde_json::{json, Value};
use tauri::{AppHandle, Manager, State, Window};
use crate::app_config::AppConfig;
use crate::publish_db::PlatformAccountRecord;
use crate::publish_store::PublishStore;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PublishLoginParams {
pub platform: String,
pub account_id: Option<i64>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PublishCheckLoginParams {
pub platform: String,
pub account_id: Option<i64>,
#[serde(default)]
pub check_browser: bool,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PublishPlatformItem {
pub platform: String,
pub account_id: Option<i64>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PublishExecuteParams {
pub platforms: Vec<PublishPlatformItem>,
pub video_path: String,
pub cover_path: String,
pub title: String,
pub description: Option<String>,
pub tags: Option<Vec<String>>,
#[serde(default)]
pub auto_publish: bool,
}
async fn run_publish_script(
app: AppHandle,
window: Window,
script_name: &str,
params: Value,
) -> Result<Value, String> {
let app_config = app.state::<AppConfig>();
let config_env = app_config.env_for_node().await;
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let pump = crate::commands::nodejs::spawn_event_pump(
app.clone(),
window,
script_name.to_string(),
rx,
);
let result = crate::nodejs::run_node_script(
script_name.to_string(),
params,
tx,
config_env,
)
.await
.map_err(|e| e.to_string())?;
pump.await.ok();
Ok(result)
}
#[tauri::command]
pub async fn publish_list_accounts(
store: State<'_, PublishStore>,
platform: Option<String>,
) -> Result<Vec<PlatformAccountRecord>, String> {
store.list(platform).await
}
#[tauri::command]
pub async fn publish_delete_account(
store: State<'_, PublishStore>,
account_id: i64,
) -> Result<(), String> {
store.delete(account_id).await
}
#[tauri::command]
pub async fn publish_login(
app: AppHandle,
window: Window,
store: State<'_, PublishStore>,
app_config: State<'_, AppConfig>,
params: PublishLoginParams,
) -> Result<Value, String> {
let _ = app_config;
let result = run_publish_script(
app,
window,
"publish_login.js",
json!({
"platform": params.platform,
"accountId": params.account_id,
}),
)
.await?;
if result.get("success").and_then(|v| v.as_bool()) == Some(true) {
let nickname = result
.get("nickname")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let uid = result
.get("uid")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let cookies = result
.get("cookies")
.map(|v| serde_json::to_string(v).unwrap_or_else(|_| "[]".to_string()))
.unwrap_or_else(|| "[]".to_string());
let rec = store
.upsert_login(
params.platform.clone(),
nickname,
uid,
cookies,
params.account_id,
)
.await?;
return Ok(json!({
"success": true,
"message": result.get("message").and_then(|v| v.as_str()).unwrap_or("登录成功"),
"account": rec,
}));
}
Ok(result)
}
#[tauri::command]
pub async fn publish_check_login(
app: AppHandle,
window: Window,
store: State<'_, PublishStore>,
params: PublishCheckLoginParams,
) -> Result<Value, String> {
if !params.check_browser {
let accounts = store.list(Some(params.platform.clone())).await?;
let account = if let Some(id) = params.account_id {
accounts.into_iter().find(|a| a.id == id)
} else {
accounts
.into_iter()
.find(|a| a.login_status == "active" && a.has_cookies)
};
if let Some(acc) = account {
return Ok(json!({
"success": true,
"isLoggedIn": acc.login_status == "active" && acc.has_cookies,
"userInfo": { "nickname": acc.nickname, "userId": acc.uid },
}));
}
return Ok(json!({
"success": true,
"isLoggedIn": false,
}));
}
let mut cookies = json!([]);
if let Some(id) = params.account_id {
if let Some((_, c)) = store.get_with_cookies(id).await? {
cookies = serde_json::from_str(&c).unwrap_or(json!([]));
}
} else if let Ok(list) = store.list(Some(params.platform.clone())).await {
if let Some(active) = list
.iter()
.find(|a| a.login_status == "active" && a.has_cookies)
{
if let Some((_, c)) = store.get_with_cookies(active.id).await? {
cookies = serde_json::from_str(&c).unwrap_or(json!([]));
}
}
}
let result = run_publish_script(
app,
window,
"publish_check_login.js",
json!({
"platform": params.platform,
"accountId": params.account_id,
"cookies": cookies,
}),
)
.await?;
if result.get("isLoggedIn").and_then(|v| v.as_bool()) == Some(true) {
if let (Some(nickname), Some(account_id)) = (
result
.get("userInfo")
.and_then(|u| u.get("nickname"))
.and_then(|v| v.as_str()),
params.account_id,
) {
let _ = store.set_login_status(account_id, "active".to_string()).await;
if let Ok(Some((mut rec, _))) = store.get_with_cookies(account_id).await {
if !nickname.is_empty() {
rec.nickname = nickname.to_string();
}
}
}
} else if let Some(account_id) = params.account_id {
let _ = store.set_login_status(account_id, "inactive".to_string()).await;
}
Ok(result)
}
#[tauri::command]
pub async fn publish_execute(
app: AppHandle,
window: Window,
store: State<'_, PublishStore>,
params: PublishExecuteParams,
) -> Result<Value, String> {
let mut platform_payloads = Vec::new();
for item in &params.platforms {
let account_id = item.account_id;
let (rec, cookies) = if let Some(id) = account_id {
store
.get_with_cookies(id)
.await?
.ok_or_else(|| format!("未找到账号 id={id}"))?
} else {
let list = store.list(Some(item.platform.clone())).await?;
let active = list
.into_iter()
.find(|a| a.login_status == "active" && a.has_cookies)
.ok_or_else(|| format!("平台 {} 无已登录账号", item.platform))?;
let full = store
.get_with_cookies(active.id)
.await?
.ok_or_else(|| "读取账号失败".to_string())?;
full
};
if rec.login_status != "active" {
return Err(format!("账号 {} 未登录", rec.nickname));
}
platform_payloads.push(json!({
"platform": item.platform,
"accountId": rec.id,
"cookies": serde_json::from_str::<Value>(&cookies).unwrap_or(json!([])),
"nickname": rec.nickname,
}));
}
run_publish_script(
app,
window,
"video_publish.js",
json!({
"platforms": platform_payloads,
"videoPath": params.video_path,
"coverPath": params.cover_path,
"title": params.title,
"description": params.description.unwrap_or_default(),
"tags": params.tags.unwrap_or_default(),
"autoPublish": params.auto_publish,
}),
)
.await
}