<?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: tian song</title>
    <description>The latest articles on DEV Community by tian song (@tian_song).</description>
    <link>https://dev.to/tian_song</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%2F4172617%2F1701cc73-bfc1-482e-9d09-909e8f2bcfaf.png</url>
      <title>DEV Community: tian song</title>
      <link>https://dev.to/tian_song</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tian_song"/>
    <language>en</language>
    <item>
      <title>I measured burned-in subtitle removal with PSNR. The failures are the interesting part.</title>
      <dc:creator>tian song</dc:creator>
      <pubDate>Wed, 04 Nov 2026 08:27:09 +0000</pubDate>
      <link>https://dev.to/tian_song/i-measured-burned-in-subtitle-removal-with-psnr-the-failures-are-the-interesting-part-4km8</link>
      <guid>https://dev.to/tian_song/i-measured-burned-in-subtitle-removal-with-psnr-the-failures-are-the-interesting-part-4km8</guid>
      <description>&lt;p&gt;Most "remove burned-in subtitles" tutorials end at a before/after. The after always looks fine as a thumbnail. The useful question is what "fine" means numerically, and almost nobody answers it.&lt;/p&gt;

&lt;p&gt;I spent a while on this, so I measured it. Here is the method, the numbers, and the two clips where it goes wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;PSNR needs a reference. So use clips where you can produce both versions: same camera, same encoder settings, one with text burned in, one without. Then you compare your output against the clean plate instead of against your memory of it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; clean.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; output.mp4 &lt;span class="nt"&gt;-lavfi&lt;/span&gt; psnr &lt;span class="nt"&gt;-f&lt;/span&gt; null -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two caveats before the numbers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PSNR is per-pixel and per-frame.&lt;/strong&gt; It cannot see temporal crawl, which is the artifact people actually notice on subtitle removal. A method can score well and still shimmer when you pause it. PSNR is a lower bound on quality, not a verdict.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The ceiling matters more than the score.&lt;/strong&gt; Re-encode the clean clip and compare it to itself, and you get the best number reachable at those settings. Mine was &lt;strong&gt;37.4 dB&lt;/strong&gt;. A 28 dB result against a 37.4 dB ceiling is a much better outcome than 28 dB sounds on its own.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;clip&lt;/th&gt;
&lt;th&gt;PSNR&lt;/th&gt;
&lt;th&gt;what's in frame&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;clean re-encode (ceiling)&lt;/td&gt;
&lt;td&gt;37.4 dB&lt;/td&gt;
&lt;td&gt;reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;street, slow pan&lt;/td&gt;
&lt;td&gt;28.3 dB&lt;/td&gt;
&lt;td&gt;text over a moving background&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;crowd, text crossing faces&lt;/td&gt;
&lt;td&gt;19.8 dB&lt;/td&gt;
&lt;td&gt;people moving under the text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vertical, drifting text&lt;/td&gt;
&lt;td&gt;20.5 dB&lt;/td&gt;
&lt;td&gt;1080x1920, karaoke-style motion&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same clips, naive baselines:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;method&lt;/th&gt;
&lt;th&gt;PSNR&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ffmpeg&lt;/code&gt; delogo&lt;/td&gt;
&lt;td&gt;16.8 dB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;box blur over the region&lt;/td&gt;
&lt;td&gt;18.0 dB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;feeding the subtitled clip back in as "output"&lt;/td&gt;
&lt;td&gt;8.8 dB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row is not a joke, it is the floor. It tells you how much of any score is just the text still being there.&lt;/p&gt;

&lt;p&gt;Both naive methods land &lt;em&gt;below&lt;/em&gt; the crowd clip that everyone would call a failure. That is the honest summary of covering text instead of rebuilding it: you are not removing anything, you are smearing it, and the metric says so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the crowd clip collapses
&lt;/h2&gt;

&lt;p&gt;Text over faces fails for a specific reason. The text occupies the same pixels across many frames, so there is no clean frame to borrow the background from. The information was never recorded. Whatever fills that hole is invented, not recovered.&lt;/p&gt;

&lt;p&gt;Then the content compounds it. What has to be reconstructed is skin and hair with fine structure, and any residual smearing reads as wrong instantly, because we are tuned to faces. On the street clip the background is a wall. A slightly soft wall is still a wall. A slightly soft cheek is a mistake.&lt;/p&gt;

&lt;p&gt;The vertical clip fails differently. The mask moves with the text, so the region that needs rebuilding changes every frame and nothing can be amortised across the clip. Static subtitles are the easy case, and most subtitles are not static.&lt;/p&gt;

&lt;h2&gt;
  
  
  What PSNR does not tell you
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Temporal consistency. The main reason a good-scoring result still looks bad.&lt;/li&gt;
&lt;li&gt;How it holds up at 2x on a phone screen, which is where most of this content is watched.&lt;/li&gt;
&lt;li&gt;Anything about audio, obviously.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And one bias worth knowing: &lt;strong&gt;PSNR rewards blur.&lt;/strong&gt; A soft reconstruction of a textured region often scores higher than a sharp one with slightly wrong structure. So the metric leans toward exactly the failure mode people complain about. If you optimise against PSNR alone you will ship something that scores well and looks like vaseline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing it
&lt;/h2&gt;

&lt;p&gt;Disclosure: I build one of these tools, so treat the numbers as coming from an interested party. That is precisely why the clips and the exact commands are public, including the failures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Benchmark repo with the clips and commands: &lt;a href="https://github.com/vaughnn/burned-in-subtitle-removal-benchmark" rel="noopener noreferrer"&gt;github.com/vaughnn/burned-in-subtitle-removal-benchmark&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The tool I work on: &lt;a href="https://videotextremove.com" rel="noopener noreferrer"&gt;videotextremove.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have a clip I did not test, run the two commands above against your own clean plate. It takes about a minute and it will tell you more than any comparison table, including mine.&lt;/p&gt;

</description>
      <category>video</category>
      <category>ffmpeg</category>
      <category>ai</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Reading a 40,000-comment YouTube thread with the Data API</title>
      <dc:creator>tian song</dc:creator>
      <pubDate>Fri, 09 Oct 2026 06:19:23 +0000</pubDate>
      <link>https://dev.to/tian_song/reading-a-40000-comment-youtube-thread-with-the-data-api-5dap</link>
      <guid>https://dev.to/tian_song/reading-a-40000-comment-youtube-thread-with-the-data-api-5dap</guid>
      <description>&lt;p&gt;Most tutorials that show you how to read YouTube comments stop at the&lt;br&gt;
first page. Here is what actually happens when you try to read a thread&lt;br&gt;
that has 40,000 of them.&lt;/p&gt;
&lt;h2&gt;
  
  
  The endpoint, and what it actually costs
&lt;/h2&gt;

&lt;p&gt;Comments live behind &lt;code&gt;commentThreads.list&lt;/code&gt; on the YouTube Data API v3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET https://www.googleapis.com/youtube/v3/commentThreads
    ?part=snippet,replies
    &amp;amp;videoId=VIDEO_ID
    &amp;amp;maxResults=100
    &amp;amp;pageToken=NEXT_PAGE
    &amp;amp;key=YOUR_API_KEY
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things bite people here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is quota-metered, not rate-limited.&lt;/strong&gt; &lt;code&gt;commentThreads.list&lt;/code&gt; costs 1 unit&lt;br&gt;
per call against a default 10,000-unit daily quota. At &lt;code&gt;maxResults=100&lt;/code&gt; that&lt;br&gt;
is 100 calls — and 100 calls is 1% of your day. A busy video with 40,000&lt;br&gt;
comments needs 400 calls. You will hit the ceiling faster than you expect,&lt;br&gt;
and the error is a plain &lt;code&gt;403 quotaExceeded&lt;/code&gt;, not a retryable 429.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;maxResults&lt;/code&gt; is capped at 100 and the cap is silent.&lt;/strong&gt; Ask for 500 and you&lt;br&gt;
get 100 with no warning. Loop until a page returns fewer items than you asked&lt;br&gt;
for, or you will spin forever on a video whose comment count is a multiple&lt;br&gt;
of 100.&lt;/p&gt;
&lt;h2&gt;
  
  
  Pagination that actually terminates
&lt;/h2&gt;

&lt;p&gt;The naive loop breaks in three places: the last page, a video with comments&lt;br&gt;
disabled, and a video that is age-restricted.&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;video_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.googleapis.com/youtube/v3/commentThreads&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;params&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;part&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;snippet,replies&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;videoId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;video_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxResults&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;api_key&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="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pageToken&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;

        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# 403 covers both "comments disabled" and "quota exhausted".
&lt;/span&gt;        &lt;span class="c1"&gt;# Read the reason, do not just retry.
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&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;errors&lt;/span&gt;&lt;span class="sh"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;commentsDisabled&lt;/span&gt;&lt;span class="sh"&gt;"&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;seen&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quota or permission problem: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&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;seen&lt;/span&gt; &lt;span class="o"&gt;+=&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;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nextPageToken&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;token&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;seen&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;nextPageToken&lt;/code&gt; is absent on the final page — that is your exit&lt;br&gt;
condition. Do not compare against &lt;code&gt;pageInfo.totalResults&lt;/code&gt;; it is an estimate&lt;br&gt;
and it drifts on large threads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replies are a second request
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;commentThreads.list&lt;/code&gt; returns the top-level comment plus, by default, a&lt;br&gt;
single reply. Getting the rest means calling &lt;code&gt;comments.list&lt;/code&gt; with&lt;br&gt;
&lt;code&gt;parentId&lt;/code&gt;. On a thread with heavy back-and-forth this doubles or triples&lt;br&gt;
your quota spend, and there is no way to know in advance how much.&lt;/p&gt;

&lt;p&gt;If you only need the top-level conversation, do not request replies at all —&lt;br&gt;
you get roughly 3x the coverage for the same quota.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you do not want to write any of this
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://apriocity.com" rel="noopener noreferrer"&gt;Apriocity&lt;/a&gt; because I kept writing this same&lt;br&gt;
pagination loop for one-off research tasks. It does the paging, the quota&lt;br&gt;
accounting and the reply expansion for you, and hands back CSV or JSON:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;paste a video link, read the whole thread, search it by keyword&lt;/li&gt;
&lt;li&gt;export the slice as CSV or JSON&lt;/li&gt;
&lt;li&gt;batch mode: up to ten videos, one ZIP back&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It uses the same public API described above, so what you see is exactly&lt;br&gt;
what is publicly visible — nothing is stored on the server side.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;commentThreads.list&lt;/code&gt; = 1 quota unit per call, &lt;code&gt;maxResults&lt;/code&gt; capped at 100&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nextPageToken&lt;/code&gt; missing = done; do not trust &lt;code&gt;totalResults&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;403&lt;/code&gt; is overloaded: check the &lt;code&gt;reason&lt;/code&gt; field before retrying&lt;/li&gt;
&lt;li&gt;Replies cost extra calls — skip them unless you need them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are doing this once, the API is fine. If you are doing it weekly,&lt;br&gt;
that loop is worth automating away.&lt;/p&gt;

</description>
      <category>api</category>
      <category>python</category>
      <category>youtube</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
