<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://mckinnon.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://mckinnon.dev/" rel="alternate" type="text/html" hreflang="en-US" /><updated>2026-04-19T09:10:44-04:00</updated><id>https://mckinnon.dev/feed.xml</id><title type="html">mckinnon.dev</title><subtitle>Explorations in Home Automation, AI, FinTech, Finance and more.</subtitle><author><name>mckinnon.dev</name><email>hey@mckinnon.dev</email></author><entry><title type="html">YouTube Without the Algorithm</title><link href="https://mckinnon.dev/youtube-without-the-algorithm/" rel="alternate" type="text/html" title="YouTube Without the Algorithm" /><published>2026-04-12T09:00:00-04:00</published><updated>2026-04-12T09:00:00-04:00</updated><id>https://mckinnon.dev/n8n-youtube</id><content type="html" xml:base="https://mckinnon.dev/youtube-without-the-algorithm/"><![CDATA[<p><a href="/assets/img/n8n-youtube/top.webp"><img src="/assets/img/n8n-youtube/top.webp" alt="YouTube Without the Algorithm Top Image" class="thumbnail" /></a></p>

<p>I grew up on <a href="https://en.wikipedia.org/wiki/Nova_(American_TV_program)">Nova</a>, <a href="https://en.wikipedia.org/wiki/Scientific_American_Frontiers">Scientific American Frontiers</a> and <a href="https://en.wikipedia.org/wiki/Modern_Marvels">Modern Marvels</a>. Those shows got me into STEM and eventually into computers. I wanted something like that for my boys…content that’s not only interesting but somewhat educational as well. So I built something.</p>

<p>I had three goals:</p>

<ol>
  <li>I need an easy way to safely curate videos for my kids.</li>
  <li>It has to be simple enough for my wife and the grandparents to use.</li>
  <li>Must be easy to view content.</li>
</ol>

<p>With those in mind, here’s what I do now: I find a video I’m ok with my son watching, I’ll send the YouTube URL (or several) to a dedicated email address, and minutes later it’s available.</p>

<p>To get this working, I’m using a combination of: <a href="https://www.docker.com/">Docker</a>, <a href="https://n8n.io/">n8n</a>, and <a href="https://watch.plex.tv/me">Plex</a>. Proxmox powers my little home lab on a <a href="https://www.amazon.com/dp/B0G69J6G4B?th=1">BeeLink PC</a>.</p>

<p class="callout">Disclosure: AI helped me refine this post and saved some time troubleshooting building this one Saturday morning.</p>

<h2 id="the-workflow">The Workflow</h2>
<p>At a high level, this works in the simplest way possible with existing building blocks:</p>

<ol>
  <li>Find videos you want, copy the URLs.</li>
  <li>Send them to a dedicated email address.</li>
  <li>Videos are available on Plex within 10–15 minutes.</li>
</ol>

<h2 id="workflow-1--store-incoming-urls">Workflow 1 — Store Incoming URLs</h2>

<p><a href="/assets/img/n8n-youtube/parse-flow.webp"><img src="/assets/img/n8n-youtube/parse-flow.webp" alt="Store Incoming Urls Workflow" class="thumbnail" /></a></p>

<p>n8n is setup with a Gmail trigger which watches the inbox, parses any YouTube URLs out of the email body and stores them in a Data Table for processing.</p>

<p>Here’s the URL parsing code for reference:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">items</span> <span class="o">=</span> <span class="nx">$input</span><span class="p">.</span><span class="nx">all</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">results</span> <span class="o">=</span> <span class="p">[];</span>
<span class="kd">const</span> <span class="nx">DOMAINS</span> <span class="o">=</span> <span class="p">[</span><span class="dl">'</span><span class="s1">youtube.com</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">youtu.be</span><span class="dl">'</span><span class="p">];</span>
<span class="kd">const</span> <span class="nx">ALLOWED_SENDERS</span> <span class="o">=</span> <span class="p">[</span><span class="dl">'</span><span class="s1">email1@example.com</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">email2@example.com</span><span class="dl">'</span><span class="p">];</span>

<span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">item</span> <span class="k">of</span> <span class="nx">items</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">raw</span> <span class="o">=</span> <span class="nx">item</span><span class="p">.</span><span class="nx">json</span><span class="p">.</span><span class="nx">headers</span><span class="p">?.</span><span class="nx">subject</span> <span class="o">||</span> <span class="dl">''</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">subject</span> <span class="o">=</span> <span class="nx">raw</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="dl">'</span><span class="s1">: </span><span class="dl">'</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="p">?</span> <span class="nx">raw</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="nx">raw</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="dl">'</span><span class="s1">: </span><span class="dl">'</span><span class="p">)</span> <span class="o">+</span> <span class="mi">2</span><span class="p">).</span><span class="nx">trim</span><span class="p">()</span> <span class="p">:</span> <span class="nx">raw</span><span class="p">.</span><span class="nx">trim</span><span class="p">();</span>
  <span class="kd">const</span> <span class="k">from</span> <span class="o">=</span> <span class="nx">item</span><span class="p">.</span><span class="nx">json</span><span class="p">.</span><span class="k">from</span><span class="p">?.</span><span class="nx">text</span> <span class="o">||</span> <span class="dl">''</span><span class="p">;</span>

  <span class="kd">const</span> <span class="nx">fromEmail</span> <span class="o">=</span> <span class="p">(</span><span class="k">from</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="sr">/&lt;</span><span class="se">(</span><span class="sr">.+</span><span class="se">?)</span><span class="sr">&gt;/</span><span class="p">)</span> <span class="o">||</span> <span class="p">[</span><span class="kc">null</span><span class="p">,</span> <span class="k">from</span><span class="p">])[</span><span class="mi">1</span><span class="p">].</span><span class="nx">trim</span><span class="p">().</span><span class="nx">toLowerCase</span><span class="p">();</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">ALLOWED_SENDERS</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">fromEmail</span><span class="p">))</span> <span class="k">continue</span><span class="p">;</span>

  <span class="kd">const</span> <span class="nx">lines</span> <span class="o">=</span> <span class="p">(</span><span class="nx">item</span><span class="p">.</span><span class="nx">json</span><span class="p">.</span><span class="nx">text</span> <span class="o">||</span> <span class="dl">''</span><span class="p">).</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="se">\n</span><span class="dl">'</span><span class="p">);</span>
  <span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">line</span> <span class="k">of</span> <span class="nx">lines</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="nx">line</span><span class="p">.</span><span class="nx">trim</span><span class="p">();</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">url</span><span class="p">.</span><span class="nx">startsWith</span><span class="p">(</span><span class="dl">'</span><span class="s1">http</span><span class="dl">'</span><span class="p">))</span> <span class="k">continue</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">noProto</span> <span class="o">=</span> <span class="nx">url</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://</span><span class="dl">'</span><span class="p">,</span> <span class="dl">''</span><span class="p">).</span><span class="nx">replace</span><span class="p">(</span><span class="dl">'</span><span class="s1">http://</span><span class="dl">'</span><span class="p">,</span> <span class="dl">''</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">rawHost</span> <span class="o">=</span> <span class="nx">noProto</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">/</span><span class="dl">'</span><span class="p">)[</span><span class="mi">0</span><span class="p">];</span>
    <span class="kd">const</span> <span class="nx">host</span> <span class="o">=</span> <span class="nx">rawHost</span><span class="p">.</span><span class="nx">startsWith</span><span class="p">(</span><span class="dl">'</span><span class="s1">www.</span><span class="dl">'</span><span class="p">)</span> <span class="p">?</span> <span class="nx">rawHost</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mi">4</span><span class="p">)</span> <span class="p">:</span> <span class="nx">rawHost</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">DOMAINS</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="nx">host</span><span class="p">)</span> <span class="o">&gt;</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">results</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span> <span class="na">json</span><span class="p">:</span> <span class="p">{</span> <span class="nx">url</span><span class="p">,</span> <span class="k">from</span><span class="p">,</span> <span class="nx">subject</span><span class="p">,</span> <span class="na">messageId</span><span class="p">:</span> <span class="nx">item</span><span class="p">.</span><span class="nx">json</span><span class="p">.</span><span class="nx">id</span> <span class="p">}</span> <span class="p">});</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nx">results</span><span class="p">;</span>
</code></pre></div></div>

<p>The Data Table schema is fairly basic as well:</p>

<pre><code class="language-csv">url,status,from_email,subject,queued_at,title,channel,site_name,skipped,downloaded_at,error_message
https://www.youtube.com/watch?v=rjFxthTkw7A,done,"""Dad"" &lt;foobar@example.com&gt;",,2026-04-05T17:21:21.752Z,How To Draw A Scary Witch Folding Surprise,Art for Kids Hub,YouTube,1,2026-04-05T17:40:43.261Z,
</code></pre>

<h4 id="workflow-2---download-videos">Workflow 2 - Download Videos</h4>

<p><a href="/assets/img/n8n-youtube/workflow-download.webp"><img src="/assets/img/n8n-youtube/workflow-download.webp" alt="Download Workflow" class="thumbnail" /></a></p>

<p>A second workflow runs separately and picks up queued URLs one at a time. I split this out because a single long-running workflow kept timing out or kept running into other wierd issues that were cumbersome to debug. This one marks each URL as “downloading,” calls <code class="language-plaintext highlighter-rouge">yt-dlp</code> via a small Python wrapper, then sends a Pushover notification when it’s done. Videos land in a NAS folder that Plex scans periodically.</p>

<p>One bug I haven’t fixed is that downloads that fail silently stay stuck at “downloading” forever. I have a safeguard in place to ensure videos aren’t too long or too large so they silently fail…for now.</p>

<h2 id="some-learnings">Some Learnings</h2>

<p><strong>The allowlist isn’t optional.</strong> Early on I got spam with YouTube links that made it into the queue. Nothing harmful got through, but it was a reminder that anything internet-facing needs filtering at the door.</p>

<p><strong>Set up failure notifications.</strong> OAuth with Google has been flaky a couple times. Without Pushover alerts I wouldn’t have known videos weren’t downloading until my son asked why something wasn’t there.</p>

<hr />

<p>This has been running for about a month now, with much fanfare. My son now gets inspired with building STEM-related project and plays a handful of safe music videos, <a href="https://www.youtube.com/watch?v=xw0xffEwExI&amp;">this has been a favorite recently</a>.</p>]]></content><author><name>mckinnon.dev</name><email>hey@mckinnon.dev</email></author><category term="parenting" /><category term="ai" /><category term="projects" /><summary type="html"><![CDATA[I built a self-hosted video queue with n8n and yt-dlp so my kid can watch YouTube without YouTube deciding what's next.]]></summary></entry></feed>