<?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: XixiSuperMan</title>
    <description>The latest articles on DEV Community by XixiSuperMan (@xixisuperman).</description>
    <link>https://dev.to/xixisuperman</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%2F4137367%2Ff66fe6b5-32ab-4afd-99ba-82c4a7fc0619.png</url>
      <title>DEV Community: XixiSuperMan</title>
      <link>https://dev.to/xixisuperman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/xixisuperman"/>
    <language>en</language>
    <item>
      <title>Monitoring YouTube without the Data API (and without the quota headache)</title>
      <dc:creator>XixiSuperMan</dc:creator>
      <pubDate>Tue, 22 Sep 2026 10:25:11 +0000</pubDate>
      <link>https://dev.to/xixisuperman/monitoring-youtube-without-the-data-api-and-without-the-quota-headache-567a</link>
      <guid>https://dev.to/xixisuperman/monitoring-youtube-without-the-data-api-and-without-the-quota-headache-567a</guid>
      <description>&lt;p&gt;If you have ever tried to watch YouTube for new videos on a topic, you have probably met the YouTube Data API's quota system. A &lt;code&gt;search.list&lt;/code&gt; call costs &lt;strong&gt;100 units&lt;/strong&gt;. The default daily allowance is &lt;strong&gt;10,000 units&lt;/strong&gt;. That is &lt;strong&gt;100 searches a day&lt;/strong&gt; — before you have read a single video's details.&lt;/p&gt;

&lt;p&gt;For a one-off script that is fine. For "tell me when someone uploads a video mentioning my product", it is not. Polling five keywords every fifteen minutes would need 480 searches a day. You would be rate-limited before lunch.&lt;/p&gt;

&lt;p&gt;You can apply for a quota increase. In my experience that is a slow process with an uncertain outcome, and it is a strange thing to need for reading public pages that any logged-out visitor can see.&lt;/p&gt;

&lt;p&gt;So I built the boring alternative: read the public pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  What YouTube actually gives you when logged out
&lt;/h2&gt;

&lt;p&gt;Load &lt;code&gt;youtube.com/results?search_query=...&lt;/code&gt; without a session and YouTube embeds the whole result set in the HTML as a JSON blob:&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="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;ytInitialData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ...several hundred KB... */&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything the page renders is in there — video IDs, titles, channels, view counts, publish times, thumbnails. No API key involved.&lt;/p&gt;

&lt;p&gt;The same is true for a channel's uploads tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that cost me an afternoon
&lt;/h2&gt;

&lt;p&gt;My first parser walked the JSON looking for &lt;code&gt;videoRenderer&lt;/code&gt; objects. It worked. It also &lt;strong&gt;silently returned 4 videos out of 14&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;YouTube does not use one component. It uses several, and which one you get depends on the page and on whatever A/B test you have been bucketed into:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Where I found it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;videoRenderer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Search results, main list&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gridVideoRenderer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Search results, grid section&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;compactVideoRenderer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sidebar recommendations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lockupViewModel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Channel pages — all 30 of them&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;lockupViewModel&lt;/code&gt; is the newer one and it is shaped completely differently. Instead of &lt;code&gt;videoId&lt;/code&gt; and &lt;code&gt;title.runs[0].text&lt;/code&gt;, you get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;contentId                                                    -&amp;gt; video id
metadata.lockupMetadataViewModel.title.content                -&amp;gt; title
...metadata.contentMetadataViewModel.metadataRows[]
     .metadataParts[].text.content                            -&amp;gt; ["486K views", "12 days ago"]
contentImage.thumbnailViewModel.image.sources[-1].url         -&amp;gt; thumbnail
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once I handled both shapes, the same search page went from 4 videos to 17. Nothing had changed on YouTube's side — I had just been reading one of several boxes.&lt;/p&gt;

&lt;p&gt;If you build this yourself, treat the component list as configuration, not as code:&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="n"&gt;CLASSIC_RENDERERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;videoRenderer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gridVideoRenderer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;compactVideoRenderer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When YouTube adds another one, you add a string.&lt;/p&gt;

&lt;h2&gt;
  
  
  The component I missed for a second time
&lt;/h2&gt;

&lt;p&gt;Once I had &lt;code&gt;lockupViewModel&lt;/code&gt; working I thought I was done. I was not.&lt;/p&gt;

&lt;p&gt;A search for &lt;code&gt;skincare&lt;/code&gt; returned 7 videos. The page actually held 39 more in a&lt;br&gt;
fifth component, &lt;code&gt;shortsLockupViewModel&lt;/code&gt; — YouTube Shorts. They are shaped&lt;br&gt;
differently again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;entityId                                       -&amp;gt; "shorts-shelf-item-&amp;lt;videoId&amp;gt;"
overlayMetadata.primaryText.content            -&amp;gt; title
overlayMetadata.secondaryText.content          -&amp;gt; "2.7K views"
thumbnailViewModel.thumbnailViewModel.image    -&amp;gt; thumbnail (yes, nested twice)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reading them took that search from 9 results to 46. For a lot of keywords Shorts&lt;br&gt;
are the majority of what YouTube returns, so dropping them silently is not a&lt;br&gt;
small omission.&lt;/p&gt;

&lt;p&gt;They carry no publish time and no duration — the component simply does not&lt;br&gt;
include either — so be honest about that in your output rather than inventing a&lt;br&gt;
timestamp.&lt;/p&gt;
&lt;h2&gt;
  
  
  Two more things worth knowing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;There is no exact publish time.&lt;/strong&gt; Logged-out pages give you &lt;code&gt;"12 days ago"&lt;/code&gt;, never a timestamp. You can convert it, but be honest about what you have — I named the field &lt;code&gt;published_ts_approx&lt;/code&gt; so nobody builds a sorting feature on top of a value that cannot support one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Being blocked does not look like an error.&lt;/strong&gt; When YouTube does not like your IP it returns &lt;strong&gt;HTTP 200 with a much smaller page&lt;/strong&gt;, not a 4xx. Checking the status code tells you nothing. Checking the response size tells you everything:&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;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;50_000&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;WallError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;got a stub page, rotate the exit IP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I lost real time to this before I noticed the pattern. Residential IPs and a retry that actually rotates the exit fixed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "only new" matters more than it sounds
&lt;/h2&gt;

&lt;p&gt;The obvious design is: run it, get the current results. The problem is that monitoring means running it every fifteen minutes, and the same videos come back every time. You pay for them again, you filter them again, and your alerting channel repeats itself.&lt;/p&gt;

&lt;p&gt;So the tool keeps a 7-day memory of video IDs keyed by the input set, and a scheduled run emits only what it has not seen. A run that returns nothing is the normal case, not a failure — and that is the behaviour you want from an alerting feed.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you would rather not maintain this
&lt;/h2&gt;

&lt;p&gt;I packaged it as an Apify Actor, because the scheduling, proxy rotation, dataset storage and webhook plumbing are not the interesting part:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://apify.com/reportable_broth/youtube-scraper-monitor" rel="noopener noreferrer"&gt;YouTube Scraper | Monitor New Videos | No API Key&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Keywords or channel handles in, new videos out, with the dedupe built in. There is a companion one for Meta Threads that works the same way:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://apify.com/reportable_broth/threads-scraper-monitor" rel="noopener noreferrer"&gt;Threads Scraper | No Login&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Either way, I hope the component-shape table above saves you the afternoon it cost me.&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>python</category>
      <category>api</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
