fix: double-paste bug, show actual yt-dlp errors in UI
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
# Video Clipper: Full SDD Implementation Summary
|
||||
|
||||
**Date:** 2026-09-21
|
||||
**Approach:** Subagent-Driven Development (12 tasks + final review)
|
||||
**Total commits:** 23 (0bc1f1e → e4710b4)
|
||||
**Test suite:** 31 Vitest + 28 Rust tests, all green
|
||||
|
||||
## Task Description
|
||||
|
||||
Implemented the full MVP of a macOS GUI Video Clipper app (Tauri v2 + Svelte 5) across 12 tasks, each with an implementer subagent, task review, and fix rounds as needed. The app enables users to paste a YouTube URL, preview/scrub the video, mark clip regions, and export clips via ffmpeg.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### Task 1: Project Scaffolding & App Shell
|
||||
- Tauri v2 + Svelte 5 project with Rust backend
|
||||
- Data models (`VideoMetadata`, `Clip`, `CookieSource`, `ExportConfig`, etc.)
|
||||
- Utility functions (`formatTime`, `parseTime`, `getClipColor`) with Vitest tests
|
||||
- App shell layout with 6 stub components
|
||||
- CSS theme with custom properties
|
||||
|
||||
### Task 2: Dependency Manager & Setup Wizard
|
||||
- Rust `dependency_manager` service (ffmpeg, yt-dlp, JS runtime, PO token detection)
|
||||
- Tauri commands with Channel-based install progress streaming
|
||||
- SetupWizard Svelte component with auto-continue
|
||||
- **Fix round:** stderr threading for install output, cookie flags on PO token check
|
||||
|
||||
### Task 3: Preferences Store
|
||||
- Svelte 5 rune store with `@tauri-apps/plugin-store` persistence
|
||||
- Output directory + cookie source preferences
|
||||
- PreferencesPanel component with gear toggle in toolbar
|
||||
- **Fix round:** guard saves against uninitialized store
|
||||
|
||||
### Task 4: Video Resolver, URL Input & Video Player
|
||||
- Rust `video_resolver` service (yt-dlp JSON parsing, stream URL extraction)
|
||||
- `resolve_url` Tauri command with cookie support
|
||||
- `videoSession` store (reactive session state)
|
||||
- UrlInput (paste detection, resolve flow) + VideoPlayer (HTML5 `<video>`)
|
||||
- **Fix round:** validate empty stream_url, clear stale metadata on error
|
||||
|
||||
### Task 5: Download Manager & Status Bar
|
||||
- Rust `download_manager` service with progress parsing
|
||||
- `start_download` command with Channel events
|
||||
- StatusBar showing download progress
|
||||
- Auto-download trigger after URL resolve
|
||||
- **Fix round:** redirect unused stdout to `Stdio::null()`
|
||||
|
||||
### Task 6: Timeline Core
|
||||
- Canvas-based timeline with `TimelineState`, coordinate mapping
|
||||
- Playhead rendering, time ticks, placeholder lanes
|
||||
- Mouse scrubbing, cursor-anchored zoom (wheel)
|
||||
- 12 Vitest tests for pure functions
|
||||
|
||||
### Task 7: Clip Management
|
||||
- `clips` store (Svelte 5 runes) with CRUD + in/out point marking
|
||||
- Canvas clip renderer with color-coded regions + drag handles
|
||||
- `hitTestClip` for mouse interaction
|
||||
- ClipList component (list, edit, select, delete)
|
||||
- 7 TDD tests for clip store
|
||||
|
||||
### Task 8: Transport Controls & Keyboard Shortcuts
|
||||
- TransportControls component (play/pause, I/O, skip, frame step)
|
||||
- Global keyboard handler (Space, I, O, J/K/L shuttle, arrows, comma/period)
|
||||
- Shared `playback.ts` module for DRY transport logic
|
||||
|
||||
### Task 9: Keyframe Index & Frame Navigation
|
||||
- Rust `keyframe_index` service (ffprobe parsing)
|
||||
- `extract_keyframes` Tauri command
|
||||
- Auto-fetch after download, wired to transport buttons (Shift+comma/period)
|
||||
|
||||
### Task 10: Waveform Generation & Display
|
||||
- Rust `waveform_generator` service (ffmpeg audio peak extraction)
|
||||
- Canvas waveform renderer with zoom-aware peak drawing
|
||||
- Static placeholder while generating
|
||||
- **Fix round:** clear waveform data on new download
|
||||
|
||||
### Task 11: Thumbnail Extraction & Display
|
||||
- Rust `thumbnail_extractor` service (ffmpeg frame extraction)
|
||||
- Adaptive density thumbnails on canvas timeline
|
||||
- Image caching with `convertFileSrc` (Tauri asset protocol)
|
||||
- **Fix round:** cache invalidation + temp directory cleanup
|
||||
|
||||
### Task 12: Clip Export & Export Dialog
|
||||
- Rust `clip_exporter` service (lossless/precise, individual/merged)
|
||||
- ExportDialog component (cut mode, scope, directory, progress)
|
||||
- 6 Rust tests for ffmpeg command construction
|
||||
- **Fix round:** temp dir cleanup on merge failure
|
||||
|
||||
### Final Review Fixes
|
||||
- Clear clips on new video load
|
||||
- WebM precise codec mapping (VP9/Opus)
|
||||
- Home path `~` expansion via `dirs` crate
|
||||
- computeZoom zero-range guard
|
||||
- Timeline viewport reset on media switch
|
||||
- Shuttle rate reset on new URL
|
||||
|
||||
## Follow-Up Items
|
||||
|
||||
These were identified in reviews as acceptable for MVP but should be addressed:
|
||||
|
||||
1. **Per-frame JPGs → sprite sheets** — Thumbnails use individual files instead of tiled grids; performance concern for very long videos
|
||||
2. **Stream URL expiry handling** — No auto-re-resolve when streaming URL expires during preview
|
||||
3. **Auto-export on download complete** — Spec calls for this when user is waiting in export dialog
|
||||
4. **Merged export overlap trimming** — Overlapping clip ranges not trimmed before merge
|
||||
5. **Export Selected** — ClipList only has Export All, not Export Selected
|
||||
6. **Cmd+A select all clips** — Only preventDefault, no implementation
|
||||
7. **Analysis failure UX** — Silent console.error instead of "Failed to generate" placeholder
|
||||
8. **Zoom slider** — Only wheel zoom; no slider control
|
||||
9. **Status bar ETA/export progress** — Minimal status bar info
|
||||
10. **Dead `ExportEvent::Error` / `DownloadEvent::Error` variants** — Unused enum variants
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
1. **Svelte 5 rune export limitation** — Can't export individual `$state` variables; must use a single `$state` object pattern (discovered in Task 3, applied throughout)
|
||||
2. **Pipe deadlock pattern** — Piping both stdout and stderr without reading both causes subprocess deadlock. Fixed in Tasks 2 and 5 with either dual-thread reading or `Stdio::null()`.
|
||||
3. **Stale state on session transitions** — Multiple tasks needed fixes for stale data (metadata, waveform, keyframes, clips) when loading a new URL. A centralized "new session" reset would prevent this pattern.
|
||||
4. **ffmpeg `aresample` semantics** — Sets sample rate (Hz), not output length; long videos produce large PCM buffers before bucketing.
|
||||
5. **SDD review loop value** — Per-task reviews caught real bugs (pipe deadlocks, stale state, missing cookie flags) that would have been expensive to debug later. The final whole-branch review caught cross-cutting issues (clip reset, codec mismatch) that per-task gates missed.
|
||||
@@ -33,32 +33,40 @@
|
||||
function handlePaste(e: ClipboardEvent) {
|
||||
const text = e.clipboardData?.getData('text')?.trim();
|
||||
if (text && (text.startsWith('http://') || text.startsWith('https://'))) {
|
||||
e.preventDefault();
|
||||
inputValue = text;
|
||||
setTimeout(handleSubmit, 50);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="url-input">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Paste a video URL and press Enter…"
|
||||
bind:value={inputValue}
|
||||
onkeydown={handleKeydown}
|
||||
onpaste={handlePaste}
|
||||
disabled={session.status === 'resolving'}
|
||||
/>
|
||||
{#if session.status === 'resolving'}
|
||||
<span class="spinner">⏳</span>
|
||||
{/if}
|
||||
<div class="url-input-wrapper">
|
||||
<div class="url-input">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Paste a video URL and press Enter…"
|
||||
bind:value={inputValue}
|
||||
onkeydown={handleKeydown}
|
||||
onpaste={handlePaste}
|
||||
disabled={session.status === 'resolving'}
|
||||
/>
|
||||
{#if session.status === 'resolving'}
|
||||
<span class="spinner">⏳</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if session.error}
|
||||
<span class="error" title={session.error}>⚠</span>
|
||||
<div class="error-bar" title={session.error}>⚠ {session.error}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.url-input {
|
||||
.url-input-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.url-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
@@ -69,8 +77,13 @@
|
||||
.spinner {
|
||||
animation: pulse 1s ease-in-out infinite;
|
||||
}
|
||||
.error {
|
||||
.error-bar {
|
||||
color: var(--danger);
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: help;
|
||||
}
|
||||
@keyframes pulse {
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
{:else if session.status === 'resolving'}
|
||||
<div class="placeholder">Loading video…</div>
|
||||
{:else if session.status === 'error'}
|
||||
<div class="placeholder error">Failed to load video. Check the URL and try again.</div>
|
||||
<div class="placeholder error">{session.error ?? 'Failed to load video.'}</div>
|
||||
{:else}
|
||||
<div class="placeholder">Paste a URL above to load a video</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user