<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Golang on Code Genos</title>
    <link>https://codegenos.github.io/tags/golang/</link>
    <description>Recent content in Golang on Code Genos</description>
    <image>
      <title>Code Genos</title>
      <url>https://codegenos.github.io/%3Clink%20or%20path%20of%20image%20for%20opengraph,%20twitter-cards%3E</url>
      <link>https://codegenos.github.io/%3Clink%20or%20path%20of%20image%20for%20opengraph,%20twitter-cards%3E</link>
    </image>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Sun, 17 May 2026 20:00:00 +0300</lastBuildDate><atom:link href="https://codegenos.github.io/tags/golang/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>yt-dlp-console: An Interactive CLI So You Never Memorize Flags Again</title>
      <link>https://codegenos.github.io/posts/yt-dlp-console-interactive-cli-video-downloader/</link>
      <pubDate>Sun, 17 May 2026 20:00:00 +0300</pubDate>
      
      <guid>https://codegenos.github.io/posts/yt-dlp-console-interactive-cli-video-downloader/</guid>
      <description>yt-dlp-console is an interactive terminal wrapper for yt-dlp that guides you through video downloads step-by-step — no flags or format codes to memorize.</description>
      <content:encoded><![CDATA[<p>I use <a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a> whenever I need to download a video. It&rsquo;s great — but every time, I end up Googling the same format selector:</p>
<pre tabindex="0"><code>yt-dlp -f &#34;bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best&#34; &lt;url&gt;
</code></pre><p>yt-dlp is powerful enough that occasional users like me never fully internalize its interface. So I built <strong><a href="https://github.com/QuickOrBeDead/yt-dlp-console">yt-dlp-console</a></strong> — an interactive wrapper that guides you through the download process without needing to remember a single flag.</p>
<h2 id="introducing-yt-dlp-console">Introducing yt-dlp-console</h2>
<p><a href="https://github.com/QuickOrBeDead/yt-dlp-console">yt-dlp-console</a> is a thin interactive wrapper around yt-dlp. It doesn&rsquo;t replace yt-dlp — yt-dlp still does all the actual work — it just replaces the mental overhead of constructing the right command.</p>
<p>You run it, and it walks you through everything:</p>
<ol>
<li>Enter the video URL</li>
<li>Choose how to authenticate (or skip it)</li>
<li>Pick your video format from a list of what&rsquo;s actually available</li>
<li>Pick an audio format if the video doesn&rsquo;t include audio</li>
<li>Watch the real-time download progress</li>
</ol>
<p>No flags. No format codes. No documentation tab.</p>
<h2 id="yt-dlp-console-features">yt-dlp-console Features</h2>
<ul>
<li><strong>Interactive step-by-step flow</strong> — each decision is a prompted choice, not a memorized flag</li>
<li><strong>Live format listing</strong> — queries yt-dlp for available formats and presents them as a selectable list, so you know exactly what you&rsquo;re picking</li>
<li><strong>Authentication support</strong> — handles unauthenticated, password-only, and username + password flows</li>
<li><strong>Concurrent fragment downloads</strong> — the <code>-N</code> flag (which controls download speed for fragmented streams) is exposed as a simple slider in the config</li>
<li><strong>Real-time progress</strong> — live output from yt-dlp piped directly to the terminal</li>
<li><strong>Persistent configuration</strong> — remembers your yt-dlp binary path and preferred concurrent fragments between sessions</li>
</ul>
<h2 id="installation">Installation</h2>
<h3 id="prerequisites">Prerequisites</h3>
<ul>
<li><a href="https://go.dev/">Go</a> 1.26.2 or later</li>
<li><a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a> installed and available in your <code>PATH</code></li>
</ul>
<h3 id="option-1--go-install-recommended">Option 1 — go install (recommended)</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">go install github.com/QuickOrBeDead/yt-dlp-console@latest
</span></span></code></pre></div><p>This compiles and installs the binary directly into your <code>$GOPATH/bin</code>.</p>
<h3 id="option-2--clone-and-build">Option 2 — Clone and build</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone https://github.com/QuickOrBeDead/yt-dlp-console.git
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> yt-dlp-console
</span></span><span class="line"><span class="cl">go build -o yt-dlp-console .
</span></span></code></pre></div><h3 id="option-3--pre-built-binary">Option 3 — Pre-built binary</h3>
<p>Pre-built binaries for Linux, macOS, and Windows are available on the <a href="https://github.com/QuickOrBeDead/yt-dlp-console/releases">GitHub Releases page</a>. Download the archive for your platform, extract it, and place the binary somewhere on your <code>PATH</code>.</p>
<h2 id="how-to-use-yt-dlp-console">How to Use yt-dlp-console</h2>
<h3 id="downloading-a-video">Downloading a video</h3>
<p>Just run the tool without any arguments:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">yt-dlp-console
</span></span></code></pre></div><p>It takes over from there. Here&rsquo;s what the interactive flow looks like:</p>
<figure class="align-center ">
    <img loading="lazy" src="https://raw.githubusercontent.com/QuickOrBeDead/yt-dlp-console/main/demo.gif#center"
         alt="yt-dlp-console interactive demo"/> <figcaption>
            <p>The full interactive download flow in action</p>
        </figcaption>
</figure>

<p>The prompts walk you through each step in order:</p>
<ol>
<li><strong>URL</strong> — paste in the video link</li>
<li><strong>Authentication</strong> — choose <em>None</em>, <em>Password</em>, or <em>Username + Password</em>; if you pick an auth method, you&rsquo;ll be prompted for the credentials</li>
<li><strong>Video format</strong> — yt-dlp queries the URL for available formats and presents them as a navigable list; you arrow through and pick one</li>
<li><strong>Audio format</strong> — if your chosen video format doesn&rsquo;t include an audio stream, you&rsquo;ll get a second list to select an audio format to merge in</li>
<li><strong>Download</strong> — yt-dlp runs with the constructed arguments and streams its progress output directly to your terminal</li>
</ol>
<h3 id="configuring-settings">Configuring settings</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">yt-dlp-console config
</span></span></code></pre></div><p>This opens an interactive config screen where you can set:</p>
<ul>
<li><strong>yt-dlp command path</strong> — useful if yt-dlp isn&rsquo;t on your <code>PATH</code> or you have multiple versions (defaults to <code>yt-dlp</code>)</li>
<li><strong>Concurrent fragments</strong> (<code>-N</code>) — controls how many fragments are downloaded in parallel for fragmented streams; range is 1–32 (defaults to 4)</li>
</ul>
<p>Settings are saved to:</p>
<ul>
<li><strong>Linux / macOS</strong>: <code>~/.config/yt-dlp-console/config.json</code></li>
<li><strong>Windows</strong>: <code>%APPDATA%\yt-dlp-console\config.json</code></li>
</ul>
<h2 id="under-the-hood">Under the Hood</h2>
<p>The project is written in Go and leans on a few libraries from <a href="https://charm.sh/">Charm</a>. Here&rsquo;s how the pieces fit together.</p>
<h3 id="code-structure">Code Structure</h3>
<p>The project is split into two top-level packages:</p>
<pre tabindex="0"><code>cmd/
  root.go       — root Cobra command + download flow
  config.go     — config subcommand
  forms.go      — Huh FormProvider interface and implementations
  update.go     — auto-update command
internal/
  appconfig/    — config struct, load/save as JSON
  console/      — styled terminal output (Error, Success, Info, Muted…)
  ytdlp/        — yt-dlp client, subprocess executor, argument builder
main.go
</code></pre><p>The <code>cmd</code> package owns the user-facing commands and form interactions. The <code>internal/ytdlp</code> package handles everything yt-dlp related — building arguments, invoking the process, and piping its output back to the terminal.</p>
<h3 id="cobra--command-structure"><a href="https://cobra.dev">Cobra</a> — Command Structure</h3>
<p><a href="https://github.com/spf13/cobra">Cobra</a> is the de facto standard for Go CLIs — the same framework behind <code>kubectl</code>, Docker CLI, GitHub CLI, and Hugo. In yt-dlp-console, the root command runs the download flow and subcommands (<code>config</code>, <code>update</code>) are registered in their respective <code>init()</code> functions:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">rootCmd</span> <span class="p">=</span> <span class="o">&amp;</span><span class="nx">cobra</span><span class="p">.</span><span class="nx">Command</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">Use</span><span class="p">:</span>   <span class="s">&#34;yt-dlp-console&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">Short</span><span class="p">:</span> <span class="s">&#34;Interactive CLI for downloading videos using yt-dlp&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">Run</span><span class="p">:</span> <span class="kd">func</span><span class="p">(</span><span class="nx">cmd</span> <span class="o">*</span><span class="nx">cobra</span><span class="p">.</span><span class="nx">Command</span><span class="p">,</span> <span class="nx">args</span> <span class="p">[]</span><span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">config</span> <span class="o">:=</span> <span class="nx">appconfig</span><span class="p">.</span><span class="nf">Get</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="nx">client</span> <span class="o">:=</span> <span class="nx">ytdlp</span><span class="p">.</span><span class="nf">NewYtDlpClient</span><span class="p">(</span><span class="nf">newYtdlpExecutor</span><span class="p">(</span><span class="nx">config</span><span class="p">),</span> <span class="nx">config</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="nx">ctx</span><span class="p">,</span> <span class="nx">cancel</span> <span class="o">:=</span> <span class="nx">context</span><span class="p">.</span><span class="nf">WithCancel</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nf">Background</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">        <span class="k">defer</span> <span class="nf">cancel</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nf">runDownloadFlow</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">client</span><span class="p">,</span> <span class="nx">defaultForms</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nx">console</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="s">&#34;%v&#34;</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="nx">console</span><span class="p">.</span><span class="nf">Success</span><span class="p">(</span><span class="s">&#34;Download complete!&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">init</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">rootCmd</span><span class="p">.</span><span class="nf">AddCommand</span><span class="p">(</span><span class="nx">configCmd</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nx">rootCmd</span><span class="p">.</span><span class="nf">AddCommand</span><span class="p">(</span><span class="nx">updateCmd</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Running <code>yt-dlp-console</code> with no arguments goes straight into <code>runDownloadFlow</code>. Subcommands like <code>yt-dlp-console config</code> are handled separately, keeping each concern cleanly isolated.</p>
<h3 id="huh--interactive-forms"><a href="https://github.com/charmbracelet/huh">Huh</a> — Interactive Forms</h3>
<p>All the interactive prompts live behind a <code>FormProvider</code> interface in <code>cmd/forms.go</code>. This makes it straightforward to swap in a fake implementation for tests:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">type</span> <span class="nx">FormProvider</span> <span class="kd">interface</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">Input</span><span class="p">(</span><span class="nx">title</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">validate</span> <span class="kd">func</span><span class="p">(</span><span class="kt">string</span><span class="p">)</span> <span class="kt">error</span><span class="p">)</span> <span class="p">(</span><span class="kt">string</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nf">InputPassword</span><span class="p">(</span><span class="nx">title</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="kt">string</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nf">Select</span><span class="p">(</span><span class="nx">title</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">options</span> <span class="p">[]</span><span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="kt">string</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nf">Confirm</span><span class="p">(</span><span class="nx">title</span><span class="p">,</span> <span class="nx">description</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="kt">bool</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>The real implementation wraps each Huh field type. <code>Input</code> accepts an optional validation function that Huh calls on every keystroke:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">RealFormProvider</span><span class="p">)</span> <span class="nf">Input</span><span class="p">(</span><span class="nx">title</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">validate</span> <span class="kd">func</span><span class="p">(</span><span class="kt">string</span><span class="p">)</span> <span class="kt">error</span><span class="p">)</span> <span class="p">(</span><span class="kt">string</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="nx">val</span> <span class="kt">string</span>
</span></span><span class="line"><span class="cl">    <span class="nx">f</span> <span class="o">:=</span> <span class="nx">huh</span><span class="p">.</span><span class="nf">NewInput</span><span class="p">().</span><span class="nf">Title</span><span class="p">(</span><span class="nx">title</span><span class="p">).</span><span class="nf">Value</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">val</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nx">validate</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">f</span> <span class="p">=</span> <span class="nx">f</span><span class="p">.</span><span class="nf">Validate</span><span class="p">(</span><span class="nx">validate</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">val</span><span class="p">,</span> <span class="nf">runHuh</span><span class="p">(</span><span class="nx">f</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Password fields use <code>EchoModePassword</code> so the input is hidden:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">RealFormProvider</span><span class="p">)</span> <span class="nf">InputPassword</span><span class="p">(</span><span class="nx">title</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="kt">string</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="nx">val</span> <span class="kt">string</span>
</span></span><span class="line"><span class="cl">    <span class="nx">err</span> <span class="o">:=</span> <span class="nf">runHuh</span><span class="p">(</span><span class="nx">huh</span><span class="p">.</span><span class="nf">NewInput</span><span class="p">().</span>
</span></span><span class="line"><span class="cl">        <span class="nf">Title</span><span class="p">(</span><span class="nx">title</span><span class="p">).</span>
</span></span><span class="line"><span class="cl">        <span class="nf">EchoMode</span><span class="p">(</span><span class="nx">huh</span><span class="p">.</span><span class="nx">EchoModePassword</span><span class="p">).</span>
</span></span><span class="line"><span class="cl">        <span class="nf">Value</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">val</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">val</span><span class="p">,</span> <span class="nx">err</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><code>Select</code> builds its options dynamically from a <code>[]string</code> — in practice this is the list of video or audio formats returned by yt-dlp:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">RealFormProvider</span><span class="p">)</span> <span class="nf">Select</span><span class="p">(</span><span class="nx">title</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">options</span> <span class="p">[]</span><span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="kt">string</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="nx">val</span> <span class="kt">string</span>
</span></span><span class="line"><span class="cl">    <span class="nx">err</span> <span class="o">:=</span> <span class="nf">runHuh</span><span class="p">(</span><span class="nx">huh</span><span class="p">.</span><span class="nx">NewSelect</span><span class="p">[</span><span class="kt">string</span><span class="p">]().</span>
</span></span><span class="line"><span class="cl">        <span class="nf">Title</span><span class="p">(</span><span class="nx">title</span><span class="p">).</span>
</span></span><span class="line"><span class="cl">        <span class="nf">Options</span><span class="p">(</span><span class="nx">huh</span><span class="p">.</span><span class="nf">NewOptions</span><span class="p">(</span><span class="nx">options</span><span class="o">...</span><span class="p">)</span><span class="o">...</span><span class="p">).</span>
</span></span><span class="line"><span class="cl">        <span class="nf">Value</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">val</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">val</span><span class="p">,</span> <span class="nx">err</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><code>Confirm</code> is used for yes/no decisions with custom labels:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">RealFormProvider</span><span class="p">)</span> <span class="nf">Confirm</span><span class="p">(</span><span class="nx">title</span><span class="p">,</span> <span class="nx">description</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="kt">bool</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="nx">val</span> <span class="kt">bool</span>
</span></span><span class="line"><span class="cl">    <span class="nx">err</span> <span class="o">:=</span> <span class="nx">huh</span><span class="p">.</span><span class="nf">NewConfirm</span><span class="p">().</span>
</span></span><span class="line"><span class="cl">        <span class="nf">Title</span><span class="p">(</span><span class="nx">title</span><span class="p">).</span>
</span></span><span class="line"><span class="cl">        <span class="nf">Description</span><span class="p">(</span><span class="nx">description</span><span class="p">).</span>
</span></span><span class="line"><span class="cl">        <span class="nf">Affirmative</span><span class="p">(</span><span class="s">&#34;Yes&#34;</span><span class="p">).</span>
</span></span><span class="line"><span class="cl">        <span class="nf">Negative</span><span class="p">(</span><span class="s">&#34;No&#34;</span><span class="p">).</span>
</span></span><span class="line"><span class="cl">        <span class="nf">Value</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">val</span><span class="p">).</span>
</span></span><span class="line"><span class="cl">        <span class="nf">Run</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">val</span><span class="p">,</span> <span class="nx">err</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>One small but important detail: after each Huh form completes it leaves the cursor on the prompt line in linux. A <code>\r\x1b[K</code> escape sequence clears that line before the next output is printed:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">runHuh</span><span class="p">(</span><span class="nx">f</span> <span class="kd">interface</span><span class="p">{</span> <span class="nf">Run</span><span class="p">()</span> <span class="kt">error</span> <span class="p">})</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">err</span> <span class="o">:=</span> <span class="nx">f</span><span class="p">.</span><span class="nf">Run</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="nx">fmt</span><span class="p">.</span><span class="nf">Print</span><span class="p">(</span><span class="s">&#34;\r\x1b[K&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>In the actual download flow, validation is attached to the URL input to catch empty values and malformed URLs before yt-dlp is ever called:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">videoUrl</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">forms</span><span class="p">.</span><span class="nf">Input</span><span class="p">(</span><span class="s">&#34;Video Url&#34;</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">s</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="nx">strings</span><span class="p">.</span><span class="nf">TrimSpace</span><span class="p">(</span><span class="nx">s</span><span class="p">))</span> <span class="o">==</span> <span class="mi">0</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;video url is required&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">!</span><span class="nf">isValidURL</span><span class="p">(</span><span class="nx">s</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;video url should be valid&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span></code></pre></div><h3 id="yt-dlp-integration--subprocess-and-streaming">yt-dlp Integration — Subprocess and Streaming</h3>
<p>The <code>internal/ytdlp</code> package handles yt-dlp in two distinct modes depending on what&rsquo;s needed.</p>
<p><strong>Format listing</strong> uses buffered output. yt-dlp&rsquo;s <code>-J</code> flag dumps all video metadata as JSON, which is captured into a <code>bytes.Buffer</code> and unmarshalled into a <code>VideoData</code> struct. While the process runs, Huh&rsquo;s spinner component wraps the wait:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="nx">YtdlpExecutorReal</span><span class="p">)</span> <span class="nf">Execute</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">cmd</span> <span class="o">*</span><span class="nx">YtDlpCommandArgs</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">cmdDesc</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">stdout</span> <span class="o">*</span><span class="nx">bytes</span><span class="p">.</span><span class="nx">Buffer</span><span class="p">,</span> <span class="nx">stderr</span> <span class="o">*</span><span class="nx">bytes</span><span class="p">.</span><span class="nx">Buffer</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nx">execCmd</span> <span class="o">:=</span> <span class="nx">exec</span><span class="p">.</span><span class="nf">CommandContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">r</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">YtDlpCommand</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">.</span><span class="nf">BuildArgs</span><span class="p">()</span><span class="o">...</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nx">execCmd</span><span class="p">.</span><span class="nx">Stdout</span> <span class="p">=</span> <span class="nx">stdout</span>
</span></span><span class="line"><span class="cl">    <span class="nx">execCmd</span><span class="p">.</span><span class="nx">Stderr</span> <span class="p">=</span> <span class="nx">stderr</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">spinner</span><span class="p">.</span><span class="nf">New</span><span class="p">().</span>
</span></span><span class="line"><span class="cl">        <span class="nf">Title</span><span class="p">(</span><span class="nx">cmdDesc</span><span class="p">).</span>
</span></span><span class="line"><span class="cl">        <span class="nf">ActionWithErr</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="nx">execCmd</span><span class="p">.</span><span class="nf">Run</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="p">}).</span>
</span></span><span class="line"><span class="cl">        <span class="nf">Run</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><strong>Download streaming</strong> works differently — buffering would mean the user sees nothing until the download finishes. Instead, <code>StdoutPipe</code> and <code>StderrPipe</code> are used to get live readers, and the process is started with <code>Start()</code> rather than <code>Run()</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="nx">YtdlpExecutorReal</span><span class="p">)</span> <span class="nf">ExecuteWithStreams</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">cmd</span> <span class="o">*</span><span class="nx">YtDlpCommandArgs</span><span class="p">)</span> <span class="p">(</span><span class="nx">io</span><span class="p">.</span><span class="nx">Reader</span><span class="p">,</span> <span class="nx">io</span><span class="p">.</span><span class="nx">Reader</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nx">execCmd</span> <span class="o">:=</span> <span class="nx">exec</span><span class="p">.</span><span class="nf">CommandContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">r</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">YtDlpCommand</span><span class="p">,</span> <span class="nx">cmd</span><span class="p">.</span><span class="nf">BuildArgs</span><span class="p">()</span><span class="o">...</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nx">stdout</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">execCmd</span><span class="p">.</span><span class="nf">StdoutPipe</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="nx">stderr</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">execCmd</span><span class="p">.</span><span class="nf">StderrPipe</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="nx">err</span> <span class="o">:=</span> <span class="nx">execCmd</span><span class="p">.</span><span class="nf">Start</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">stdout</span><span class="p">,</span> <span class="nx">stderr</span><span class="p">,</span> <span class="nx">err</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Back in the client, stderr is consumed in a goroutine so it never blocks stdout. Stdout lines are parsed as JSON — yt-dlp is invoked with <code>--progress-template &quot;%(progress)j&quot;</code> so each progress update arrives as a JSON object. Parsed lines are printed in-place using a carriage return, giving a live updating progress display:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">go</span> <span class="kd">func</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">defer</span> <span class="nb">close</span><span class="p">(</span><span class="nx">done</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="nx">stderrScanner</span><span class="p">.</span><span class="nf">Scan</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">console</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="s">&#34;%s&#34;</span><span class="p">,</span> <span class="nx">stderrScanner</span><span class="p">.</span><span class="nf">Text</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="nx">stdoutScanner</span><span class="p">.</span><span class="nf">Scan</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">line</span> <span class="o">:=</span> <span class="nx">stdoutScanner</span><span class="p">.</span><span class="nf">Text</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nx">json</span><span class="p">.</span><span class="nf">Valid</span><span class="p">([]</span><span class="nb">byte</span><span class="p">(</span><span class="nx">line</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kd">var</span> <span class="nx">result</span> <span class="nx">DownloadResult</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">json</span><span class="p">.</span><span class="nf">Unmarshal</span><span class="p">([]</span><span class="nb">byte</span><span class="p">(</span><span class="nx">line</span><span class="p">),</span> <span class="o">&amp;</span><span class="nx">result</span><span class="p">);</span> <span class="nx">err</span> <span class="o">==</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nx">console</span><span class="p">.</span><span class="nf">SuccessSameLine</span><span class="p">(</span><span class="s">&#34;\r%s\x1b[K&#34;</span><span class="p">,</span> <span class="nx">result</span><span class="p">.</span><span class="nx">DefaultTemplate</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">console</span><span class="p">.</span><span class="nf">Info</span><span class="p">(</span><span class="s">&#34;%s&#34;</span><span class="p">,</span> <span class="nx">line</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>One more thing worth noting: credentials are never logged. The argument builder has a <code>BuildArgsMasked()</code> method that replaces password values with <code>******</code> for any debug output, while <code>BuildArgs()</code> produces the real arguments passed to the process.</p>
<h2 id="get-started-with-yt-dlp-console">Get Started with yt-dlp-console</h2>
<p>If you use yt-dlp occasionally and find yourself reaching for the documentation every time, give <a href="https://github.com/QuickOrBeDead/yt-dlp-console">yt-dlp-console</a> a try. It won&rsquo;t teach you any new yt-dlp flags — that&rsquo;s the whole point.</p>
<p>The project is open source under the MIT license. If you run into a bug or want to see a feature added, <a href="https://github.com/QuickOrBeDead/yt-dlp-console/issues">issues and PRs are open</a>.</p>
]]></content:encoded>
      
      <category><![CDATA[yt-dlp]]></category>
      
      <category><![CDATA[Go]]></category>
      
      <category><![CDATA[Golang]]></category>
      
      <category><![CDATA[CLI]]></category>
      
      <category><![CDATA[video-download]]></category>
      
      <category><![CDATA[Charm]]></category>
      
      <category><![CDATA[Bubbletea]]></category>
      
      <category><![CDATA[Huh]]></category>
      
    </item>
    
  </channel>
</rss>
