# YouTube Scraper (`api-center/youtube-scraper`) Actor

Extract metadata and direct stream links for one YouTube video per run with yt-dlp through a SOCKS5 proxy. No YouTube API key or quota required.

- **URL**: https://apify.com/api-center/youtube-scraper.md
- **Developed by:** [Leo Yan](https://apify.com/api-center) (community)
- **Stats:** 2 total users, 1 monthly users, 50.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.001 / actor start

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## YouTube Scraper

An [Apify Actor](https://apify.com/actors) that extracts metadata for **one
YouTube video** per run using [yt-dlp](https://github.com/yt-dlp/yt-dlp) through
a SOCKS5 (or HTTP/HTTPS) proxy.

- No YouTube API key, OAuth token or quota.
- One run, one video, one dataset item.
- Accepts watch, `youtu.be`, Shorts, embed and live URLs.
- Metadata only: no media is downloaded and no stream links are returned.

The Actor is written in Go and uses
[`github.com/lrstanley/go-ytdlp`](https://github.com/lrstanley/go-ytdlp) for the
yt-dlp integration and
[`github.com/apify/apify-client-go`](https://github.com/apify/apify-client-go)
for Apify storage.

### How it works

yt-dlp is invoked with `--skip-download`, `--no-playlist`,
`--extractor-args "youtube:skip=hls,dash"` and a single `--print` template of
tab separated columns. Scalars are printed with the `%s` conversion, free text
and lists with `%j`, so a title or description containing a newline cannot
break the one-line-per-video output:

```
--print "%(id)s	%(title)j	%(duration)s	..."
```

`--dump-json` is never used: only the columns the dataset needs leave the
process. Skipping the HLS and DASH manifests removes several requests per
video.

### Input

| Field         | Type    | Default | Description                                   |
| ------------- | ------- | ------- | --------------------------------------------- |
| `url`         | string  | —       | One YouTube video URL. Required.              |
| `timeoutSecs` | integer | `45`    | Timeout for the extraction (30–45 seconds).  |

```json
{
  "url": "https://www.youtube.com/watch?v=SieELGaJV8k",
  "timeoutSecs": 45
}
```

Only single-video URLs are accepted: `/watch?v=…`, `https://youtu.be/<id>`,
`youtube.com/shorts/<id>`, `/embed/<id>` and `/live/<id>`. A watch URL that also
contains a `list` parameter returns the selected video only. Playlist, channel,
handle and search URLs are rejected — run the Actor once per video instead.

### Output

The Actor pushes exactly one item to the default dataset. Field names follow the
popular [YouTube Scraper](https://apify.com/streamers/youtube-scraper) where
practical.

```json
{
  "title": "21 juillet 1969, 3h56, revivez le direct le plus célèbre de la télévision",
  "id": "SieELGaJV8k",
  "url": "https://www.youtube.com/watch?v=SieELGaJV8k",
  "thumbnailUrl": "https://i.ytimg.com/vi/SieELGaJV8k/maxresdefault.jpg",
  "viewCount": 119503,
  "date": "2019-07-20",
  "modifiedDate": "2024-01-15",
  "likes": 765,
  "channelName": "Le Parisien",
  "channelUrl": "https://www.youtube.com/channel/UCfHn_8-ehdem86fEvlFg-Gw",
  "channelId": "UCfHn_8-ehdem86fEvlFg-Gw",
  "channelUsername": "LeParisien",
  "numberOfSubscribers": 2130000,
  "channelIsVerified": true,
  "duration": "11:14",
  "durationSeconds": 674,
  "commentsCount": 0,
  "text": "…",
  "descriptionLinks": [{ "text": "https://example.com", "url": "https://example.com" }],
  "tags": ["…"],
  "categories": ["News & Politics"],
  "isAgeRestricted": false,
  "ageLimit": 0,
  "isLive": false,
  "wasLive": false,
  "liveStatus": "not_live",
  "availability": "public",
  "playableInEmbed": true,
  "platform": "youtube",
  "chapters": [{ "title": "Intro", "startTime": 0, "endTime": 30.5 }],
  "timestamp": 1563642667,
  "inputUrl": "https://www.youtube.com/watch?v=SieELGaJV8k",
  "scrapedAt": "2026-10-29T02:48:55Z"
}
```

Live and scheduling fields are only filled when they apply: `concurrentViewCount` while a stream is live, `releaseTimestamp` for a scheduled premiere, `modifiedDate` when YouTube reports an edit, and `playableInEmbed` for every video.

The full field list and Output-tab views are defined in
[`.actor/dataset_schema.json`](.actor/dataset_schema.json).

### Run limits

The Actor runs with fixed resources that callers are not meant to tune:

| Setting             | Value                | Where it is set                                                                                                |
| ------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------- |
| Memory              | 512 MB               | `.actor/actor.json`: `minMemoryMbytes` = `maxMemoryMbytes` = `defaultMemoryMbytes` = 512, so Apify cannot offer another value |
| Run budget          | 60 s                 | `runTimeout` in [`main.go`](main.go): the run always ends within it, whatever timeout the platform uses           |
| Extraction timeout  | 45 s (input 30–45 s) | `DefaultTimeout` / `MaxTimeout` in [`internal/config`](internal/config/config.go), kept below the run budget       |

Apify Console also has a per-Actor run timeout (**Actor → Settings**). Set it to
60 s so the platform and the Actor agree; the Actor stops at 60 s regardless of
what is configured there.

### Proxy

The image bundles the Cloudflare SOCKS5/HTTP client as
`/usr/local/bin/cf-socks5-http` (downloaded by the `Dockerfile`; override the URL
with `--build-arg NODE_PROXY_URL=…`). On every run the Actor picks one of the 31
North America nodes in [`internal/nodeproxy`](internal/nodeproxy/nodeproxy.go)
— AWS, Azure and GCP regions in the US, Canada and Mexico — and starts it in the
background:

```
cf-socks5-http -cf_domain=<node>:443 -token=pc10201 -client_ip=:30000
```

yt-dlp then uses `socks5h://127.0.0.1:30000`, the default of the `proxyURL`
global in [`main.go`](main.go). The client is killed when the run ends.

To use another proxy instead, point `proxyURL` at it; set it to `""` to connect
directly. Any of these forms is accepted:

```
socks5://host:1080
socks5h://user:pass@host:1080
socks5://host                 # port 1080 is added
host:1080                     # treated as socks5h://host:1080
http://user:pass@host:8080
https://host:8443
```

`socks5h://` resolves DNS through the proxy and is the safest choice. When
`proxyURL` is empty the Actor connects directly and logs a warning. When the
bundled client is missing (local runs outside the image) the Actor also warns and
falls back to the configured `proxyURL`. Proxy credentials are redacted from logs
and error messages.

### Run locally

Requirements:

- Go 1.27 or newer (`go-ytdlp` uses `encoding/json/v2`).
- yt-dlp on `PATH`: `pip install "yt-dlp[default]"` (or set `YTDLP_PATH`).

The Actor takes no command-line flags. It reads the input from the Apify
key-value store when running on the platform, and from `INPUT.json` (or the file
named by `ACTOR_INPUT_FILE`) locally.

```bash
cp INPUT.example.json INPUT.json
## edit INPUT.json, then:
go run .                       # pretty JSON item to stdout

## or write to the local Apify storage layout:
APIFY_LOCAL_STORAGE_DIR=storage go run .   # storage/datasets/default/000000001.json
```

The project uses the Go modules proxy. If `proxy.golang.org` is blocked, run:

```bash
GOPROXY=https://goproxy.cn,direct GOSUMDB=off go test ./...
```

### Build and deploy

```bash
## Build the container
docker build -t youtube-scraper .
## With a reachable module proxy:
docker build --build-arg GOPROXY=https://goproxy.cn,direct -t youtube-scraper .

## Smoke test the image (input comes from the mounted INPUT.json)
docker run --rm -v "$PWD/INPUT.json:/INPUT.json" youtube-scraper

## Deploy with the Apify CLI
npm install -g apify-cli
apify login
apify push
```

You can also connect the project to a Git repository in Apify Console and let
the platform build it from the `Dockerfile`. The runtime stage uses
[`jauderho/yt-dlp:2026.10.26`](https://hub.docker.com/r/jauderho/yt-dlp), which
ships the pinned yt-dlp binary on Alpine. The Go binary is built on Debian in a
separate stage, is statically linked (`CGO_ENABLED=0`), and adds no build
tooling to the final image. To pick up a newer extractor, bump the base image
tag in the `Dockerfile`.

### Pricing

The intended price is **$1 per 1,000 videos** ($0.001 per dataset item). This
maps to Apify pay-per-event model:

- Event: `apify-default-dataset-item`
- Price: **$1.00 per 1,000 items**
- Result: $0.001 per extracted video

A run that returns one video is charged for one dataset item ($0.001). A run
that returns nothing is not charged for results.

Prices are configured in Apify Console under **Actor → Monetization**, not in
`actor.json`. Keep the **Actor start** event free (or unselected); otherwise the
run is charged twice.

### Limitations

- One run extracts one video. Playlist, channel, handle and search URLs are
  rejected at validation time.
- Metadata only: no direct stream links and no subtitle or caption data.
- Private, members-only, age-restricted or region-blocked videos can fail
  depending on what yt-dlp can access through the proxy.
- Comment text is not scraped; only `commentsCount` is reported.
- YouTube changes its site regularly. The yt-dlp version is pinned by the
  `jauderho/yt-dlp:2026.10.26` base image; if extraction starts failing, bump
  that tag in the `Dockerfile` and redeploy.
- Use the Actor in line with YouTube Terms of Service and applicable law. It
  retrieves publicly available metadata only.

### Project layout

```
main.go                              entrypoint, global proxy, logging
internal/config/                      input validation, proxy normalization, URL rules
internal/metadata/extractor.go        yt-dlp invocation and --print column parsing
internal/metadata/video.go            yt-dlp result to dataset item
internal/nodeproxy/                   bundled SOCKS5 client, North America nodes
internal/runtime/actor.go             Apify client / local file storage
.actor/                               Actor, input, output and dataset schemas
Dockerfile                            Go builder + pinned yt-dlp runtime
```

# Actor input Schema

## `url` (type: `string`):

One YouTube video URL, for example https://www.youtube.com/watch?v=SieELGaJV8k. Shorts, youtu.be, embed and live URLs are supported. A watch URL with a list parameter returns only the selected video. Playlist, channel and search URLs are rejected: one run extracts one video.

## `timeoutSecs` (type: `integer`):

Maximum time yt-dlp may spend on the video (30-45 seconds). The run itself is hard capped at 60 seconds and 512 MB of memory.

## Actor input object example

```json
{
  "url": "https://www.youtube.com/watch?v=SieELGaJV8k",
  "timeoutSecs": 45
}
```

# Actor output Schema

## `metadata` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "url": "https://www.youtube.com/watch?v=SieELGaJV8k"
};

// Run the Actor and wait for it to finish
const run = await client.actor("api-center/youtube-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "url": "https://www.youtube.com/watch?v=SieELGaJV8k" }

# Run the Actor and wait for it to finish
run = client.actor("api-center/youtube-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "url": "https://www.youtube.com/watch?v=SieELGaJV8k"
}' |
apify call api-center/youtube-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,api-center/youtube-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/QHbSwAersUzxvy5Qy/builds/1J5QgMyoVdAqdjOdy/openapi.json
