DEV Community

ahmet gedik
ahmet gedik

Posted on

Multi-Region Video Discovery with YouTube Data API

The YouTube Data API lets you fetch trending videos from specific countries using the regionCode parameter. In this article, I'll show how to build a multi-region fetching system, handle API quotas intelligently, and normalize the results. This approach powers DailyWatch, which aggregates trending content from 8 countries.

Understanding the API

The key endpoint is videos.list with the chart=mostPopular parameter:

GET https://www.googleapis.com/youtube/v3/videos
  ?part=snippet,statistics,contentDetails
  &chart=mostPopular
  &regionCode=US
  &maxResults=50
  &key=YOUR_API_KEY
Enter fullscreen mode Exit fullscreen mode

This costs 1 quota unit and returns up to 50 videos.

The Multi-Region Fetcher

class RegionalVideoFetcher {
    private const API_BASE = 'https://www.googleapis.com/youtube/v3/videos';
    private const PARTS = 'snippet,statistics,contentDetails';
    private int $quotaUsed = 0;

    public function __construct(
        private readonly string $apiKey,
        private readonly int $dailyQuotaLimit = 10000,
    ) {}

    /**
     * Fetch trending videos from multiple regions
     * @param string[] $regions ISO 3166-1 alpha-2 country codes
     * @return array<string, Video[]> Region => Videos mapping
     */
    public function fetchMultiRegion(array $regions, int $maxPerRegion = 50): array {
        $results = [];

        foreach ($regions as $region) {
            if (!$this->hasQuota(cost: 1)) {
                echo "Quota limit approaching, stopping at region {$region}\n";
                break;
            }

            try {
                $results[$region] = $this->fetchRegion($region, $maxPerRegion);
                echo "Fetched " . count($results[$region]) . " videos for {$region}\n";
            } catch (\RuntimeException $e) {
                echo "Error fetching {$region}: {$e->getMessage()}\n";
                $results[$region] = [];
            }

            usleep(200000); // 200ms delay between regions
        }

        return $results;
    }

    private function fetchRegion(string $regionCode, int $maxResults): array {
        $url = self::API_BASE . '?' . http_build_query([
            'part' => self::PARTS,
            'chart' => 'mostPopular',
            'regionCode' => strtoupper($regionCode),
            'maxResults' => min($maxResults, 50),
            'key' => $this->apiKey,
        ]);

        $response = file_get_contents($url);
        $this->quotaUsed += 1;

        if ($response === false) {
            throw new \RuntimeException("API request failed for region {$regionCode}");
        }

        $data = json_decode($response, true);

        if (isset($data['error'])) {
            throw new \RuntimeException($data['error']['message'] ?? 'Unknown API error');
        }

        return array_map(
            fn(array $item) => $this->normalizeVideo($item, $regionCode),
            $data['items'] ?? []
        );
    }

    private function normalizeVideo(array $item, string $region): array {
        $snippet = $item['snippet'] ?? [];
        $stats = $item['statistics'] ?? [];
        $content = $item['contentDetails'] ?? [];

        return [
            'video_id'      => $item['id'],
            'title'         => $snippet['title'] ?? '',
            'description'   => mb_substr($snippet['description'] ?? '', 0, 500),
            'channel_title' => $snippet['channelTitle'] ?? '',
            'channel_id'    => $snippet['channelId'] ?? '',
            'category_id'   => (int)($snippet['categoryId'] ?? 0),
            'thumbnail_url' => $snippet['thumbnails']['medium']['url']
                              ?? $snippet['thumbnails']['default']['url'] ?? '',
            'published_at'  => $snippet['publishedAt'] ?? '',
            'duration'      => $content['duration'] ?? '',
            'view_count'    => (int)($stats['viewCount'] ?? 0),
            'like_count'    => (int)($stats['likeCount'] ?? 0),
            'region'        => $region,
        ];
    }

    public function hasQuota(int $cost = 1): bool {
        return ($this->quotaUsed + $cost) <= $this->dailyQuotaLimit;
    }

    public function getQuotaUsed(): int {
        return $this->quotaUsed;
    }
}
Enter fullscreen mode Exit fullscreen mode

Storing with Deduplication

Videos trending in multiple regions need deduplication:

function storeVideos(PDO $db, array $regionResults): int {
    $stored = 0;

    $insertVideo = $db->prepare("
        INSERT OR IGNORE INTO videos
            (video_id, title, description, channel_title, channel_id,
             category_id, thumbnail_url, published_at, duration,
             view_count, like_count)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    ");

    $insertRegion = $db->prepare("
        INSERT OR IGNORE INTO video_regions (video_id, region, fetched_at)
        VALUES (?, ?, datetime('now'))
    ");

    $updateViews = $db->prepare("
        UPDATE videos SET view_count = MAX(view_count, ?)
        WHERE video_id = ?
    ");

    $db->beginTransaction();

    foreach ($regionResults as $region => $videos) {
        foreach ($videos as $v) {
            $insertVideo->execute([
                $v['video_id'], $v['title'], $v['description'],
                $v['channel_title'], $v['channel_id'], $v['category_id'],
                $v['thumbnail_url'], $v['published_at'], $v['duration'],
                $v['view_count'], $v['like_count'],
            ]);

            $insertRegion->execute([$v['video_id'], $region]);
            $updateViews->execute([$v['view_count'], $v['video_id']]);

            $stored++;
        }
    }

    $db->commit();
    return $stored;
}
Enter fullscreen mode Exit fullscreen mode

Usage

$fetcher = new RegionalVideoFetcher(apiKey: $apiKey);
$regions = ['US', 'GB', 'DE', 'FR', 'IN', 'BR', 'AU', 'CA'];

$results = $fetcher->fetchMultiRegion($regions, maxPerRegion: 50);
$stored = storeVideos($db, $results);

echo "Stored {$stored} video records, quota used: {$fetcher->getQuotaUsed()}\n";
Enter fullscreen mode Exit fullscreen mode

This system powers the content pipeline at dailywatch.video, fetching fresh trending data every 2 hours across 8 regions while staying well within API quota limits.

Top comments (0)