2026-09-21 10:57:43 -04:00
|
|
|
use crate::models::{CookieSource, VideoMetadata};
|
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>
2026-09-22 10:48:16 -04:00
|
|
|
use crate::services::{download_manager, subtitle_downloader, video_resolver};
|
2026-09-21 11:00:46 -04:00
|
|
|
use serde::Serialize;
|
|
|
|
|
use std::io::{BufRead, BufReader};
|
|
|
|
|
use tauri::ipc::Channel;
|
2026-09-21 10:57:43 -04:00
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub async fn resolve_url(
|
|
|
|
|
url: String,
|
|
|
|
|
cookie_source: CookieSource,
|
|
|
|
|
) -> Result<VideoMetadata, String> {
|
|
|
|
|
tokio::task::spawn_blocking(move || video_resolver::resolve(&url, &cookie_source))
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| format!("Task failed: {e}"))?
|
|
|
|
|
}
|
2026-09-21 11:00:46 -04:00
|
|
|
|
|
|
|
|
#[derive(Clone, Serialize)]
|
|
|
|
|
#[serde(rename_all = "camelCase", tag = "event", content = "data")]
|
|
|
|
|
pub enum DownloadEvent {
|
|
|
|
|
Progress { percent: f64 },
|
|
|
|
|
FilePath { path: String },
|
|
|
|
|
Finished { success: bool, path: String },
|
2026-09-22 13:32:23 -04:00
|
|
|
#[allow(dead_code)] // Reserved for future error-channel reporting
|
2026-09-21 11:00:46 -04:00
|
|
|
Error { message: String },
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-09-22 10:48:16 -04:00
|
|
|
fn variant_dir(variant: &str) -> String {
|
|
|
|
|
std::env::temp_dir()
|
|
|
|
|
.join("video-clipper")
|
|
|
|
|
.join(variant)
|
|
|
|
|
.to_string_lossy()
|
|
|
|
|
.to_string()
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-21 11:00:46 -04:00
|
|
|
#[tauri::command]
|
|
|
|
|
pub async fn start_download(
|
|
|
|
|
url: String,
|
|
|
|
|
cookie_source: CookieSource,
|
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>
2026-09-22 10:48:16 -04:00
|
|
|
format_spec: String,
|
|
|
|
|
variant: String,
|
2026-09-21 11:00:46 -04:00
|
|
|
on_event: Channel<DownloadEvent>,
|
|
|
|
|
) -> Result<(), String> {
|
|
|
|
|
tokio::task::spawn_blocking(move || {
|
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>
2026-09-22 10:48:16 -04:00
|
|
|
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
|
|
|
|
|
);
|
2026-09-21 11:00:46 -04:00
|
|
|
|
|
|
|
|
let (mut child, _template) =
|
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>
2026-09-22 10:48:16 -04:00
|
|
|
download_manager::start_download(&url, &cookie_source, &output_dir, &format_spec)?;
|
2026-09-21 11:00:46 -04:00
|
|
|
|
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>
2026-09-22 10:48:16 -04:00
|
|
|
let Some(stdout) = child.stdout.take() else {
|
2026-09-21 11:00:46 -04:00
|
|
|
let status = child
|
|
|
|
|
.wait()
|
|
|
|
|
.map_err(|e| format!("Download failed: {e}"))?;
|
|
|
|
|
let _ = on_event.send(DownloadEvent::Finished {
|
|
|
|
|
success: status.success(),
|
|
|
|
|
path: String::new(),
|
|
|
|
|
});
|
|
|
|
|
return Ok(());
|
|
|
|
|
};
|
|
|
|
|
|
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>
2026-09-22 10:48:16 -04:00
|
|
|
let reader = BufReader::new(stdout);
|
2026-09-21 11:00:46 -04:00
|
|
|
let mut last_file_path: Option<String> = None;
|
|
|
|
|
|
|
|
|
|
for line in reader.lines().map_while(Result::ok) {
|
|
|
|
|
if let Some(pct) = download_manager::parse_progress_line(&line) {
|
|
|
|
|
let _ = on_event.send(DownloadEvent::Progress { percent: pct });
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-09-22 10:48:16 -04:00
|
|
|
if line.contains("[download] Destination:") {
|
2026-09-21 11:00:46 -04:00
|
|
|
if let Some(path) = line.split(": ").nth(1) {
|
|
|
|
|
let trimmed = path.trim().trim_matches('"').to_string();
|
|
|
|
|
let _ = on_event.send(DownloadEvent::FilePath {
|
|
|
|
|
path: trimmed.clone(),
|
|
|
|
|
});
|
|
|
|
|
last_file_path = Some(trimmed);
|
|
|
|
|
}
|
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>
2026-09-22 10:48:16 -04:00
|
|
|
} 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-21 11:00:46 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let status = child
|
|
|
|
|
.wait()
|
|
|
|
|
.map_err(|e| format!("Download failed: {e}"))?;
|
|
|
|
|
let path = last_file_path.unwrap_or_default();
|
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>
2026-09-22 10:48:16 -04:00
|
|
|
eprintln!(
|
|
|
|
|
"[video-clipper:download:{}] finished — success={}, path='{}'",
|
|
|
|
|
variant,
|
|
|
|
|
status.success(),
|
|
|
|
|
path
|
|
|
|
|
);
|
2026-09-21 11:00:46 -04:00
|
|
|
let _ = on_event.send(DownloadEvent::Finished {
|
|
|
|
|
success: status.success(),
|
|
|
|
|
path,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| format!("Task failed: {e}"))?
|
|
|
|
|
}
|
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>
2026-09-22 10:48:16 -04:00
|
|
|
|
|
|
|
|
/// 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}"))?
|
|
|
|
|
}
|