<?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: Hr MuKi</title>
    <description>The latest articles on DEV Community by Hr MuKi (@denisbaagoe).</description>
    <link>https://dev.to/denisbaagoe</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3864342%2F02db3204-b71f-49b3-a1e9-9671d37e818d.jpg</url>
      <title>DEV Community: Hr MuKi</title>
      <link>https://dev.to/denisbaagoe</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/denisbaagoe"/>
    <language>en</language>
    <item>
      <title>I Automated My Entire YouTube Channel with Python — Here's the Architecture</title>
      <dc:creator>Hr MuKi</dc:creator>
      <pubDate>Mon, 06 Apr 2026 17:54:46 +0000</pubDate>
      <link>https://dev.to/denisbaagoe/i-automated-my-entire-youtube-channel-with-python-heres-the-architecture-4706</link>
      <guid>https://dev.to/denisbaagoe/i-automated-my-entire-youtube-channel-with-python-heres-the-architecture-4706</guid>
      <description>&lt;p&gt;I run multiple YouTube channels that are almost entirely automated using Python. Content generation, editing, thumbnail creation, metadata optimization, scheduling, uploading ‚Äî all handled by scripts. Here's how the whole system works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Running a YouTube channel manually is a full-time job. Script writing, recording/generating video, editing, creating thumbnails, writing titles/descriptions, tagging, scheduling, uploading, cross-posting to TikTok and Instagram. For ONE video. I needed to publish 3-4 shorts per day across multiple channels.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;‚îå‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îê
‚îÇ  Content Engine  ‚îÇ ‚Üê Generates scripts, selects footage
‚îú‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚î§
‚îÇ  Video Pipeline  ‚îÇ ‚Üê FFmpeg + moviepy for assembly
‚îú‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚î§
‚îÇ  Thumbnail Gen   ‚îÇ ‚Üê Pillow + custom templates
‚îú‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚î§
‚îÇ  Metadata Layer  ‚îÇ ‚Üê Title/desc/tags optimization
‚îú‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚î§
‚îÇ  Upload Daemon   ‚îÇ ‚Üê Scheduled via LaunchAgents (macOS)
‚îú‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚î§
‚îÇ  Analytics       ‚îÇ ‚Üê Weekly performance reports
‚îî‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îÄ‚îò
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Content Engine
&lt;/h2&gt;

&lt;p&gt;Each channel has a "content profile" ‚Äî a JSON config defining the niche, tone, format, hooks, and source material. The engine takes a topic, pulls relevant data/footage from APIs (Pexels for stock video, ElevenLabs for voiceover), and assembles a script.&lt;/p&gt;

&lt;p&gt;Key libraries: &lt;code&gt;requests&lt;/code&gt;, &lt;code&gt;openai&lt;/code&gt;, &lt;code&gt;json&lt;/code&gt;, &lt;code&gt;pathlib&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Video Pipeline
&lt;/h2&gt;

&lt;p&gt;This is where most of the engineering went. The pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Takes the script and splits it into timed segments&lt;/li&gt;
&lt;li&gt;Matches each segment to footage (keyword-based search + relevance scoring)&lt;/li&gt;
&lt;li&gt;Generates voiceover via ElevenLabs API&lt;/li&gt;
&lt;li&gt;Assembles everything with &lt;code&gt;moviepy&lt;/code&gt; ‚Äî footage clips, voiceover, captions, background music&lt;/li&gt;
&lt;li&gt;Exports at 1080x1920 (9:16) for Shorts or 1920x1080 for long-form&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The caption system was the hardest part. I built a custom word-level subtitle renderer that highlights the current word ‚Äî similar to what CapCut does but fully programmatic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CaptionRenderer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;font_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;font_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;highlight_color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#FFD700&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ImageFont&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;truetype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;font_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;font_size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;highlight_color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;highlight_color&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current_index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frame_size&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;RGBA&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frame_size&lt;/span&gt;&lt;span class="p"&gt;,&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="mi"&gt;0&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;draw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ImageDraw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# Word-level highlighting with bounce animation
&lt;/span&gt;        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;word&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;highlight_color&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;current_index&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#FFFFFF&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="n"&gt;draw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fill&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Upload Daemon
&lt;/h2&gt;

&lt;p&gt;macOS LaunchAgents trigger uploads at scheduled times. Each channel has its own plist file with the schedule. The upload script uses YouTube's Data API v3.&lt;/p&gt;

&lt;p&gt;The tricky part was handling quota limits. YouTube gives you 10,000 units/day. A video upload costs 1,600 units. So with multiple channels, I had to implement a queue system that respects the daily quota.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UploadQueue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;daily_quota&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;upload_cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1600&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;daily_quota&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PriorityQueue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# priority by schedule time
&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;can_upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;upload_cost&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Thumbnail Generation
&lt;/h2&gt;

&lt;p&gt;Each channel has thumbnail templates (Pillow-based). The system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Selects a key frame from the video&lt;/li&gt;
&lt;li&gt;Applies the channel's template (text overlay, borders, color scheme)&lt;/li&gt;
&lt;li&gt;Generates 2-3 variants for A/B testing&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;22 LaunchAgents running daily across all channels&lt;/li&gt;
&lt;li&gt;~15-20 videos generated and uploaded per day&lt;/li&gt;
&lt;li&gt;Average production time per video: 3-8 minutes (compute time, not human time)&lt;/li&gt;
&lt;li&gt;Human involvement: ~30 min/day reviewing outputs and tweaking configs&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;FFmpeg is king.&lt;/strong&gt; &lt;code&gt;moviepy&lt;/code&gt; is great for prototyping but for production I had to drop down to raw FFmpeg commands for performance.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rate limiting everything.&lt;/strong&gt; YouTube API, ElevenLabs, Pexels ‚Äî they all have rate limits. Build a centralized rate limiter from day one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Idempotent uploads.&lt;/strong&gt; Network failures happen. Every upload job has a unique ID and the system checks if it's already been uploaded before retrying.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Content quality &amp;gt; quantity.&lt;/strong&gt; YouTube's algorithm punishes low-retention content. I scaled back to only publish videos that pass a minimum quality threshold.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;I documented the entire architecture, every script template, the scheduling system, and the automation patterns in a toolkit: &lt;a href="https://jakobsend.gumroad.com/l/qwyfrb" rel="noopener noreferrer"&gt;YouTube Automation Toolkit&lt;/a&gt; ‚Äî $7 if you want to build something similar.&lt;/p&gt;

&lt;p&gt;I also have guides for &lt;a href="https://jakobsend.gumroad.com/l/zqrvx" rel="noopener noreferrer"&gt;trading bot development&lt;/a&gt;, &lt;a href="https://jakobsend.gumroad.com/l/wbjfjh" rel="noopener noreferrer"&gt;strategy testing&lt;/a&gt;, and a &lt;a href="https://jakobsend.gumroad.com/l/sgzcwe" rel="noopener noreferrer"&gt;full Python trader's bundle&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Happy to answer questions about specific parts of the pipeline!&lt;/p&gt;

</description>
      <category>python</category>
      <category>automation</category>
      <category>youtube</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
