# Video Metadata Scraper (`zeshanmax/media-metadata-extractor`) Actor

One scraper for thousands of video and audio sites - YouTube, TikTok, Vimeo, SoundCloud, Twitch and more. Get title, views, likes, duration, thumbnails, formats and subtitles as clean JSON, one row per URL. Failed URLs return a typed reason, never silently dropped.

- **URL**: https://apify.com/zeshanmax/media-metadata-extractor.md
- **Developed by:** [Zeshan Max](https://apify.com/zeshanmax) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Media Metadata Extractor

Extract metadata, formats, thumbnails and subtitles from **thousands of yt-dlp-supported
websites** — YouTube, Vimeo, SoundCloud, TikTok, Twitch, X, and many more, plus direct
media files.

Give it one URL or a thousand. Every URL produces exactly one Dataset row, including the
ones that fail, so your row count always matches your input count.

This Actor **does not download media**. It reads the page's metadata and returns it as
clean, consistently-shaped JSON.

***

### What you get

One record per URL. Every successful record carries the same key set, so you can index
into it without guarding — missing data is `null`, or `[]` / `{}` for collections.

```json
{
  "success": true,
  "input_url": "https://youtu.be/dQw4w9WgXcQ",
  "webpage_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
  "extractor": "Youtube",
  "content_type": "video",
  "id": "dQw4w9WgXcQ",
  "title": "Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster)",
  "description": "The official video for …",
  "uploader": "Rick Astley",
  "channel": "Rick Astley",
  "channel_follower_count": 4210000,
  "duration": 213,
  "duration_string": "3:33",
  "upload_date": "2009-10-25",
  "timestamp": 1256440800,
  "thumbnail": "https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg",
  "thumbnails": [{ "id": "39", "url": "…", "width": 1920, "height": 1080 }],
  "view_count": 1814660272,
  "like_count": 19384769,
  "comment_count": 2400000,
  "tags": ["rick astley", "never gonna give you up"],
  "categories": ["Music"],
  "language": "en",
  "age_limit": 0,
  "availability": "public",
  "live_status": "not_live",
  "chapters": [],
  "formats": [
    {
      "format_id": "137",
      "ext": "mp4",
      "resolution": "1920x1080",
      "width": 1920,
      "height": 1080,
      "fps": 25,
      "vcodec": "avc1.640028",
      "acodec": "none",
      "tbr": 4000.5,
      "filesize": 123456789,
      "protocol": "https"
    }
  ],
  "format_count": 44,
  "subtitles": { "en": [{ "ext": "vtt", "url": "…", "name": "English" }] },
  "subtitle_languages": ["de-DE", "en", "es-419", "ja", "pt-BR"],
  "has_subtitles": true,
  "automatic_captions": {},
  "error_type": null,
  "error": null,
  "extracted_at": "2026-09-12T02:41:14Z"
}
```

A failure keeps the same shape and tells you *why*:

```json
{
  "success": false,
  "input_url": "https://www.youtube.com/watch?v=aaaaaaaaaaa",
  "extractor": null,
  "error_type": "unavailable",
  "error": "[youtube] aaaaaaaaaaa: This video is unavailable",
  "retryable": false,
  "extracted_at": "2026-09-12T02:41:14Z"
}
```

Filter on `success` to split the two, or use the built-in **Failures only** Dataset view.

***

### Input

| Field | Type | Default | What it does |
| --- | --- | --- | --- |
| `urls` | array | — | The URLs to extract. |
| `url` | string | — | Shortcut for a single URL. Merged with `urls` if you give both. |
| `proxyConfiguration` | object | Apify Proxy, `RESIDENTIAL` | **Keep this on.** See below. |
| `playlistMode` | enum | `single` | `single` / `expand` / `index` — see below. |
| `maxItemsPerPlaylist` | integer | `50` | Caps playlist expansion. |
| `includeFormatUrls` | boolean | `false` | Adds signed stream URLs to each format. |
| `includeAutomaticCaptions` | boolean | `false` | Adds auto-generated captions. |
| `concurrency` | integer | `5` | URLs processed in parallel (1–20). |
| `timeoutSecs` | integer | `90` | Per-URL wall clock (10–600). |

Minimal input:

```json
{ "urls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"] }
```

#### Proxies matter

Datacenter IP ranges are frequently blocked by the larger video sites. Without a
residential proxy the Actor will look broken on exactly the sites you test first. The
default input turns Apify Proxy on with the `RESIDENTIAL` group; leave it that way unless
you have a reason not to.

If proxy credentials are missing, the run does **not** fail — it logs a warning and
continues on a direct connection.

#### Playlists and channels

A playlist URL is one input that can mean a thousand videos, so you choose what happens:

| Mode | Result | Cost |
| --- | --- | --- |
| `single` *(default)* | One row describing the playlist itself — title, uploader, entry count. | 1 request |
| `expand` | Full metadata for each video, capped by `maxItemsPerPlaylist`. Each entry is extracted independently, so one private video does not sink the rest. | 1 + N |
| `index` | Ids, titles and URLs only. Fastest way to enumerate a channel. | 1 request |

#### Two switches that change record size a lot

Both are off by default, measured on a single YouTube video:

| Setting | Record size |
| --- | --- |
| Defaults | **36 KB** |
| `includeFormatUrls` + `includeAutomaticCaptions` | **557 KB** (15×) |

`includeAutomaticCaptions` adds ~160 machine-translated caption languages. Turn these on
only if you need them — and note that format URLs are signed and expire within hours, so a
stored one is usually dead by the time you read it.

***

### Error types

One failed URL never stops the run. Every failure is labelled:

| `error_type` | Meaning | Retried |
| --- | --- | --- |
| `invalid_url` | Malformed, empty, or not a URL | no |
| `unsupported_url` | No yt-dlp extractor matches the site | no |
| `no_media` | Site supported, page read fine — there's just no video on it | no |
| `private_content` | Private video or private account | no |
| `unavailable` | Deleted, removed, 404 | no |
| `login_required` | Sign-in wall, members-only, premium-only | no |
| `age_restricted` | Age gate blocks extraction | no |
| `geo_restricted` | Blocked in the exit node's country | yes |
| `rate_limited` | HTTP 429 or a bot check | yes |
| `network_error` | DNS, reset, socket timeout | yes |
| `timeout` | Exceeded `timeoutSecs` | no |
| `extraction_failed` | The extractor broke — usually the site changed | yes |

Retryable failures are re-attempted on a fresh proxy session before being recorded — transient
transport errors (`network_error`, `rate_limited`) get two retries, everything else one.

The run itself fails only when the input is unusable (no URLs at all). Progress is written
to the Dataset as each URL finishes, so an aborted run still keeps everything done so far.

A run summary lands in the default key-value store under `OUTPUT`:

```json
{
  "total_records": 9,
  "succeeded": 4,
  "failed": 5,
  "failures_by_type": { "invalid_url": 2, "login_required": 1, "unavailable": 1, "unsupported_url": 1 },
  "input_urls": 9,
  "playlist_mode": "single",
  "proxy_used": true,
  "finished_at": "2026-09-12T02:41:14Z"
}
```

***

### Coverage

yt-dlp supports **thousands** of sites and chooses the extractor for you. It is not every
site on the internet, and some sites require a login this Actor does not perform — those
come back as `login_required` rather than silently empty. Run
[`yt-dlp --list-extractors`](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md)
for the current list.

***

### Running it locally

No Apify account or CLI needed — the SDK falls back to local storage on disk.

```bash
py -3.13 -m venv .venv
.venv/Scripts/activate          # macOS/Linux: source .venv/bin/activate
pip install -r requirements-dev.txt

mkdir -p storage/key_value_stores/default
echo '{"urls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"]}' \
  > storage/key_value_stores/default/INPUT.json

python -m src
```

Results land in `storage/datasets/default/`, the run summary in
`storage/key_value_stores/default/OUTPUT`.

#### Tests

```bash
pytest                            # 55 offline unit tests, no network
python -m tests.live_matrix       # end-to-end matrix against real sites
python -m tests.live_matrix --all # plus the proxy-sensitive rows
```

`live_matrix` runs the real Actor over a fixed set of URLs and checks both the per-site
outcomes and the cross-cutting invariants — one record per unique URL, an identical key
set on every success record, no record near the 9 MB Dataset ceiling, no private yt-dlp
keys leaked. Run it before every release: a red row usually means a site changed, not that
the code did.

***

### Maintenance

**yt-dlp needs to stay current.** Sites change and yt-dlp ships extractor fixes constantly;
a stale image is the most likely reason a working Actor stops working. `requirements.txt`
floors the version rather than pinning it — schedule a **weekly rebuild** on Apify so each
build picks up the latest release.

***

### Project layout

```
.actor/           Apify manifest, input + dataset schemas, Dockerfile
src/
  __main__.py     asyncio entrypoint
  main.py         input parsing, proxy, worker pool, push_data
  extractor.py    the only module that talks to yt-dlp
  normalize.py    info dict -> output contract
  errors.py       exception -> error_type
  constants.py    limits, defaults, field allow-lists
tests/
  test_errors.py      classification table, offline
  test_normalize.py   the output contract, offline
  live_matrix.py      end-to-end, needs network
```

Built with [yt-dlp](https://github.com/yt-dlp/yt-dlp) and the
[Apify Python SDK](https://docs.apify.com/sdk/python/).

# Actor input Schema

## `urls` (type: `array`):

One or more page URLs to extract metadata from. Works with any site yt-dlp supports.

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

A shortcut for extracting one URL. Merged with the list above if both are given.

## `proxyConfiguration` (type: `object`):

Strongly recommended. Datacenter IP ranges are frequently blocked by the larger video sites; residential proxies are what make results consistent.

## `playlistMode` (type: `string`):

What to do when a URL points at a playlist or channel rather than a single video.

## `maxItemsPerPlaylist` (type: `integer`):

Caps expansion so an accidental channel URL cannot become a multi-hour run.

## `includeFormatUrls` (type: `boolean`):

Adds the signed stream URL to every format. These expire within hours and make records far larger, so they are off by default.

## `includeAutomaticCaptions` (type: `boolean`):

YouTube lists auto-captions in 100+ languages. Including them can add megabytes to every record, so they are off by default. Human-written subtitles are always included.

## `concurrency` (type: `integer`):

How many URLs to process at once. Higher is not always faster — sites rate-limit.

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

A single slow URL cannot hold up the rest of the run beyond this.

## Actor input object example

```json
{
  "urls": [
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "playlistMode": "single",
  "maxItemsPerPlaylist": 50,
  "includeFormatUrls": false,
  "includeAutomaticCaptions": false,
  "concurrency": 5,
  "timeoutSecs": 90
}
```

# Actor output Schema

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

One JSON record per input URL. Successful records carry an identical key set; failed ones carry error\_type and error. Filter on the success field to split them.

## `metadataCsv` (type: `string`):

The same records as a spreadsheet-ready CSV.

## `runSummary` (type: `string`):

Totals for the run: how many URLs succeeded, how many failed, and the count for each error type.

# 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 = {
    "urls": [
        "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("zeshanmax/media-metadata-extractor").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 = {
    "urls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("zeshanmax/media-metadata-extractor").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 '{
  "urls": [
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call zeshanmax/media-metadata-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,zeshanmax/media-metadata-extractor"
        }
    }
}
```

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/gQ6awOQ6CNPugjQN2/builds/d84wOWjfk1JrTYHRM/openapi.json
