Skip to content

Media Downloads Guide

Download images, videos, and thumbnails from ad creatives to your local filesystem.

Quick start

from meta_ads_collector import MetaAdsCollector

with MetaAdsCollector() as collector:
    for ad, media_results in collector.collect_with_media(
        query="fashion",
        country="US",
        max_results=20,
        media_output_dir="./fashion_media",
    ):
        for result in media_results:
            if result.success:
                print(f"  {result.media_type}: {result.local_path}")

How it works

For each ad creative, the downloader attempts to download every available media URL:

Media Type Source Field Description
image creative.image_url Main creative image
video_hd creative.video_hd_url HD video
video_sd creative.video_sd_url SD video
thumbnail creative.thumbnail_url Video preview/thumbnail image

Files are saved as {ad_id}_{creative_index}_{media_type}.{ext}, for example: - 123456_0_image.jpg - 123456_0_video_hd.mp4 - 123456_1_thumbnail.webp

File extension detection

Extensions are resolved in priority order:

  1. Recognizable extension in the URL path (e.g., .jpg, .mp4)
  2. Content-Type header from the HTTP response
  3. Fallback: .bin

Collect with media

The collect_with_media() method on MetaAdsCollector yields (Ad, list[MediaDownloadResult]) tuples:

from meta_ads_collector import MetaAdsCollector

with MetaAdsCollector() as collector:
    for ad, results in collector.collect_with_media(
        query="tech",
        media_output_dir="./media",
        max_results=50,
    ):
        successful = [r for r in results if r.success]
        failed = [r for r in results if not r.success]
        print(f"Ad {ad.id}: {len(successful)} downloaded, {len(failed)} failed")

The ad is yielded even when one or more media downloads fail. Each attempted download has a result entry with success=False and an error message; an empty result list means no downloadable media URL was available or the download loop itself could not produce a result.

Download media for a single ad

from meta_ads_collector import MetaAdsCollector

with MetaAdsCollector() as collector:
    ad = next(collector.search(query="test", max_results=1), None)
    if ad is None:
        print("No ads found")
    else:
        results = collector.download_ad_media(ad, output_dir="./single_ad_media")
        for result in results:
            print(f"{result.media_type}: success={result.success}, path={result.local_path}")

Using MediaDownloader directly

This standalone example retrieves an ad first, then passes it to MediaDownloader:

from meta_ads_collector import MediaDownloader, MetaAdsCollector

with MetaAdsCollector() as collector:
    ad = next(collector.search(query="test", max_results=1), None)
    if ad is not None:
        downloader = MediaDownloader(
            output_dir="./media",
            timeout=30,
            max_retries=2,
        )
        results = downloader.download_ad_media(ad)
        print(results)
    else:
        print("No ads found")

MediaDownloadResult

Each download attempt produces a MediaDownloadResult:

Field Type Description
ad_id str The ad archive ID
creative_index int Zero-based index of the creative
media_type str image, video_hd, video_sd, or thumbnail
url str The source URL
local_path str or None Absolute path to the downloaded file
success bool Whether the download succeeded
error str or None Error message on failure
file_size int or None File size in bytes on success

CLI usage

# Download media alongside ad collection
meta-ads-collector -q "fashion" --download-media --media-dir ./fashion_media -o ads.json

# Custom media directory
meta-ads-collector -q "tech" --download-media --media-dir /data/ad_media -o tech.json

Retry behavior

Downloads retry up to max_retries times (default 2) with exponential backoff. The downloader treats HTTP 403 as a likely expired URL and does not retry it; a 403 response alone does not establish why access was denied.

Completed files with non-zero size are skipped automatically. Downloads use temporary files and are renamed only after success; interrupted .part files are never treated as completed downloads. Returned paths are absolute, and filenames are confined to the output directory.