<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vallabh Sharma</title>
    <description>The latest articles on DEV Community by Vallabh Sharma (@iv_sharma).</description>
    <link>https://dev.to/iv_sharma</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1836235%2F53888693-45ca-4b9a-8dc1-20ee6c039842.png</url>
      <title>DEV Community: Vallabh Sharma</title>
      <link>https://dev.to/iv_sharma</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/iv_sharma"/>
    <language>en</language>
    <item>
      <title>I built a video clipper that downloads only the part you need</title>
      <dc:creator>Vallabh Sharma</dc:creator>
      <pubDate>Sat, 14 Nov 2026 08:10:31 +0000</pubDate>
      <link>https://dev.to/iv_sharma/i-built-a-video-clipper-that-downloads-only-the-part-you-need-2bi7</link>
      <guid>https://dev.to/iv_sharma/i-built-a-video-clipper-that-downloads-only-the-part-you-need-2bi7</guid>
      <description>&lt;p&gt;I needed twelve minutes from a ninety-minute conference talk. Every tool I tried downloaded the whole thing first, then asked me to trim it afterward. That is backwards. I already knew the range before I clicked download.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/ivsharma/clipshed" rel="noopener noreferrer"&gt;Clipshed&lt;/a&gt;: paste a YouTube, Facebook, or Instagram link, pick a start and end time, choose MP4, MP3, or both, and fetch only that section. It runs entirely on your machine, and it ships as a Windows desktop app with yt-dlp, ffmpeg, and QuickJS bundled, so there is no Python or Node install on the target PC.&lt;/p&gt;

&lt;p&gt;This post is about why it exists, how it is put together, and the decisions that turned out to matter.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvrlyouhbq244lu5ep0ct.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvrlyouhbq244lu5ep0ct.png" alt="Clipshed range selection" width="800" height="1186"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;Most downloaders treat the time range as an afterthought: download everything, open the file in an editor, scrub, cut, export. That workflow is fine if you want the whole video. It is wasteful if you want a clip.&lt;/p&gt;

&lt;p&gt;The range is not something you discover after downloading. It is an input, like the URL. If yt-dlp can be told to fetch &lt;code&gt;00:15:00&lt;/code&gt; through &lt;code&gt;00:20:00&lt;/code&gt;, then the full download was never necessary.&lt;/p&gt;

&lt;p&gt;Two smaller frustrations pushed me in the same direction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generic yt-dlp GUIs hide the interesting switches. Fast cut versus frame-accurate cut is a real quality and speed trade-off, and it deserves a button.&lt;/li&gt;
&lt;li&gt;Download tools see your URLs, and often your cookies. I did not want an account, a cloud queue, or a telemetry pipeline in the middle of that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Local-first was the obvious constraint. Everything else followed from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Range-first:&lt;/strong&gt; full video or an exact start/end range, with a draggable timeline and chapter markers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MP4, MP3, or both:&lt;/strong&gt; "Both" downloads the section once and extracts the MP3 locally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast or exact cuts:&lt;/strong&gt; keyframe-aligned stream copy, or a frame-accurate re-encode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real job handling:&lt;/strong&gt; queue, live progress over SSE, cancellation, history, and cleanup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local only:&lt;/strong&gt; the server binds to &lt;code&gt;127.0.0.1&lt;/code&gt;; no account, no uploads, no telemetry.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architecture: one server, two shells
&lt;/h2&gt;

&lt;p&gt;The repository is four pieces:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shared/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Types, time parsing, platform URL rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;server/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Express API, job manager, yt-dlp/ffmpeg orchestration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;web/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;React + Vite UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;desktop/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Electron shell, packaging, Windows integration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important part is that the desktop app is &lt;strong&gt;not&lt;/strong&gt; a separate implementation. Electron starts the same Express server that &lt;code&gt;npm start&lt;/code&gt; runs, then points a window at it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// desktop/main.cjs&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;startLocalServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serverBundle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;startLocalServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"127.0.0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Port &lt;code&gt;0&lt;/code&gt; asks the OS for a free loopback port, so the app never collides with a dev server or another instance. Electron reads the assigned port and loads &lt;code&gt;http://127.0.0.1:&amp;lt;port&amp;gt;&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────────────────┐
│ Electron main                                        │
│  single instance · window · downloads · notifications│
│         │ starts on 127.0.0.1:&amp;lt;random&amp;gt;               │
│         ▼                                            │
│  Express server (one esbuild bundle)                 │
│   /api/probe   /api/jobs   /api/settings   SSE       │
│         │ spawns                                     │
│         ├── yt-dlp.exe ──► ffmpeg.exe                │
│         └── ffprobe.exe                              │
│         │ serves                                     │
│         ▼                                            │
│  React UI in a sandboxed BrowserWindow               │
└──────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server is bundled to a single CommonJS file (about 1.9 MB) with esbuild. The renderer runs with &lt;code&gt;contextIsolation: true&lt;/code&gt;, &lt;code&gt;nodeIntegration: false&lt;/code&gt;, and &lt;code&gt;sandbox: true&lt;/code&gt;; it is just a browser talking to loopback HTTP.&lt;/p&gt;

&lt;p&gt;This buys a lot for very little: the browser workflow still works, the API can be tested without Electron, and the UI iterates with Vite's dev server. The desktop shell only owns things a browser cannot do: the window, the single-instance lock, download handling, notifications, and lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Engineering decisions that mattered
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The range is a yt-dlp argument, not a post-processing step
&lt;/h3&gt;

&lt;p&gt;This is the whole product in a few lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;range&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;formatSectionTimestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;formatSectionTimestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;range&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"--download-sections"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sb"&gt;`*${start}-${end}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;precise&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;range&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s2"&gt;"mp3"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"--force-keyframes-at-cuts"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--download-sections&lt;/code&gt; hands the range to yt-dlp's ffmpeg integration, which seeks into the stream and pulls the fragments it needs instead of the entire file. For a one-hour video, a two-minute clip is roughly two minutes of transfer, not sixty.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;precise&lt;/code&gt; flag is the interesting trade-off:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fast cut&lt;/strong&gt; (default) uses stream copy. It is fast and lossless, but the start snaps to the nearest keyframe, so you may get a few extra seconds at the beginning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exact cut&lt;/strong&gt; adds &lt;code&gt;--force-keyframes-at-cuts&lt;/code&gt;, which re-encodes around the cut points. Frame-accurate, slower, especially at 4K.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MP3 is always re-encoded, so its cuts are effectively sample-accurate either way.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. "Both" means one download, two outputs
&lt;/h3&gt;

&lt;p&gt;The naive implementation of MP4 + MP3 is two jobs: download the video, download the audio again, convert. That doubles the network traffic for the same range.&lt;/p&gt;

&lt;p&gt;Instead, "Both" downloads the MP4 section once, then extracts the MP3 from that file with ffmpeg:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;extractMp3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mp3Temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bitrate&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;192&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activeChild&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;moveFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mp3Temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sb"&gt;`${task.base}.mp3`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The extracted MP3 is probed with ffprobe for duration and size, and both outputs are exposed as separate download buttons. One fetch, two files, and the audio is derived from exactly the same section.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Machine-readable progress, not log scraping
&lt;/h3&gt;

&lt;p&gt;yt-dlp's default output is human-readable and changes. Parsing &lt;code&gt;[download]  42.3% of ...&lt;/code&gt; works until it doesn't. A progress template is far more stable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;PROGRESS_TEMPLATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="s2"&gt;"download:PROGRESS|%(progress.status)s|%(progress.downloaded_bytes)s|"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="s2"&gt;"%(progress.total_bytes)s|%(progress.total_bytes_estimate)s|"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="s2"&gt;"%(progress.speed)s|%(progress.eta)s|%(info.format_id)s|"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="s2"&gt;"%(info.vcodec)s|%(info.acodec)s"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That line arrives on stdout and is split on &lt;code&gt;|&lt;/code&gt;. ffmpeg, meanwhile, is told to emit &lt;code&gt;-progress pipe:2&lt;/code&gt;, which writes &lt;code&gt;out_time_us&lt;/code&gt;, &lt;code&gt;speed&lt;/code&gt;, and &lt;code&gt;size&lt;/code&gt; to stderr. The tracker merges both sources, throttles updates to 250 ms, and pushes them to the UI over SSE.&lt;/p&gt;

&lt;p&gt;One detail that cost me an afternoon: ffmpeg writes progress with carriage returns, so a normal line-based reader misses updates. The line splitter splits on &lt;code&gt;\r\n&lt;/code&gt;, &lt;code&gt;\n&lt;/code&gt;, &lt;strong&gt;and&lt;/strong&gt; &lt;code&gt;\r&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;\&lt;span class="n"&gt;r&lt;/span&gt;\&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;\&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;\&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without that, the progress bar freezes during the range download and then jumps to 100%.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Cancellation is a process-tree problem
&lt;/h3&gt;

&lt;p&gt;yt-dlp spawns ffmpeg for merging, converting, and section cuts. Killing the parent leaves children behind, still writing files.&lt;/p&gt;

&lt;p&gt;On Windows the reliable way to kill the whole tree is &lt;code&gt;taskkill /T /F&lt;/code&gt;; on POSIX, the child is started in its own process group and the group is killed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s2"&gt;"win32"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"taskkill"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/pid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"/T"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"/F"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;windowsHide&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;stdio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"ignore"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"SIGKILL"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cancellation is also a state machine, not a single kill. The manager marks the job, freezes progress, kills the active child (which may be yt-dlp &lt;em&gt;or&lt;/em&gt; ffmpeg), removes temp files, and only then reports &lt;code&gt;cancelled&lt;/code&gt;. Otherwise a late &lt;code&gt;close&lt;/code&gt; event would mark the job failed after the user already cancelled it.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Some binaries are read-only; one must be writable
&lt;/h3&gt;

&lt;p&gt;Bundling ffmpeg, ffprobe, and QuickJS is straightforward: they never change, so they live in Electron's resources directory and run from there.&lt;/p&gt;

&lt;p&gt;yt-dlp is different. Extractors break when sites change, so the app needs &lt;code&gt;yt-dlp -U&lt;/code&gt; to work. The install directory may be read-only, especially under &lt;code&gt;Program Files&lt;/code&gt;, and tools that self-update inside an asar archive are asking for trouble.&lt;/p&gt;

&lt;p&gt;The split is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bundled, read-only:&lt;/strong&gt; &lt;code&gt;ffmpeg.exe&lt;/code&gt;, &lt;code&gt;ffprobe.exe&lt;/code&gt;, &lt;code&gt;qjs.exe&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staged, writable:&lt;/strong&gt; &lt;code&gt;yt-dlp.exe&lt;/code&gt; is copied once to &lt;code&gt;%LOCALAPPDATA%\Clipshed\bin&lt;/code&gt; and updated in place.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ytdlpSource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bundledBinDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"yt-dlp.exe"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ytdlpDestination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;binDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"yt-dlp.exe"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ytdlpDestination&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ytdlpSource&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;fsp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copyFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ytdlpSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ytdlpDestination&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one decision makes in-app yt-dlp updates work in a packaged build, which is the difference between an app that works for a month and one that works next year.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. yt-dlp needs a JavaScript runtime, and Electron is not one
&lt;/h3&gt;

&lt;p&gt;YouTube's extraction path needs a JavaScript engine for signature challenges. Shipping Node would be absurd inside a desktop app, so the build bundles QuickJS-ng and points yt-dlp at it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qjsPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"--js-runtimes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sb"&gt;`quickjs:${config.qjsPath}`&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"--js-runtimes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In development it falls back to Node. In the packaged app there is no Node on the PATH, so the bundled &lt;code&gt;qjs.exe&lt;/code&gt; is what keeps extraction working.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Local-only changes the security checklist
&lt;/h3&gt;

&lt;p&gt;A loopback server is not a public API, but it still handles URLs, cookies, and filesystem paths, so a few things are deliberate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bind to &lt;code&gt;127.0.0.1&lt;/code&gt;, never &lt;code&gt;0.0.0.0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Renderer is sandboxed with no Node integration, and navigation to other origins is blocked and handed to the system browser.&lt;/li&gt;
&lt;li&gt;Job IDs are validated as UUIDs before they are used in any filesystem path, and cleanup only removes UUID-named directories under trusted roots.&lt;/li&gt;
&lt;li&gt;Time input is capped at 64 characters before parsing, which kills a potential ReDoS.&lt;/li&gt;
&lt;li&gt;Cookies are never returned by the API and never written to job history.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also dismissed two "missing rate limiting" scanner findings. For a single-user app bound to loopback, throttling would only interfere with large downloads and SSE while adding no meaningful protection. Not every scanner finding deserves code.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to use it
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Run it from source ⭐
&lt;/h3&gt;

&lt;p&gt;Requirements: Windows 10/11 and Node.js 22+.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;npm&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt;
&lt;span class="n"&gt;npm&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="n"&gt;setup&lt;/span&gt;
&lt;span class="n"&gt;npm&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="n"&gt;dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://127.0.0.1:5173&lt;/code&gt;. &lt;code&gt;npm run setup&lt;/code&gt; fetches yt-dlp, static ffmpeg/ffprobe builds, and QuickJS into &lt;code&gt;bin/&lt;/code&gt;. For a production-style local run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;npm&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;
&lt;span class="n"&gt;npm&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Or try the desktop app (pre-release)
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://github.com/ivsharma/clipshed/releases" rel="noopener noreferrer"&gt;https://github.com/ivsharma/clipshed/releases&lt;/a&gt; &lt;/p&gt;

&lt;h3&gt;
  
  
  The workflow
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Paste a video link. Clipshed probes it and shows the title, duration, chapters, and available qualities.&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;MP4&lt;/strong&gt;, &lt;strong&gt;MP3&lt;/strong&gt;, or &lt;strong&gt;Both&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Leave &lt;strong&gt;Full video&lt;/strong&gt; on, or switch to a custom range and drag the handles (or type &lt;code&gt;12:30&lt;/code&gt; / &lt;code&gt;01:02:03&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Pick &lt;strong&gt;Fast cut&lt;/strong&gt; unless you need frame accuracy; then enable &lt;strong&gt;Exact cut&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Download. When the job finishes, save the file from the job panel.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A few practical notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Both&lt;/strong&gt; is the bandwidth-efficient choice when you want audio and video.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast cut&lt;/strong&gt; may include a few seconds before your start time. That is the keyframe, not a bug.&lt;/li&gt;
&lt;li&gt;Facebook and Instagram are experimental and often need cookies from a signed-in browser. YouTube is the stable path.&lt;/li&gt;
&lt;li&gt;Playlists are intentionally unsupported. One video, one range.&lt;/li&gt;
&lt;li&gt;Download only content you have the right to use, and follow each site's terms.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I would change
&lt;/h2&gt;

&lt;p&gt;Electron is heavy. The portable build is about 217 MB, and most of that is Chromium. A Tauri build would be smaller, but it would also mean rebuilding the server in Rust and giving up the "same Node server in dev and production" property that made the whole thing quick to build.&lt;/p&gt;

&lt;p&gt;I would also reconsider bundling ffprobe: it only measures finished files, and dropping it would remove a 162 MB binary at the cost of independent output verification&lt;/p&gt;

&lt;p&gt;The range-first approach is also a bet on yt-dlp's extractors. When a site changes, the app changes with it, which is why the managed, updatable yt-dlp copy matters more than any UI detail.&lt;/p&gt;

&lt;p&gt;The trade-off I am happy with: a boring Express server, a plain React UI, and two well-understood command-line tools, packaged so none of them need to be installed separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/ivsharma/clipshed" rel="noopener noreferrer"&gt;github.com/ivsharma/clipshed&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Releases: &lt;a href="https://github.com/ivsharma/clipshed/releases" rel="noopener noreferrer"&gt;github.com/ivsharma/clipshed/releases&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try it, I would like to know which site breaks first.&lt;br&gt;
Thanks !! Let me know your thoughts&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>downloader</category>
      <category>electron</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
