Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Youtube Downloader Laravel Package

athlon1600/youtube-downloader

A pure PHP YouTube downloader with no JavaScript interpreters, shell calls, or heavy dependencies. Fetch video and audio stream links via a simple API, with built-in exception handling. Actively maintained.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and verify the floor

composer require athlon1600/youtube-downloader "^4.0"

The library requires PHP 7.4+ and pulls in only athlon1600/php-curl-file-downloader. There is no service provider, facade, config file, or migration — installing the package does not register anything. Budget half a day for the glue below before you write feature code.

Minimal smoke test — run this in php artisan tinker before building anything on top of it, so you learn immediately whether your server's IP is being blocked by YouTube:

use YouTube\YouTubeDownloader;
use YouTube\Exception\YouTubeException;

try {
    $options = (new YouTubeDownloader())->getDownloadLinks('https://www.youtube.com/watch?v=aqz-KE-bpKQ');
    $format  = $options->getFirstCombinedFormat();
    dump($format?->url, $options->getVideoInfo()->getTitle());
} catch (YouTubeException $e) {
    dump($e->getMessage());
}

Where to look first in the source

  • src/YouTubeDownloader.php — the only class most apps need. getDownloadLinks() is the entry point; getBrowser() hands you the HTTP layer.
  • src/Models/DownloadOptions.php — getAllFormats(), getCombinedFormats(), getFirstCombinedFormat(), getVideoInfo().
  • src/Models/StreamFormat.php — each format's ->url plus itag-derived metadata (mime type, quality, audio/video flags). Read this to answer "which format do I pick?".
  • src/Models/VideoInfo.php — title, duration, author, thumbnails. Replaced the old VideoDetails class in v4.
  • src/Exception/YouTubeException.php — the single catch-all for anything YouTube-shaped going wrong.
  • src/YouTubeStreamer.php — server-side proxy streaming. Useful, but see the Gotchas.

First real use case: a "paste a URL, get the title + a download button" form. Resolve the link synchronously for the metadata preview, then push the actual download into a queued job (see below). Don't start with batch downloads or a Chrome extension — those amplify every failure mode in the Gotchas section.

Implementation Patterns

Wrap it behind your own interface, not the vendor class. The package has no Laravel glue, so you own the binding. This also gives you one place to swap in a fake for tests:

// app/Contracts/YouTubeResolver.php
interface YouTubeResolver
{
    public function resolve(string $url): ResolvedVideo; // your DTO, not DownloadOptions
}

// app/Services/YouTube/YouTubeDownloaderResolver.php
class YouTubeDownloaderResolver implements YouTubeResolver
{
    public function resolve(string $url): ResolvedVideo
    {
        $options = (new YouTubeDownloader())->getDownloadLinks($url);
        // map to your DTO here; never let StreamFormat escape into app code
    }
}

// app/Providers/AppServiceProvider.php
$this->app->bind(YouTubeResolver::class, YouTubeDownloaderResolver::class);

Mapping to a DTO is the single highest-value decision here. StreamFormat is derived from YouTube's itag tables, which the maintainer updates in minor releases — if your controllers and Blade views consume it directly, a format-detection change becomes an app-wide refactor.

Always resolve on a queue, never in the request cycle. getDownloadLinks() makes several sequential cURL calls (watch page → signature challenge → player response) with no Guzzle timeout conventions and no Laravel HTTP middleware in front of it. In an unqueued controller a slow or hanging YouTube response consumes a php-fpm worker for its full duration.

final class ResolveYouTubeVideo implements ShouldQueue
{
    public int $tries = 3;
    public array $backoff = [30, 120, 600];
    public int $timeout = 60;

    public function __construct(public int $videoRequestId) {}

    public function handle(YouTubeResolver $resolver): void
    {
        // resolve, persist StreamFormat URLs + expires_at, dispatch completion event
    }
}

Note $backoff — a 429 or a transient 5xx from YouTube is worth retrying on a widening schedule; a malformed URL is not, so catch YouTubeException and distinguish in failed() if you care about that distinction.

Cache aggressively and store the expiry. Download URLs are signed and short-lived (roughly six hours). Persist them with a timestamp rather than re-resolving per click:

Cache::remember("yt:links:{$videoId}", now()->addHours(5), function () use ($url) {
    return $this->toDto((new YouTubeDownloader())->getDownloadLinks($url));
});

Storing them in the database is worth it if you show a download page users return to — a download_urls JSON column plus refreshed_at beats a cache miss on a cold entry.

Format selection: be explicit, never trust getFirstCombinedFormat() blindly. It returns ?StreamFormat. The library does not merge audio and video (see To-Do list), so combined formats top out around 720p. Write the fallback yourself:

$combined = $options->getCombinedFormats();

$format = collect($combined)
    ->sortByDesc(fn ($f) => (int) $f->itag)  // or your own quality ranking
    ->first()
    ?? $combined[0]
    ?? null;

abort_if($format === null, 422, 'No downloadable format available for this video.');

If a user demands 1080p+, you must either accept the limitation or add ffmpeg merging out of band — the package will not do it for you.

Delivery: redirect for browsers, proxy only when you must. For public videos, redirect()->away($format->url) is free and puts zero load on your server. Use YouTubeStreamer only when you need to force Content-Disposition: attachment or mask the googlevideo.com URL — and if you do, serve it from a dedicated queue worker or a streaming-optimized pool, because the PHP process stays alive for the length of the transfer. If you accept a user-supplied URL into the streamer endpoint, validate it against an allowlist of googlevideo hosts; v3.1.1 shipped a fix for exactly this class of issue in the package's own stream.php demo.

Age-restricted / region-blocked content. Both hooks live on the Browser, and both are per-instance, so set them on the object you use:

$downloader = new YouTubeDownloader();
$browser = $downloader->getBrowser();
$browser->consentCookies();                          // EU consent interstitial
$browser->setCookieFile(storage_path('yt-cookies.txt')); // age-gated videos
$browser->setUserAgent('Mozilla/5.0 ...');          // match the browser you exported from

Treat the cookie file as a secret: store it in storage/app/private, exclude it from git, and never log the Browser object. It is a full session for whatever Google account you exported it from.

Test with a fake, not the network. Because the library uses its own cURL layer, Http::fake() does nothing. Bind your YouTubeResolver interface to a fake in tests and unit-test format selection and DTO mapping against fixture arrays — leave a single, opt-in, network-touching test for the smoke check.

Gotchas and Tips

URLs expire, and your tests will not catch it. A cached StreamFormat->url from 8 hours ago returns a 403 from googlevideo.com, not from YouTube. When a user reports "download broken but the page works," check URL age first. Set your cache TTL comfortably under the expiry window and refresh proactively.

Throttling is the package's own open problem. The To-Do list explicitly names YouTube's sub-100 kb/s throttle and the lack of captcha/429 handling. On shared hosting or cloud NAT, where many apps egress from one IP, expect intermittent YouTubeExceptions and 429s. Mitigations, in order of effort: rotate through a small pool of proxies (the Browser supports a proxy), serialise requests through a named queue with a RateLimited middleware, and surface a clear "try again in a few minutes" message instead of a stack trace.

Signature deciphering is a moving target. SignatureDecoder parses YouTube's obfuscated player JS. When YouTube changes it, links break until the maintainer ships a patch — historically that has been days, occasionally longer. Pin the constraint ("^4.0" is fine), subscribe to releases, and keep an alert on the exception rate of your resolve job so you learn about breakage from monitoring rather than from users.

No Laravel HTTP integration means no Http::fake, no retry middleware, no outbound logging. You get no request timing, no status-code metrics, and no central place to add a circuit breaker. Add Log::info / timing instrumentation inside your YouTubeResolver implementation — it is your only observability seam.

getDownloadLinks() throws; it does not return empty. Since v3.0 the contract is exceptions, not falsy returns. Every call site needs a try/catch for YouTubeException, translated into a domain error (422, ValidationException, or a user-facing message). A bare $options->getFirstCombinedFormat()->url will fatal on audio-only or livestream URLs — null-check it.

v4 renamed things. If you copy snippets from older blog posts or Stack Overflow: VideoDetails → VideoInfo, AbstractModel → JsonObject, PlayerResponseParser → SignatureLinkParser, and SplitStream/getSplitFormats() are gone. Anything referencing those is pre-v4.

YouTubeStreamer::stream() echoes and exits. It is written for a standalone PHP script, not a controller. Don't return a Laravel Response alongside it, don't call it mid-middleware, and don't expect Content-Length handling or ranged requests to behave the way Storage::download() does. If you need a polished proxy, wrap the byte pump yourself rather than relying on the class's defaults.

Config caching and long-lived workers are harmless here — there's no config to cache and the downloader holds no cross-request state, but it is cURL-handle-backed, so don't memoise a YouTubeDownloader instance as a singleton in a long-running Octane/queue worker and assume the cookie jar or connection state stays clean. Construct per job.

Legal and policy surface is yours, not the package's. The README carries a personal-use-only disclaimer and notes that downloading copyrighted material violates YouTube's ToS. If you're shipping this in a client product, that is a business conversation to have before the technical one — put a clear ownership/rights attestation in your UI and log who requested what.

Keep an eye on the Browser as an extension point. Cookies, user-agent, proxy, and consent all live there. That's the seam for every regional or account-scoped workaround you'll need — build your wrapper so those four knobs are constructor arguments, not hardcoded, and you'll avoid re-plumbing later.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log