Video Metadata Scraper
Pricing
Pay per usage
Video Metadata Scraper
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.
Pricing
Pay per usage
Rating
0.0
(0)
Developer
Zeshan Max
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
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.
{"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:
{"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:
{ "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:
{"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
for the current list.
Running it locally
No Apify account or CLI needed — the SDK falls back to local storage on disk.
py -3.13 -m venv .venv.venv/Scripts/activate # macOS/Linux: source .venv/bin/activatepip install -r requirements-dev.txtmkdir -p storage/key_value_stores/defaultecho '{"urls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"]}' \> storage/key_value_stores/default/INPUT.jsonpython -m src
Results land in storage/datasets/default/, the run summary in
storage/key_value_stores/default/OUTPUT.
Tests
pytest # 55 offline unit tests, no networkpython -m tests.live_matrix # end-to-end matrix against real sitespython -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, Dockerfilesrc/__main__.py asyncio entrypointmain.py input parsing, proxy, worker pool, push_dataextractor.py the only module that talks to yt-dlpnormalize.py info dict -> output contracterrors.py exception -> error_typeconstants.py limits, defaults, field allow-liststests/test_errors.py classification table, offlinetest_normalize.py the output contract, offlinelive_matrix.py end-to-end, needs network
Built with yt-dlp and the Apify Python SDK.