chore: stage all pending work — caption styling, media server, processing modal, docs, summaries
Includes: - Extended caption styling (font, shadow, dimmed color, bg toggle) - Media server, subtitle downloader, VTT parser, processing modal - Waveform tiers, thumbnail/timeline improvements, transport controls - Hybrid download model, dependency management, clip export enhancements - 21 chat summaries, 2 implementation plans, 2 design specs Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
use crate::models::{CookieSource, VideoMetadata};
|
||||
use crate::services::{download_manager, video_resolver};
|
||||
use crate::services::{download_manager, subtitle_downloader, video_resolver};
|
||||
use serde::Serialize;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use tauri::ipc::Channel;
|
||||
@@ -23,24 +23,36 @@ pub enum DownloadEvent {
|
||||
Error { message: String },
|
||||
}
|
||||
|
||||
fn variant_dir(variant: &str) -> String {
|
||||
std::env::temp_dir()
|
||||
.join("video-clipper")
|
||||
.join(variant)
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn start_download(
|
||||
url: String,
|
||||
cookie_source: CookieSource,
|
||||
format_spec: String,
|
||||
variant: String,
|
||||
on_event: Channel<DownloadEvent>,
|
||||
) -> Result<(), String> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let temp_dir = std::env::temp_dir()
|
||||
.join("video-clipper")
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
std::fs::create_dir_all(&temp_dir)
|
||||
.map_err(|e| format!("Failed to create temp dir: {e}"))?;
|
||||
let output_dir = variant_dir(&variant);
|
||||
std::fs::create_dir_all(&output_dir)
|
||||
.map_err(|e| format!("Failed to create dir: {e}"))?;
|
||||
|
||||
eprintln!(
|
||||
"[video-clipper:download:{}] starting — format='{}' dir='{}'",
|
||||
variant, format_spec, output_dir
|
||||
);
|
||||
|
||||
let (mut child, _template) =
|
||||
download_manager::start_download(&url, &cookie_source, &temp_dir)?;
|
||||
download_manager::start_download(&url, &cookie_source, &output_dir, &format_spec)?;
|
||||
|
||||
let Some(stderr) = child.stderr.take() else {
|
||||
let Some(stdout) = child.stdout.take() else {
|
||||
let status = child
|
||||
.wait()
|
||||
.map_err(|e| format!("Download failed: {e}"))?;
|
||||
@@ -51,7 +63,7 @@ pub async fn start_download(
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let reader = BufReader::new(stderr);
|
||||
let reader = BufReader::new(stdout);
|
||||
let mut last_file_path: Option<String> = None;
|
||||
|
||||
for line in reader.lines().map_while(Result::ok) {
|
||||
@@ -59,9 +71,7 @@ pub async fn start_download(
|
||||
let _ = on_event.send(DownloadEvent::Progress { percent: pct });
|
||||
}
|
||||
|
||||
if line.contains("[download] Destination:")
|
||||
|| line.contains("[Merger] Merging formats into")
|
||||
{
|
||||
if line.contains("[download] Destination:") {
|
||||
if let Some(path) = line.split(": ").nth(1) {
|
||||
let trimmed = path.trim().trim_matches('"').to_string();
|
||||
let _ = on_event.send(DownloadEvent::FilePath {
|
||||
@@ -69,6 +79,30 @@ pub async fn start_download(
|
||||
});
|
||||
last_file_path = Some(trimmed);
|
||||
}
|
||||
} else if line.contains("[Merger] Merging formats into") {
|
||||
let trimmed = line
|
||||
.trim_start_matches("[Merger] Merging formats into ")
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.to_string();
|
||||
if !trimmed.is_empty() {
|
||||
let _ = on_event.send(DownloadEvent::FilePath {
|
||||
path: trimmed.clone(),
|
||||
});
|
||||
last_file_path = Some(trimmed);
|
||||
}
|
||||
} else if line.contains("has already been downloaded") {
|
||||
if let Some(rest) = line.strip_prefix("[download] ") {
|
||||
if let Some(path) = rest.strip_suffix(" has already been downloaded") {
|
||||
let trimmed = path.trim().to_string();
|
||||
if !trimmed.is_empty() {
|
||||
let _ = on_event.send(DownloadEvent::FilePath {
|
||||
path: trimmed.clone(),
|
||||
});
|
||||
last_file_path = Some(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +110,12 @@ pub async fn start_download(
|
||||
.wait()
|
||||
.map_err(|e| format!("Download failed: {e}"))?;
|
||||
let path = last_file_path.unwrap_or_default();
|
||||
eprintln!(
|
||||
"[video-clipper:download:{}] finished — success={}, path='{}'",
|
||||
variant,
|
||||
status.success(),
|
||||
path
|
||||
);
|
||||
let _ = on_event.send(DownloadEvent::Finished {
|
||||
success: status.success(),
|
||||
path,
|
||||
@@ -86,3 +126,67 @@ pub async fn start_download(
|
||||
.await
|
||||
.map_err(|e| format!("Task failed: {e}"))?
|
||||
}
|
||||
|
||||
/// Check if a video with the given title exists in the variant's download dir.
|
||||
#[tauri::command]
|
||||
pub async fn check_cached_download(title: String, variant: String) -> Option<String> {
|
||||
let dir = variant_dir(&variant);
|
||||
let dir_path = std::path::Path::new(&dir);
|
||||
if !dir_path.is_dir() {
|
||||
return None;
|
||||
}
|
||||
let entries = std::fs::read_dir(dir_path).ok()?;
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
|
||||
if stem == title {
|
||||
let path_str = path.to_string_lossy().to_string();
|
||||
if let Ok(meta) = std::fs::metadata(&path_str) {
|
||||
if meta.len() > 0 {
|
||||
eprintln!(
|
||||
"[video-clipper:cache:{}] found: '{}'",
|
||||
variant, path_str
|
||||
);
|
||||
return Some(path_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Download English VTT subtitles for a video URL.
|
||||
/// Returns the path to the .vtt file, or an error string.
|
||||
#[tauri::command]
|
||||
pub async fn download_subtitles(
|
||||
url: String,
|
||||
cookie_source: CookieSource,
|
||||
is_auto: bool,
|
||||
) -> Result<String, String> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let output_dir = std::env::temp_dir()
|
||||
.join("video-clipper")
|
||||
.join("subtitles")
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
eprintln!(
|
||||
"[video-clipper:subtitles] downloading (auto={}) to '{}'",
|
||||
is_auto, output_dir
|
||||
);
|
||||
|
||||
let result =
|
||||
subtitle_downloader::download_subtitles(&url, &cookie_source, &output_dir, is_auto);
|
||||
|
||||
match &result {
|
||||
Ok(path) => eprintln!("[video-clipper:subtitles] done: '{}'", path),
|
||||
Err(e) => eprintln!("[video-clipper:subtitles] FAILED: {}", e),
|
||||
}
|
||||
result
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("Task failed: {e}"))?
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user