11
This commit is contained in:
@@ -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())
|
||||
}
|
||||
|
||||
@@ -6,4 +6,5 @@ pub mod avatar;
|
||||
pub mod video;
|
||||
pub mod fs_util;
|
||||
pub mod nodejs;
|
||||
pub mod publish;
|
||||
pub mod quickjs;
|
||||
|
||||
@@ -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,
|
||||
|
||||
276
src-tauri/src/commands/publish.rs
Normal file
276
src-tauri/src/commands/publish.rs
Normal 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 ¶ms.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
|
||||
}
|
||||
Reference in New Issue
Block a user