11
This commit is contained in:
@@ -3,6 +3,7 @@ pub mod app_config;
|
|||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod audio;
|
pub mod audio;
|
||||||
pub mod avatar;
|
pub mod avatar;
|
||||||
|
pub mod video;
|
||||||
pub mod fs_util;
|
pub mod fs_util;
|
||||||
pub mod nodejs;
|
pub mod nodejs;
|
||||||
pub mod quickjs;
|
pub mod quickjs;
|
||||||
|
|||||||
121
src-tauri/src/commands/video.rs
Normal file
121
src-tauri/src/commands/video.rs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
//! 工作流口播视频历史(SQLite + 文件落盘)
|
||||||
|
|
||||||
|
use std::path::{Component, Path, PathBuf};
|
||||||
|
|
||||||
|
use tauri::{AppHandle, Manager, State};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::video_db::GeneratedVideoRecord;
|
||||||
|
use crate::video_store::VideoStore;
|
||||||
|
|
||||||
|
fn normalize_path(path: &str) -> PathBuf {
|
||||||
|
Path::new(path)
|
||||||
|
.components()
|
||||||
|
.filter(|c| !matches!(c, Component::ParentDir))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generated_videos_dir(app: &AppHandle) -> Result<PathBuf, String> {
|
||||||
|
let dir = app
|
||||||
|
.path()
|
||||||
|
.app_data_dir()
|
||||||
|
.map_err(|e| format!("无法获取应用数据目录: {e}"))?
|
||||||
|
.join("generated_videos");
|
||||||
|
Ok(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_under_generated_videos_dir(root: &Path, target: &Path) -> bool {
|
||||||
|
if let (Ok(root), Ok(canonical)) = (root.canonicalize(), target.canonicalize()) {
|
||||||
|
return canonical.starts_with(&root);
|
||||||
|
}
|
||||||
|
let root_s = root.to_string_lossy().to_lowercase();
|
||||||
|
let target_s = target.to_string_lossy().to_lowercase();
|
||||||
|
target_s.contains("generated_videos") && target_s.starts_with(&root_s)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn list_generated_videos(
|
||||||
|
store: State<'_, VideoStore>,
|
||||||
|
) -> Result<Vec<GeneratedVideoRecord>, String> {
|
||||||
|
store.list().await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn insert_generated_video(
|
||||||
|
store: State<'_, VideoStore>,
|
||||||
|
name: String,
|
||||||
|
file_path: String,
|
||||||
|
avatar_id: Option<i64>,
|
||||||
|
audio_id: Option<i64>,
|
||||||
|
) -> Result<GeneratedVideoRecord, String> {
|
||||||
|
store.insert(name, file_path, avatar_id, audio_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn delete_generated_video(
|
||||||
|
app: AppHandle,
|
||||||
|
store: State<'_, VideoStore>,
|
||||||
|
id: i64,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let file_path = store.delete(id).await?;
|
||||||
|
if let Some(path) = file_path {
|
||||||
|
delete_video_file_if_allowed(&app, &path).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 将数字人输出视频复制到应用数据目录并写入 SQLite
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn import_generated_video(
|
||||||
|
app: AppHandle,
|
||||||
|
store: State<'_, VideoStore>,
|
||||||
|
source_path: String,
|
||||||
|
name: String,
|
||||||
|
avatar_id: Option<i64>,
|
||||||
|
audio_id: Option<i64>,
|
||||||
|
) -> Result<GeneratedVideoRecord, String> {
|
||||||
|
let src = normalize_path(source_path.trim());
|
||||||
|
if !src.is_file() {
|
||||||
|
return Err(format!("视频文件不存在: {}", src.display()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let ext = src
|
||||||
|
.extension()
|
||||||
|
.and_then(|e| e.to_str())
|
||||||
|
.unwrap_or("mp4")
|
||||||
|
.to_lowercase();
|
||||||
|
let allowed = ["mp4", "mov", "webm", "mkv", "m4v", "avi"];
|
||||||
|
if !allowed.contains(&ext.as_str()) {
|
||||||
|
return Err("仅支持 mp4、mov、webm、mkv 等常见视频格式".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let dir = generated_videos_dir(&app)?;
|
||||||
|
tokio::fs::create_dir_all(&dir)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("创建视频目录失败: {e}"))?;
|
||||||
|
|
||||||
|
let file_name = format!("{}.{}", Uuid::new_v4(), ext);
|
||||||
|
let dest = dir.join(&file_name);
|
||||||
|
|
||||||
|
tokio::fs::copy(&src, &dest)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("复制视频失败: {e}"))?;
|
||||||
|
|
||||||
|
store
|
||||||
|
.insert(name, dest.to_string_lossy().to_string(), avatar_id, audio_id)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_video_file_if_allowed(app: &AppHandle, path: &str) -> Result<(), String> {
|
||||||
|
let target = normalize_path(path);
|
||||||
|
let dir = generated_videos_dir(app)?;
|
||||||
|
if !is_under_generated_videos_dir(&dir, &target) {
|
||||||
|
return Err("不允许删除该路径下的文件".into());
|
||||||
|
}
|
||||||
|
if target.is_file() {
|
||||||
|
tokio::fs::remove_file(&target)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("删除文件失败: {e}"))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ pub mod audio_db;
|
|||||||
pub mod audio_store;
|
pub mod audio_store;
|
||||||
pub mod avatar_db;
|
pub mod avatar_db;
|
||||||
pub mod avatar_store;
|
pub mod avatar_store;
|
||||||
|
pub mod video_db;
|
||||||
|
pub mod video_store;
|
||||||
pub mod local_config;
|
pub mod local_config;
|
||||||
pub mod asr;
|
pub mod asr;
|
||||||
pub mod auth_session;
|
pub mod auth_session;
|
||||||
@@ -27,12 +29,14 @@ pub fn run() {
|
|||||||
.manage(app_config::AppConfig::new())
|
.manage(app_config::AppConfig::new())
|
||||||
.manage(avatar_store::AvatarStore::new())
|
.manage(avatar_store::AvatarStore::new())
|
||||||
.manage(audio_store::AudioStore::new())
|
.manage(audio_store::AudioStore::new())
|
||||||
|
.manage(video_store::VideoStore::new())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let handle = app.handle().clone();
|
let handle = app.handle().clone();
|
||||||
tauri::async_runtime::block_on(async move {
|
tauri::async_runtime::block_on(async move {
|
||||||
let app_config = handle.state::<app_config::AppConfig>();
|
let app_config = handle.state::<app_config::AppConfig>();
|
||||||
let avatar_store = handle.state::<avatar_store::AvatarStore>();
|
let avatar_store = handle.state::<avatar_store::AvatarStore>();
|
||||||
let audio_store = handle.state::<audio_store::AudioStore>();
|
let audio_store = handle.state::<audio_store::AudioStore>();
|
||||||
|
let video_store = handle.state::<video_store::VideoStore>();
|
||||||
let dir = handle
|
let dir = handle
|
||||||
.path()
|
.path()
|
||||||
.app_data_dir()
|
.app_data_dir()
|
||||||
@@ -41,6 +45,7 @@ pub fn run() {
|
|||||||
app_config.init_local_store(db_path).await?;
|
app_config.init_local_store(db_path).await?;
|
||||||
avatar_store.init(dir.join("avatars.db")).await?;
|
avatar_store.init(dir.join("avatars.db")).await?;
|
||||||
audio_store.init(dir.join("generated_audios.db")).await?;
|
audio_store.init(dir.join("generated_audios.db")).await?;
|
||||||
|
video_store.init(dir.join("generated_videos.db")).await?;
|
||||||
Ok::<(), String>(())
|
Ok::<(), String>(())
|
||||||
})
|
})
|
||||||
.map_err(|e| -> Box<dyn std::error::Error> { e.into() })?;
|
.map_err(|e| -> Box<dyn std::error::Error> { e.into() })?;
|
||||||
@@ -81,6 +86,10 @@ pub fn run() {
|
|||||||
commands::audio::list_generated_audios,
|
commands::audio::list_generated_audios,
|
||||||
commands::audio::insert_generated_audio,
|
commands::audio::insert_generated_audio,
|
||||||
commands::audio::delete_generated_audio,
|
commands::audio::delete_generated_audio,
|
||||||
|
commands::video::list_generated_videos,
|
||||||
|
commands::video::insert_generated_video,
|
||||||
|
commands::video::import_generated_video,
|
||||||
|
commands::video::delete_generated_video,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
196
src-tauri/src/video_db.rs
Normal file
196
src-tauri/src/video_db.rs
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
//! 工作流生成的口播视频 SQLite 存储
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use chrono::{Datelike, Local, Timelike};
|
||||||
|
use rusqlite::{params, Connection};
|
||||||
|
use serde::Serialize;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
const MAX_RECORDS: i64 = 30;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum VideoDbError {
|
||||||
|
#[error("{0}")]
|
||||||
|
Db(#[from] rusqlite::Error),
|
||||||
|
#[error("{0}")]
|
||||||
|
Message(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct GeneratedVideoRecord {
|
||||||
|
pub id: i64,
|
||||||
|
pub name: String,
|
||||||
|
pub file_path: String,
|
||||||
|
pub avatar_id: Option<i64>,
|
||||||
|
pub audio_id: Option<i64>,
|
||||||
|
pub created_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct VideoDb {
|
||||||
|
conn: Mutex<Connection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VideoDb {
|
||||||
|
pub fn open(path: PathBuf) -> Result<Self, VideoDbError> {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent).map_err(|e| {
|
||||||
|
VideoDbError::Message(format!("创建数据库目录失败: {e}"))
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
let conn = Connection::open(path)?;
|
||||||
|
conn.execute_batch(
|
||||||
|
r#"
|
||||||
|
PRAGMA journal_mode = WAL;
|
||||||
|
CREATE TABLE IF NOT EXISTS generated_videos (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL DEFAULT '',
|
||||||
|
file_path TEXT NOT NULL,
|
||||||
|
avatar_id INTEGER,
|
||||||
|
audio_id INTEGER,
|
||||||
|
created_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
Ok(Self {
|
||||||
|
conn: Mutex::new(conn),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_name() -> String {
|
||||||
|
let n = Local::now();
|
||||||
|
format!(
|
||||||
|
"{:02}-{:02} {:02}:{:02}",
|
||||||
|
n.month(),
|
||||||
|
n.day(),
|
||||||
|
n.hour(),
|
||||||
|
n.minute()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list(&self) -> Result<Vec<GeneratedVideoRecord>, VideoDbError> {
|
||||||
|
let conn = self.conn.lock().map_err(|_| {
|
||||||
|
VideoDbError::Message("视频数据库锁异常".into())
|
||||||
|
})?;
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"SELECT id, name, file_path, avatar_id, audio_id, created_at
|
||||||
|
FROM generated_videos ORDER BY id DESC LIMIT ?1",
|
||||||
|
)?;
|
||||||
|
let rows = stmt.query_map(params![MAX_RECORDS], |row| {
|
||||||
|
Ok(GeneratedVideoRecord {
|
||||||
|
id: row.get(0)?,
|
||||||
|
name: row.get(1)?,
|
||||||
|
file_path: row.get(2)?,
|
||||||
|
avatar_id: row.get(3)?,
|
||||||
|
audio_id: row.get(4)?,
|
||||||
|
created_at: row.get(5)?,
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
let mut items = Vec::new();
|
||||||
|
for row in rows {
|
||||||
|
items.push(row?);
|
||||||
|
}
|
||||||
|
Ok(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, id: i64) -> Result<Option<GeneratedVideoRecord>, VideoDbError> {
|
||||||
|
let conn = self.conn.lock().map_err(|_| {
|
||||||
|
VideoDbError::Message("视频数据库锁异常".into())
|
||||||
|
})?;
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"SELECT id, name, file_path, avatar_id, audio_id, created_at
|
||||||
|
FROM generated_videos WHERE id = ?1",
|
||||||
|
)?;
|
||||||
|
let mut rows = stmt.query_map(params![id], |row| {
|
||||||
|
Ok(GeneratedVideoRecord {
|
||||||
|
id: row.get(0)?,
|
||||||
|
name: row.get(1)?,
|
||||||
|
file_path: row.get(2)?,
|
||||||
|
avatar_id: row.get(3)?,
|
||||||
|
audio_id: row.get(4)?,
|
||||||
|
created_at: row.get(5)?,
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
match rows.next() {
|
||||||
|
Some(row) => Ok(Some(row?)),
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
file_path: &str,
|
||||||
|
avatar_id: Option<i64>,
|
||||||
|
audio_id: Option<i64>,
|
||||||
|
) -> Result<GeneratedVideoRecord, VideoDbError> {
|
||||||
|
let file_path = file_path.trim();
|
||||||
|
if file_path.is_empty() {
|
||||||
|
return Err(VideoDbError::Message("文件路径不能为空".into()));
|
||||||
|
}
|
||||||
|
let name = name.trim();
|
||||||
|
let name = if name.is_empty() {
|
||||||
|
Self::default_name()
|
||||||
|
} else {
|
||||||
|
name.to_string()
|
||||||
|
};
|
||||||
|
let created_at = chrono::Utc::now().to_rfc3339();
|
||||||
|
let conn = self.conn.lock().map_err(|_| {
|
||||||
|
VideoDbError::Message("视频数据库锁异常".into())
|
||||||
|
})?;
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO generated_videos (name, file_path, avatar_id, audio_id, created_at)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||||
|
params![name, file_path, avatar_id, audio_id, created_at],
|
||||||
|
)?;
|
||||||
|
let id = conn.last_insert_rowid();
|
||||||
|
self.prune_excess(&conn)?;
|
||||||
|
Ok(GeneratedVideoRecord {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
file_path: file_path.to_string(),
|
||||||
|
avatar_id,
|
||||||
|
audio_id,
|
||||||
|
created_at,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prune_excess(&self, conn: &Connection) -> Result<(), VideoDbError> {
|
||||||
|
let count: i64 = conn.query_row(
|
||||||
|
"SELECT COUNT(1) FROM generated_videos",
|
||||||
|
[],
|
||||||
|
|row| row.get(0),
|
||||||
|
)?;
|
||||||
|
if count <= MAX_RECORDS {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let excess = count - MAX_RECORDS;
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM generated_videos WHERE id IN (
|
||||||
|
SELECT id FROM generated_videos ORDER BY id ASC LIMIT ?1
|
||||||
|
)",
|
||||||
|
params![excess],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete(&self, id: i64) -> Result<Option<String>, VideoDbError> {
|
||||||
|
let conn = self.conn.lock().map_err(|_| {
|
||||||
|
VideoDbError::Message("视频数据库锁异常".into())
|
||||||
|
})?;
|
||||||
|
let file_path: String = match conn.query_row(
|
||||||
|
"SELECT file_path FROM generated_videos WHERE id = ?1",
|
||||||
|
params![id],
|
||||||
|
|row| row.get(0),
|
||||||
|
) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(rusqlite::Error::QueryReturnedNoRows) => return Ok(None),
|
||||||
|
Err(e) => return Err(e.into()),
|
||||||
|
};
|
||||||
|
conn.execute("DELETE FROM generated_videos WHERE id = ?1", params![id])?;
|
||||||
|
Ok(Some(file_path))
|
||||||
|
}
|
||||||
|
}
|
||||||
84
src-tauri/src/video_store.rs
Normal file
84
src-tauri/src/video_store.rs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
//! 口播视频:SQLite 访问与 Tauri 状态
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
use crate::video_db::{VideoDb, GeneratedVideoRecord};
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct VideoStore {
|
||||||
|
inner: Arc<RwLock<Option<Arc<VideoDb>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VideoStore {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn init(&self, db_path: PathBuf) -> Result<(), String> {
|
||||||
|
let db = tokio::task::spawn_blocking(move || {
|
||||||
|
VideoDb::open(db_path).map_err(|e| e.to_string())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())??;
|
||||||
|
|
||||||
|
let count = tokio::task::spawn_blocking({
|
||||||
|
let db = Arc::new(db);
|
||||||
|
let db2 = db.clone();
|
||||||
|
move || db2.list().map(|l| (db, l.len())).map_err(|e| e.to_string())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())??;
|
||||||
|
|
||||||
|
let (db, n) = count;
|
||||||
|
*self.inner.write().await = Some(db);
|
||||||
|
log::info!(target: "video", "口播视频历史数据库已初始化,共 {n} 条记录");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn with_db<F, T>(&self, f: F) -> Result<T, String>
|
||||||
|
where
|
||||||
|
F: FnOnce(Arc<VideoDb>) -> Result<T, String> + Send + 'static,
|
||||||
|
T: Send + 'static,
|
||||||
|
{
|
||||||
|
let db = self
|
||||||
|
.inner
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.clone()
|
||||||
|
.ok_or_else(|| "视频数据库未初始化".to_string())?;
|
||||||
|
tokio::task::spawn_blocking(move || f(db))
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list(&self) -> Result<Vec<GeneratedVideoRecord>, String> {
|
||||||
|
self.with_db(|db| db.list().map_err(|e| e.to_string())).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get(&self, id: i64) -> Result<Option<GeneratedVideoRecord>, String> {
|
||||||
|
self.with_db(move |db| db.get(id).map_err(|e| e.to_string()))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn insert(
|
||||||
|
&self,
|
||||||
|
name: String,
|
||||||
|
file_path: String,
|
||||||
|
avatar_id: Option<i64>,
|
||||||
|
audio_id: Option<i64>,
|
||||||
|
) -> Result<GeneratedVideoRecord, String> {
|
||||||
|
self.with_db(move |db| {
|
||||||
|
db.insert(&name, &file_path, avatar_id, audio_id)
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete(&self, id: i64) -> Result<Option<String>, String> {
|
||||||
|
self.with_db(move |db| db.delete(id).map_err(|e| e.to_string()))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import { convertFileSrc, isTauri } from "@tauri-apps/api/core";
|
import { convertFileSrc, isTauri } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 本地视频文件转为 WebView 可播放地址
|
|
||||||
* @param {string} filePath
|
* @param {string} filePath
|
||||||
*/
|
*/
|
||||||
export function localVideoToPlayableUrl(filePath) {
|
export function localVideoToPlayableUrl(filePath) {
|
||||||
if (!isTauri()) {
|
if (!isTauri()) {
|
||||||
throw new Error("请在 Tauri 桌面端预览视频");
|
throw new Error("请在 Tauri 桌面端使用视频播放功能");
|
||||||
}
|
}
|
||||||
return convertFileSrc(filePath);
|
return convertFileSrc(filePath);
|
||||||
}
|
}
|
||||||
|
|||||||
42
src/services/videoDb.js
Normal file
42
src/services/videoDb.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { invoke, isTauri } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
|
function ensureTauri() {
|
||||||
|
if (!isTauri()) {
|
||||||
|
throw new Error("视频历史需在 Tauri 桌面端使用");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {{ id: number, name: string, filePath: string, avatarId: number | null, audioId: number | null, createdAt: string }} GeneratedVideoRecord
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @returns {Promise<GeneratedVideoRecord[]>} */
|
||||||
|
export async function listGeneratedVideos() {
|
||||||
|
ensureTauri();
|
||||||
|
return invoke("list_generated_videos");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} sourcePath
|
||||||
|
* @param {string} name
|
||||||
|
* @param {number | null} [avatarId]
|
||||||
|
* @param {number | null} [audioId]
|
||||||
|
* @returns {Promise<GeneratedVideoRecord>}
|
||||||
|
*/
|
||||||
|
export async function importGeneratedVideo(sourcePath, name, avatarId = null, audioId = null) {
|
||||||
|
ensureTauri();
|
||||||
|
return invoke("import_generated_video", {
|
||||||
|
sourcePath,
|
||||||
|
name,
|
||||||
|
avatarId: avatarId ?? null,
|
||||||
|
audioId: audioId ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} id
|
||||||
|
*/
|
||||||
|
export async function deleteGeneratedVideo(id) {
|
||||||
|
ensureTauri();
|
||||||
|
return invoke("delete_generated_video", { id });
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user