Skip to main content
RTILA Marketplace

YouTube Metrics and Comments Collector - project configuration

Collect video metrics and top-level comments from public YouTube watch pages into CSV on your own machine.

Social Media Intermediate Stable v1.0.0 Updated Updated 2026-09-18 10 min read
Download
  • Windows x86_64

    BOT_youtube-metrics-comments-collector.exe

    64.5 MB

    Download
  • macOS Apple Silicon chmod +x

    BOT_youtube-metrics-comments-collector.sh

    58.0 MB

    Download
  • macOS Intel chmod +x

    BOT_youtube-metrics-comments-collector.sh

    64.9 MB

    Download
  • Linux x86_64 chmod +x

    BOT_youtube-metrics-comments-collector.sh

    82.3 MB

    Download

Do not rename the downloaded file - the filename carries the bot's configuration id.

This tab presents the complete project configuration exactly as published - the same JSON the launcher fetches at launch - ready to read, copy, or import into the desktop product. Back to the overview tab.

Project configuration

Full project JSON for youtube-metrics-comments-collector

Canonical byte length: 21,709 - the length of the canonical form the digest covers.

Canonical digest: a331f5367f9707ecdbf27df35c164756245cbd99c2ec534746235fb4e5562454

About the delivered configuration encoding

At launch the backend delivers this configuration to the launcher as plain JSON. The block below is the complete project definition the launcher receives.

What you are reading has machine-readable equivalents, and the JSON at those routes is the same document as the JSON rendered on this page: the listing overview, the per-listing detail document and the configuration document. The digest and byte count shown in the panel header are computed over that document's canonical JSON form - recursively sorted keys, two-space indentation, no trailing newline - which is the form the provisioning pipeline signs and the reconcile control re-digests; it differs from the highlighted block above in key order and by that trailing newline, so hashing the visible text will not reproduce the digest.

Download this JSON
{
  "name": "YouTube Metrics and Comments Collector",
  "settings": {
    "urls": [
      "${video_url}"
    ],
    "headless": false,
    "navigation_timeout": 60000,
    "humanoidEnabled": false,
    "proxyEnabled": false,
    "captchaEnabled": false,
    "matchTimezoneAndLocale": false,
    "block_resources": false,
    "get_urls_from_sitemap": false,
    "respect_robots_txt": false,
    "skip_initial_navigation": true,
    "variables": [
      {
        "name": "video_url",
        "value": "",
        "required": true,
        "is_list": false
      },
      {
        "name": "comment_count",
        "value": "50",
        "required": true,
        "is_list": false
      },
      {
        "name": "sort_order",
        "value": "top",
        "required": true,
        "is_list": false
      }
    ]
  },
  "datasets": {
    "video_metrics": {
      "item_selector": "css=html",
      "properties": [
        {
          "name": "video_id",
          "type": "text",
          "selector": "variable=m_video_id"
        },
        {
          "name": "channel_id",
          "type": "text",
          "selector": "variable=m_channel_id"
        },
        {
          "name": "title",
          "type": "text",
          "selector": "variable=m_title"
        },
        {
          "name": "channel",
          "type": "text",
          "selector": "variable=m_channel"
        },
        {
          "name": "views",
          "type": "text",
          "selector": "variable=m_views"
        },
        {
          "name": "likes",
          "type": "text",
          "selector": "variable=m_likes"
        },
        {
          "name": "published",
          "type": "text",
          "selector": "variable=m_published"
        },
        {
          "name": "duration_seconds",
          "type": "text",
          "selector": "variable=m_duration"
        },
        {
          "name": "description",
          "type": "attribute",
          "selector": "css=meta[property='og:description']",
          "attribute": "content"
        },
        {
          "name": "page_url",
          "type": "page_url"
        }
      ],
      "required_fields": [
        "title"
      ]
    },
    "comments": {
      "item_selector": "css=ytd-comment-thread-renderer",
      "properties": [
        {
          "name": "author",
          "type": "text",
          "selector": "css=#author-text",
          "transformations": [
            {
              "type": "trim"
            }
          ]
        },
        {
          "name": "author_url",
          "type": "attribute",
          "selector": "css=#author-text",
          "attribute": "href",
          "transformations": [
            {
              "type": "script",
              "code": "if(!value) return ''; var v=String(value); return v.indexOf('http')===0 ? v : 'https://www.youtube.com'+(v.charAt(0)==='/'?'':'/')+v;"
            }
          ]
        },
        {
          "name": "text",
          "type": "text",
          "selector": "css=#content-text",
          "transformations": [
            {
              "type": "trim"
            }
          ]
        },
        {
          "name": "likes",
          "type": "text",
          "selector": "css=#vote-count-middle",
          "transformations": [
            {
              "type": "script",
              "code": "if(!value) return '0'; var m=String(value).match(/([\\d.,]+)\\s*([KM]?)/i); if(!m) return '0'; var n=parseFloat(m[1].replace(/,/g,'')); if(isNaN(n)) return '0'; var s=(m[2]||'').toUpperCase(); if(s==='K')n*=1e3; if(s==='M')n*=1e6; return String(Math.round(n));"
            }
          ]
        },
        {
          "name": "published",
          "type": "text",
          "selector": "css=.published-time-text, #published-time-text",
          "transformations": [
            {
              "type": "trim"
            }
          ]
        },
        {
          "name": "replies",
          "type": "text",
          "selector": "css=#more-replies, #more-text",
          "transformations": [
            {
              "type": "trim"
            }
          ]
        },
        {
          "name": "page_url",
          "type": "page_url"
        }
      ]
    }
  },
  "commands": [
    {
      "command": "set_cookie",
      "params": {
        "name": "SOCS",
        "value": "CAI",
        "url": "https://www.youtube.com"
      }
    },
    {
      "command": "set_cookie",
      "params": {
        "name": "CONSENT",
        "value": "YES+cb",
        "url": "https://www.youtube.com"
      }
    },
    {
      "command": "set_cookie",
      "params": {
        "name": "PREF",
        "value": "hl=en",
        "url": "https://www.youtube.com"
      }
    },
    {
      "command": "set_viewport_size",
      "params": {
        "width": 480,
        "height": 940
      }
    },
    {
      "command": "goto",
      "params": {
        "url": "${last_target_url}"
      }
    },
    {
      "command": "try_catch",
      "params": {
        "try": [
          {
            "command": "wait_for_selector",
            "params": {
              "selector": "css=button[aria-label*='Reject all'], button[aria-label*='Accept all']",
              "timeout": 4000
            }
          },
          {
            "command": "click",
            "params": {
              "selector": "css=button[aria-label*='Reject all'], button[aria-label*='Accept all']",
              "timeout": 5000
            }
          }
        ],
        "catch": [
          {
            "command": "comment",
            "params": {
              "text": "No consent interstitial (cookies worked or non-EU)."
            }
          }
        ]
      }
    },
    {
      "command": "wait_for_selector",
      "params": {
        "selector": "css=ytd-watch-flexy",
        "timeout": 45000
      }
    },
    {
      "command": "set_viewport_size",
      "params": {
        "width": 480,
        "height": 940
      }
    },
    {
      "command": "comment",
      "params": {
        "text": "PHASE 1: metrics from embedded JSON (stable route)."
      }
    },
    {
      "command": "try_catch",
      "params": {
        "try": [
          {
            "command": "execute_script",
            "output_variable": "m",
            "params": {
              "script": "var p=window.ytInitialPlayerResponse||{};var v=p.videoDetails||{};var mf=(p.microformat||{}).playerMicroformatRenderer||{};var d=window.ytInitialData||{};var likes='';(function w(o,dep){if(likes||!o||typeof o!=='object'||dep>12)return;for(var k in o){if(/likeCount/i.test(k)&&typeof o[k]==='string'&&/[0-9]/.test(o[k])){likes=o[k];return;}w(o[k],dep+1);}})(d,0);return {video_id:String(v.videoId||''),channel_id:String(mf.channelId||''),title:String(v.title||document.title||''),channel:String(v.author||mf.ownerChannelName||''),views:String(v.viewCount||''),likes:String(likes||''),published:String(mf.publishDate||''),duration:String(v.lengthSeconds||'')};"
            }
          },
          {
            "command": "set_variable",
            "params": {
              "name": "m_video_id",
              "value": "${m.video_id}"
            }
          },
          {
            "command": "set_variable",
            "params": {
              "name": "m_channel_id",
              "value": "${m.channel_id}"
            }
          },
          {
            "command": "set_variable",
            "params": {
              "name": "m_title",
              "value": "${m.title}"
            }
          },
          {
            "command": "set_variable",
            "params": {
              "name": "m_channel",
              "value": "${m.channel}"
            }
          },
          {
            "command": "set_variable",
            "params": {
              "name": "m_views",
              "value": "${m.views}"
            }
          },
          {
            "command": "set_variable",
            "params": {
              "name": "m_likes",
              "value": "${m.likes}"
            }
          },
          {
            "command": "set_variable",
            "params": {
              "name": "m_published",
              "value": "${m.published}"
            }
          },
          {
            "command": "set_variable",
            "params": {
              "name": "m_duration",
              "value": "${m.duration}"
            }
          },
          {
            "command": "extract_data",
            "params": {
              "dataset": "video_metrics"
            }
          }
        ],
        "catch": [
          {
            "command": "comment",
            "params": {
              "text": "METRICS_DEGRADED: embedded JSON unavailable; comments will still be collected."
            }
          }
        ]
      }
    },
    {
      "command": "comment",
      "params": {
        "text": "PHASE 2: strip related-videos column + footer so the comments section owns the document bottom. This is what makes infinite_scroll's bottom-teleport land ON the comment continuation spinner instead of overshooting into lazily-loaded related videos."
      }
    },
    {
      "command": "execute_script",
      "params": {
        "script": "var removed=[];var sec=document.querySelector('ytd-watch-flexy #secondary')||document.querySelector('#secondary');if(sec){sec.remove();removed.push('secondary');}var rel=document.querySelector('ytd-watch-next-secondary-results-renderer');if(rel){rel.remove();removed.push('related');}var foot=document.querySelector('tp-yt-app #footer')||document.querySelector('ytd-app #footer');if(foot){foot.remove();removed.push('footer');}return removed.join(',');"
      },
      "output_variable": "stripped"
    },
    {
      "command": "comment",
      "params": {
        "text": "PHASE 2b: teleport to true document bottom ONCE so every comment-section sentinel intersects (full hydration + interactive sort header + armed continuation spinner). Safe only because #secondary was just stripped — bottom == end of comments."
      }
    },
    {
      "command": "scroll",
      "params": {
        "selector": "css=html",
        "direction": "bottom",
        "delay": 0
      }
    },
    {
      "command": "wait",
      "params": {
        "timeout": 1500
      }
    },
    {
      "command": "comment",
      "params": {
        "text": "PHASE 3: force-mount lazy comment section. Gradual 600px steps (never teleport) so YouTube's IntersectionObserver samples the sentinel; dual visibility gates exit on first thread OR on 'comments off' message."
      }
    },
    {
      "command": "set_variable",
      "params": {
        "name": "threads_visible",
        "value": "false"
      }
    },
    {
      "command": "set_variable",
      "params": {
        "name": "comments_off",
        "value": "false"
      }
    },
    {
      "command": "set_variable",
      "params": {
        "name": "mount_tries",
        "value": "0"
      }
    },
    {
      "command": "while",
      "params": {
        "condition": {
          "type": "script",
          "expression": "String(variables.threads_visible) !== 'true' && String(variables.comments_off) !== 'true' && Number(variables.mount_tries) < 40"
        },
        "do": [
          {
            "command": "scroll",
            "params": {
              "direction": "by_pixels",
              "amount": 600,
              "delay": 0
            }
          },
          {
            "command": "wait",
            "params": {
              "timeout": 450
            }
          },
          {
            "command": "is_visible",
            "params": {
              "selector": "css=ytd-comment-thread-renderer"
            },
            "output_variable": "threads_visible"
          },
          {
            "command": "is_visible",
            "params": {
              "selector": "css=ytd-comments #message, ytd-comments ytd-message-renderer"
            },
            "output_variable": "comments_off"
          },
          {
            "command": "set_variable",
            "params": {
              "name": "mount_tries",
              "value": "${Number(mount_tries) + 1}"
            }
          }
        ]
      }
    },
    {
      "command": "set_variable",
      "params": {
        "name": "comments_unavailable",
        "value": "false"
      }
    },
    {
      "command": "try_catch",
      "params": {
        "try": [
          {
            "command": "wait_for_selector",
            "params": {
              "selector": "css=ytd-comment-thread-renderer",
              "timeout": 10000
            }
          }
        ],
        "catch": [
          {
            "command": "set_variable",
            "params": {
              "name": "comments_unavailable",
              "value": "true"
            }
          }
        ]
      }
    },
    {
      "command": "if",
      "params": {
        "condition": {
          "type": "script",
          "expression": "String(variables.comments_unavailable) !== 'true'"
        },
        "then": [
          {
            "command": "comment",
            "params": {
              "text": "PHASE 4: explicit sort selection (both branches click — never trust the channel's default ordering). Position-based nth click first (language-neutral), exact-text fallback second (guaranteed English via PREF hl=en cookie)."
            }
          },
          {
            "command": "try_catch",
            "params": {
              "try": [
                {
                  "command": "scroll_into_view",
                  "params": {
                    "selector": "css=ytd-comments-header-renderer",
                    "timeout": 4000
                  }
                },
                {
                  "command": "if",
                  "params": {
                    "condition": {
                      "type": "script",
                      "expression": "String(variables.sort_order || '').toLowerCase().indexOf('recent') > -1 || String(variables.sort_order || '').toLowerCase().indexOf('new') > -1"
                    },
                    "then": [
                      {
                        "command": "click",
                        "params": {
                          "selector": "css=ytd-comments-header-renderer tp-yt-paper-dropdown-menu-trigger #icon-button, ytd-comments-header-renderer tp-yt-paper-dropdown-menu-trigger, ytd-comments-header-renderer button[aria-label*='Sort'], #sort-menu #icon-button",
                          "timeout": 8000
                        }
                      },
                      {
                        "command": "wait",
                        "params": {
                          "timeout": 1200
                        }
                      },
                      {
                        "command": "try_catch",
                        "params": {
                          "try": [
                            {
                              "command": "click",
                              "params": {
                                "selector": "css=ytd-comments-header-renderer tp-yt-paper-listbox a, ytd-comments-header-renderer tp-yt-paper-listbox tp-yt-paper-item >> nth=1",
                                "timeout": 5000
                              }
                            }
                          ],
                          "catch": [
                            {
                              "command": "click",
                              "params": {
                                "selector": "text=Newest first",
                                "timeout": 5000
                              }
                            }
                          ]
                        }
                      },
                      {
                        "command": "wait",
                        "params": {
                          "timeout": 2500
                        }
                      },
                      {
                        "command": "wait_for_selector",
                        "params": {
                          "selector": "css=ytd-comment-thread-renderer",
                          "timeout": 20000
                        }
                      }
                    ],
                    "else": [
                      {
                        "command": "click",
                        "params": {
                          "selector": "css=ytd-comments-header-renderer tp-yt-paper-dropdown-menu-trigger #icon-button, ytd-comments-header-renderer tp-yt-paper-dropdown-menu-trigger, ytd-comments-header-renderer button[aria-label*='Sort'], #sort-menu #icon-button",
                          "timeout": 8000
                        }
                      },
                      {
                        "command": "wait",
                        "params": {
                          "timeout": 1200
                        }
                      },
                      {
                        "command": "try_catch",
                        "params": {
                          "try": [
                            {
                              "command": "click",
                              "params": {
                                "selector": "css=ytd-comments-header-renderer tp-yt-paper-listbox a, ytd-comments-header-renderer tp-yt-paper-listbox tp-yt-paper-item >> nth=0",
                                "timeout": 5000
                              }
                            }
                          ],
                          "catch": [
                            {
                              "command": "click",
                              "params": {
                                "selector": "text=Top comments",
                                "timeout": 5000
                              }
                            }
                          ]
                        }
                      },
                      {
                        "command": "wait",
                        "params": {
                          "timeout": 2500
                        }
                      },
                      {
                        "command": "wait_for_selector",
                        "params": {
                          "selector": "css=ytd-comment-thread-renderer",
                          "timeout": 20000
                        }
                      }
                    ]
                  }
                }
              ],
              "catch": [
                {
                  "command": "comment",
                  "params": {
                    "text": "SORT_DEGRADED: menu interaction failed; continuing with default ordering."
                  }
                }
              ]
            }
          },
          {
            "command": "comment",
            "params": {
              "text": "PHASE 5: collect. infinite_scroll on css=html (the standards-mode scrollingElement; body.scrollHeight is unreliable on YouTube). do-block: re-strip insurance, deduped extract, exact-N break. Current engine checkpoints variables+memory every cycle, so crash-resume never duplicates rows."
            }
          },
          {
            "command": "set_variable",
            "params": {
              "name": "total",
              "value": "0"
            }
          },
          {
            "command": "infinite_scroll",
            "params": {
              "selector": "css=html",
              "max_scrolls": "${Math.max(12, Math.ceil(Number(comment_count) / 10) + 8)}",
              "scroll_delay": 2000,
              "do": [
                {
                  "command": "execute_script",
                  "params": {
                    "script": "var s=document.querySelector('ytd-watch-flexy #secondary')||document.querySelector('#secondary');if(s)s.remove();var f=document.querySelector('tp-yt-app #footer')||document.querySelector('ytd-app #footer');if(f)f.remove();return true;"
                  }
                },
                {
                  "command": "extract_data",
                  "params": {
                    "dataset": "comments"
                  },
                  "output_variable": "fresh"
                },
                {
                  "command": "set_variable",
                  "params": {
                    "name": "total",
                    "value": "${Number(total) + (Array.isArray(fresh) ? fresh.length : 0)}"
                  }
                },
                {
                  "command": "if",
                  "params": {
                    "condition": {
                      "type": "script",
                      "expression": "Number(variables.total) >= Number(variables.comment_count)"
                    },
                    "then": [
                      {
                        "command": "break",
                        "params": {}
                      }
                    ],
                    "else": []
                  }
                }
              ]
            }
          },
          {
            "command": "comment",
            "params": {
              "text": "Collection finished: total=${total} requested=${comment_count} sort=${sort_order} mount_tries=${mount_tries} stripped=${stripped}"
            }
          }
        ],
        "else": [
          {
            "command": "comment",
            "params": {
              "text": "COMMENTS_UNAVAILABLE: comment section never rendered (disabled, restricted, bot-walled, or blocked). Metrics were still collected."
            }
          }
        ]
      }
    }
  ],
  "authors": []
}

Frequently asked questions

How do I export YouTube comments to CSV?
Set the video_url input to a public watch page, choose a comment target and a sort order, and run the launcher. The bot scrolls the comment section, extracts each thread into the comments dataset, and saves the result as a CSV in the launcher output folder on your machine - one row per top-level comment, with author, text, normalised like count, published label and the page URL. No cloud account and no conversion step are involved.
How many comments can one run collect?
As many as you ask for, bounded by what the video actually has. The comment_count input defaults to 50 top-level comments; the scroll cap is derived from it automatically (at least 12 scroll steps, plus roughly one step per ten requested comments). Raise the target for deep comment sections; the run stops as soon as the collected total reaches it.
Why does the final comment count exceed the target?
YouTube loads comments in batches of roughly twenty threads. The collector checks the total after each extracted batch and stops once the target is met, so the final file can overshoot by up to one batch - requesting 50 may save up to about 64 rows. This is deliberate: cutting mid-batch would silently drop comments the page had already rendered.
Does it collect comment replies?
No. The bot collects top-level comment threads only and never expands reply chains. The replies column records the raw label shown under a thread (the view-replies toggle text) where one exists, so you can see that replies exist and roughly how many, but reply content itself is out of scope for this build.
Can I scrape multiple videos at once?
Yes. The video_url input supports list mode: enable it and paste one watch-page URL per line, and the engine processes each URL in turn with the same comment target and sort order. Every output row carries its own page_url, video_id and channel_id, so a multi-video batch stays separable after export.
What happens when comments are disabled?
The run stays useful and fails loud. The mounting loop watches for the comments-disabled message; if it appears, or if no comment thread renders in time, the bot logs a COMMENTS_UNAVAILABLE marker and skips collection. The video metrics row is still saved, so you always get the statistics half of the dataset.
How do I sort comments by newest first?
Set the sort_order input to recent (the aliases new and newest are also accepted); the default top collects the popular ordering. The bot never trusts the channel default ordering - it opens the sort menu and clicks the matching entry on every run, with a position-based click first and an exact-English-text fallback second, because the English-locale cookie is pinned before navigation.
Why does the bot set cookies before loading the video?
Three cookies are set first: two consent cookies that preempt the EU cookie interstitial, and one that pins the interface language to English. This keeps runs unattended (no consent dialog to dismiss mid-scroll) and makes the English text fallbacks in the sort menu reliable. If an interstitial still appears, a try/catch block clicks the reject-all or accept-all button and carries on.
How are duplicate comments handled?
The comments dataset de-duplicates on the combination of author, text and published label. Because collection overlaps batches while scrolling - each cycle re-extracts everything currently rendered - deduplication is what makes appending safe: a comment seen in three cycles lands in the CSV exactly once.
Is scraping YouTube comments allowed?
That is your call to own, not the bot's. Automated collection may conflict with the platform's terms of service, and comment text plus author names are personal data under GDPR, UK GDPR and similar regimes. Review the terms, establish a lawful basis before repurposing anything, keep collection to what a patient human viewer could read, and never use the output for spam or profiling.
Does the bot need an internet connection to run?
Yes on first run: the launcher fetches its configuration by id from the backend at launch, so an internet connection is required at least the first time it starts.
Can I rename the downloaded file?
No. The downloaded file must keep its exact name - the filename carries the per-bot configuration id, and renaming it breaks the configuration fetch at launch.