Scrape YouTube Video Data
Extract title, channel, view count, likes, and upload date from a YouTube video page using stealth mode.
- A Browserless API token from your account dashboard
Steps
YouTube renders all video metadata via JavaScript after the initial page load. A plain HTTP fetch returns almost no data. The examples below wait for the page to settle before reading, and route through stealth mode because YouTube actively detects plain browser automation.
YouTube's DOM selectors change periodically. If any field returns null or an empty string, inspect the live page with browser DevTools to find the updated selector.
- REST API
- Frameworks
- BQL
Send the BQL mutation over HTTP to the stealth endpoint. No browser library or BQL IDE required.
- cURL
- JavaScript
- Python
- Java
- C#
1. Send the request
curl -X POST \
"https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{
"query": "mutation ScrapeYouTubeVideo { goto(url: \"https://www.youtube.com/watch?v=dQw4w9WgXcQ\", waitUntil: networkIdle) { status } waitForSelector(selector: \"h1.ytd-watch-metadata\", timeout: 10000) { time } title: text(selector: \"h1.ytd-watch-metadata yt-formatted-string\") channel: text(selector: \"ytd-channel-name yt-formatted-string a\") views: text(selector: \".ytd-video-view-count-renderer .view-count\") likes: text(selector: \"ytd-segmented-like-dislike-button-renderer yt-formatted-string\") uploadDate: text(selector: \"#info-strings yt-formatted-string\") }",
"variables": {}
}'
2. Check the output
{
"data": {
"goto": { "status": 200 },
"waitForSelector": { "time": 1243 },
"title": "Rick Astley - Never Gonna Give You Up (Official Music Video)",
"channel": "Rick Astley",
"views": "1,234,567,890 views",
"likes": "16M",
"uploadDate": "Oct 24, 2009"
}
}
1. Send the request
const query = `mutation ScrapeYouTubeVideo {
goto(url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", waitUntil: networkIdle) {
status
}
waitForSelector(selector: "h1.ytd-watch-metadata", timeout: 10000) {
time
}
title: text(selector: "h1.ytd-watch-metadata yt-formatted-string")
channel: text(selector: "ytd-channel-name yt-formatted-string a")
views: text(selector: ".ytd-video-view-count-renderer .view-count")
likes: text(selector: "ytd-segmented-like-dislike-button-renderer yt-formatted-string")
uploadDate: text(selector: "#info-strings yt-formatted-string")
}`;
const response = await fetch(
'https://production-sfo.browserless.io/stealth/bql?token=YOUR_API_TOKEN_HERE',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables: {} }),
}
);
const { data } = await response.json();
console.log(JSON.stringify(data, null, 2));
2. Check the output
{
"data": {
"goto": { "status": 200 },
"waitForSelector": { "time": 1243 },
"title": "Rick Astley - Never Gonna Give You Up (Official Music Video)",
"channel": "Rick Astley",
"views": "1,234,567,890 views",
"likes": "16M",
"uploadDate": "Oct 24, 2009"
}
}
1. Install dependencies
pip install requests
2. Send the request
import requests
query = """
mutation ScrapeYouTubeVideo {
goto(url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", waitUntil: networkIdle) {
status
}
waitForSelector(selector: "h1.ytd-watch-metadata", timeout: 10000) {
time
}
title: text(selector: "h1.ytd-watch-metadata yt-formatted-string")
channel: text(selector: "ytd-channel-name yt-formatted-string a")
views: text(selector: ".ytd-video-view-count-renderer .view-count")
likes: text(selector: "ytd-segmented-like-dislike-button-renderer yt-formatted-string")
uploadDate: text(selector: "#info-strings yt-formatted-string")
}
"""
response = requests.post(
'https://production-sfo.browserless.io/stealth/bql',
params={'token': 'YOUR_API_TOKEN_HERE'},
json={'query': query, 'variables': {}},
)
print(response.json())
3. Check the output
{
"data": {
"goto": { "status": 200 },
"waitForSelector": { "time": 1243 },
"title": "Rick Astley - Never Gonna Give You Up (Official Music Video)",
"channel": "Rick Astley",
"views": "1,234,567,890 views",
"likes": "16M",
"uploadDate": "Oct 24, 2009"
}
}
1. Send the request
import java.net.URI;
import java.net.http.*;
String token = "YOUR_API_TOKEN_HERE";
String endpoint = "https://production-sfo.browserless.io/stealth/bql?token=" + token;
String query = "mutation ScrapeYouTubeVideo {"
+ " goto(url: \\\"https://www.youtube.com/watch?v=dQw4w9WgXcQ\\\", waitUntil: networkIdle) { status }"
+ " waitForSelector(selector: \\\"h1.ytd-watch-metadata\\\", timeout: 10000) { time }"
+ " title: text(selector: \\\"h1.ytd-watch-metadata yt-formatted-string\\\")"
+ " channel: text(selector: \\\"ytd-channel-name yt-formatted-string a\\\")"
+ " views: text(selector: \\\".ytd-video-view-count-renderer .view-count\\\")"
+ " likes: text(selector: \\\"ytd-segmented-like-dislike-button-renderer yt-formatted-string\\\")"
+ " uploadDate: text(selector: \\\"#info-strings yt-formatted-string\\\")"
+ " }";
String payload = "{\"query\": \"" + query + "\", \"variables\": {}}";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(endpoint))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
2. Check the output
{
"data": {
"goto": { "status": 200 },
"waitForSelector": { "time": 1243 },
"title": "Rick Astley - Never Gonna Give You Up (Official Music Video)",
"channel": "Rick Astley",
"views": "1,234,567,890 views",
"likes": "16M",
"uploadDate": "Oct 24, 2009"
}
}
1. Send the request
using System.Net.Http;
using System.Text;
using System.Text.Json;
string token = "YOUR_API_TOKEN_HERE";
string endpoint = $"https://production-sfo.browserless.io/stealth/bql?token={token}";
var payload = new
{
query = @"mutation ScrapeYouTubeVideo {
goto(url: ""https://www.youtube.com/watch?v=dQw4w9WgXcQ"", waitUntil: networkIdle) { status }
waitForSelector(selector: ""h1.ytd-watch-metadata"", timeout: 10000) { time }
title: text(selector: ""h1.ytd-watch-metadata yt-formatted-string"")
channel: text(selector: ""ytd-channel-name yt-formatted-string a"")
views: text(selector: "".ytd-video-view-count-renderer .view-count"")
likes: text(selector: ""ytd-segmented-like-dislike-button-renderer yt-formatted-string"")
uploadDate: text(selector: ""#info-strings yt-formatted-string"")
}",
variables = new { },
};
using (HttpClient httpClient = new HttpClient())
{
var content = new StringContent(
JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync(endpoint, content);
string body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}
2. Check the output
{
"data": {
"goto": { "status": 200 },
"waitForSelector": { "time": 1243 },
"title": "Rick Astley - Never Gonna Give You Up (Official Music Video)",
"channel": "Rick Astley",
"views": "1,234,567,890 views",
"likes": "16M",
"uploadDate": "Oct 24, 2009"
}
}
Connect through stealth mode so the browser looks like a real Chrome session, then wait for the title element before reading. YouTube's metadata loads after the initial HTML, so without waitForSelector you'd get empty fields.
- Puppeteer
- Playwright
1. Install dependencies
npm install puppeteer-core
2. Connect and scrape
import puppeteer from 'puppeteer-core';
const browser = await puppeteer.connect({
browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE',
});
try {
const page = await browser.newPage();
await page.goto('https://www.youtube.com/watch?v=dQw4w9WgXcQ', {
waitUntil: 'networkidle2',
});
// Wait for the title to confirm the video metadata has rendered.
await page.waitForSelector('h1.ytd-watch-metadata');
// Batch all reads into one evaluate() call to avoid multiple round-trips.
const data = await page.evaluate(() => ({
title: document.querySelector('h1.ytd-watch-metadata yt-formatted-string')?.textContent?.trim() ?? null,
channel: document.querySelector('ytd-channel-name yt-formatted-string a')?.textContent?.trim() ?? null,
views: document.querySelector('.ytd-video-view-count-renderer .view-count')?.textContent?.trim() ?? null,
likes: document.querySelector('ytd-segmented-like-dislike-button-renderer yt-formatted-string')?.textContent?.trim() ?? null,
uploadDate: document.querySelector('#info-strings yt-formatted-string')?.textContent?.trim() ?? null,
}));
console.log(JSON.stringify(data, null, 2));
} finally {
// Always close to release the session even on error.
await browser.close();
}
3. Check the output
Run with node scrape-youtube.mjs. null values mean the element wasn't present when the script ran. Increase the waitForSelector timeout or check whether a consent banner appeared.
{
"title": "Rick Astley - Never Gonna Give You Up (Official Music Video)",
"channel": "Rick Astley",
"views": "1,234,567,890 views",
"likes": "16M",
"uploadDate": "Oct 24, 2009"
}
1. Install dependencies
npm install playwright-core
2. Connect and scrape
import { chromium } from 'playwright-core';
const browser = await chromium.connect(
'wss://production-sfo.browserless.io/chromium/playwright?token=YOUR_API_TOKEN_HERE'
);
try {
const page = await browser.newPage();
await page.goto('https://www.youtube.com/watch?v=dQw4w9WgXcQ', {
waitUntil: 'networkidle',
});
// Wait for the title to confirm the video metadata has rendered.
await page.waitForSelector('h1.ytd-watch-metadata');
// Batch all reads into one evaluate() call to avoid multiple round-trips.
const data = await page.evaluate(() => ({
title: document.querySelector('h1.ytd-watch-metadata yt-formatted-string')?.textContent?.trim() ?? null,
channel: document.querySelector('ytd-channel-name yt-formatted-string a')?.textContent?.trim() ?? null,
views: document.querySelector('.ytd-video-view-count-renderer .view-count')?.textContent?.trim() ?? null,
likes: document.querySelector('ytd-segmented-like-dislike-button-renderer yt-formatted-string')?.textContent?.trim() ?? null,
uploadDate: document.querySelector('#info-strings yt-formatted-string')?.textContent?.trim() ?? null,
}));
console.log(JSON.stringify(data, null, 2));
} finally {
// Always close to release the session even on error.
await browser.close();
}
3. Check the output
Run with node scrape-youtube.mjs. null values mean the element wasn't present when the script ran. Increase the waitForSelector timeout or check whether a consent banner appeared.
{
"title": "Rick Astley - Never Gonna Give You Up (Official Music Video)",
"channel": "Rick Astley",
"views": "1,234,567,890 views",
"likes": "16M",
"uploadDate": "Oct 24, 2009"
}
1. Write the mutation
Navigate to the video URL and use waitForSelector to block until the title element appears, then read each metadata field. We send this to /stealth/bql instead of the default /bql because YouTube's bot detection blocks a plain browser session before the page fully loads.
mutation ScrapeYouTubeVideo {
goto(url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", waitUntil: networkIdle) {
status
}
waitForSelector(selector: "h1.ytd-watch-metadata", timeout: 10000) {
time
}
title: text(selector: "h1.ytd-watch-metadata yt-formatted-string")
channel: text(selector: "ytd-channel-name yt-formatted-string a")
views: text(selector: ".ytd-video-view-count-renderer .view-count")
likes: text(selector: "ytd-segmented-like-dislike-button-renderer yt-formatted-string")
uploadDate: text(selector: "#info-strings yt-formatted-string")
}
2. Run it
Paste into the BQL IDE and click Run.
3. Check the output
{
"data": {
"goto": { "status": 200 },
"waitForSelector": { "time": 1243 },
"title": "Rick Astley - Never Gonna Give You Up (Official Music Video)",
"channel": "Rick Astley",
"views": "1,234,567,890 views",
"likes": "16M",
"uploadDate": "Oct 24, 2009"
}
}
Next steps
- Scrape Glassdoor Job Listings — another stealth-mode scrape against aggressive bot detection
- Automate Google Search — pull search results using the same
/stealth/bqlendpoint - Solving Cloudflare Challenges — bypass Cloudflare's interstitial pages before scraping