diff --git a/spec/logging_feature.md b/spec/logging_feature.md new file mode 100644 index 0000000..7e5157c --- /dev/null +++ b/spec/logging_feature.md @@ -0,0 +1,58 @@ +# Feature Request: Real-time Logging & UI Refinement + +## 1. Context & Objective +Users currently experience download failures without sufficient context or error details, making troubleshooting difficult. +The objective is to introduce a dedicated **Logs System** within the application to capture, display, and persist detailed execution logs from the `yt-dlp` process. +Additionally, the application's sidebar layout requires balancing for better UX. + +## 2. Requirements + +### 2.1. Backend (Rust) - Logging Infrastructure +* **Stream Capture**: The `downloader` module must capture both `STDOUT` (progress) and `STDERR` (errors/warnings) from the `yt-dlp` process. +* **Event Emission**: Emit a new Tauri event `download-log` to the frontend in real-time. + * Payload: `{ id: String, message: String, level: 'info' | 'error' | 'debug' }` +* **Persistence (Optional but Recommended)**: Ideally, write logs to a local file (e.g., `logs/{date}.log` or `logs/{task_id}.log`) for post-mortem analysis. *For this iteration, real-time event emission is priority.* + +### 2.2. Frontend - Logs View +* **New Route**: `/logs` +* **UI Layout**: + * A dedicated **Logs Tab** in the main navigation. + * A console-like view displaying a stream of log messages. + * Filters: Filter by "Task ID" or "Level" (Error/Info). + * **Real-time**: The view must update automatically as new log events arrive. + * **Detail**: Failed downloads must show the specific error message returned by `yt-dlp` (e.g., "Sign in required", "Video unavailable"). + +### 2.3. UI/UX - Sidebar Refactoring +* **Reordering**: + * **Top Section**: Home (Downloader), History. + * **Bottom Section**: Logs, Settings. +* **Removal**: Remove the "Version Number" text from the bottom of the sidebar (it can be moved to the Settings page if needed, or just removed as requested). + +## 3. Implementation Steps + +### Step 1: Rust Backend Updates +* Modify `src-tauri/src/downloader.rs`: + * Update `download_video` to spawn the process with `Stdio::piped()` for both stdout and stderr. + * Use `tokio::select!` or separate tasks to read both streams concurrently. + * Emit `download-log` events for every line read. + * Ensure the final error message (if exit code != 0) is explicitly captured and returned/logged. + +### Step 2: Frontend Store & Logic +* Create `src/stores/logs.ts`: + * State: `logs: LogEntry[]`. + * Action: `addLog(entry)`. + * Listener: Listen for `download-log` events globally. + +### Step 3: Frontend UI +* Create `src/views/Logs.vue`: + * Display logs in a scrollable container. + * Style error logs in red, info in gray/white. +* Update `src/App.vue`: + * Add the `FileText` (or similar) icon for Logs. + * Refactor the sidebar Flexbox layout to push Logs and Settings to the bottom. + * Remove the version footer. +* Update `src/router/index.ts` to include the new route. + +## 4. Success Criteria +* When a download fails, the user can go to the "Logs" tab and see the exact error output from `yt-dlp`. +* The sidebar looks balanced with navigation items split between top and bottom. diff --git a/src-tauri/src/downloader.rs b/src-tauri/src/downloader.rs index d135011..29c92ef 100644 --- a/src-tauri/src/downloader.rs +++ b/src-tauri/src/downloader.rs @@ -46,6 +46,13 @@ pub struct ProgressEvent { pub status: String, // "downloading", "processing", "finished", "error" } +#[derive(Serialize, Clone, Debug)] +pub struct LogEvent { + pub id: String, + pub message: String, + pub level: String, // "info", "error" +} + pub async fn fetch_metadata(app: &AppHandle, url: &str, parse_mix_playlist: bool) -> Result { let ytdlp_path = ytdlp::get_ytdlp_path(app)?; @@ -148,31 +155,60 @@ pub async fn download_video( let mut child = Command::new(ytdlp_path) .args(&args) .stdout(Stdio::piped()) - .stderr(Stdio::piped()) + .stderr(Stdio::piped()) // Capture stderr for logs .spawn()?; let stdout = child.stdout.take().ok_or(anyhow!("Failed to open stdout"))?; - let mut reader = BufReader::new(stdout); - let mut line = String::new(); - - // Regex for progress: [download] 42.5% of 10.00MiB at 2.00MiB/s ETA 00:05 + let stderr = child.stderr.take().ok_or(anyhow!("Failed to open stderr"))?; + + let mut stdout_reader = BufReader::new(stdout); + let mut stderr_reader = BufReader::new(stderr); + let re = Regex::new(r"\[download\]\s+(\d+\.?\d*)%").unwrap(); - while reader.read_line(&mut line).await? > 0 { - if let Some(caps) = re.captures(&line) { - if let Some(pct_match) = caps.get(1) { - if let Ok(pct) = pct_match.as_str().parse::() { - // Emit event - app.emit("download-progress", ProgressEvent { - id: id.clone(), - progress: pct, - speed: "TODO".to_string(), // Speed parsing is a bit more complex, skipping for MVP or adding regex for it - status: "downloading".to_string(), - }).ok(); + // Loop to read both streams + loop { + let mut out_line = String::new(); + let mut err_line = String::new(); + + tokio::select! { + res = stdout_reader.read_line(&mut out_line) => { + if res.unwrap_or(0) == 0 { + break; // EOF + } + + // Log info + app.emit("download-log", LogEvent { + id: id.clone(), + message: out_line.trim().to_string(), + level: "info".to_string(), + }).ok(); + + // Parse progress + if let Some(caps) = re.captures(&out_line) { + if let Some(pct_match) = caps.get(1) { + if let Ok(pct) = pct_match.as_str().parse::() { + app.emit("download-progress", ProgressEvent { + id: id.clone(), + progress: pct, + speed: "TODO".to_string(), + status: "downloading".to_string(), + }).ok(); + } + } } } + res = stderr_reader.read_line(&mut err_line) => { + if res.unwrap_or(0) > 0 { + // Log error + app.emit("download-log", LogEvent { + id: id.clone(), + message: err_line.trim().to_string(), + level: "error".to_string(), + }).ok(); + } + } } - line.clear(); } let status = child.wait().await?; @@ -194,4 +230,4 @@ pub async fn download_video( }).ok(); Err(anyhow!("Download process failed")) } -} \ No newline at end of file +} diff --git a/src/App.vue b/src/App.vue index 04d0f4b..742ffbc 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,18 +2,21 @@ @@ -28,7 +31,8 @@ onMounted(async () => { -