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 Transcript Laravel Package

mrmysql/youtube-transcript

Fetch YouTube video transcripts in Laravel with a simple, fluent API. Pull captions and subtitle text by video ID or URL, ready for search, summarization, and AI pipelines.

Deep Wiki
Context7

Getting Started

This is a framework-agnostic PHP library, not a Laravel package — expect no service provider, no config file, and no facade. That shapes everything about how you adopt it.

Install and orient yourself:

composer require mrmysql/youtube-transcript

# What did I actually get?
composer show mrmysql/youtube-transcript
find vendor/mrmysql/youtube-transcript/src -name "*.php" | head -50
grep -rn "public function" vendor/mrmysql/youtube-transcript/src

At 20 stars with a thin README, the source is the documentation. Spend ten minutes on the src/ tree before writing any application code: find the main entry class, the exception classes, and whatever value object represents a transcript segment. Those three things determine your integration.

First use case — validate in Tinker before touching your app:

php artisan tinker
// Confirm the real class/method names against the source first.
$transcript = app(\MrMySQL\YoutubeTranscript\YoutubeTranscript::class)
    ->fetch('dQw4w9WgXcQ', 'en');

dump($transcript); // inspect the shape: array of segments? DTO? JSON string?

Do this on 5–6 videos across categories (a captioned tutorial, a music video, a non-English video, one with captions disabled) so you see both the happy path and the failure modes before you design around them. Whatever the return shape is — array of ['text' => ..., 'start' => ..., 'duration' => ...], a collection of DTOs, or raw JSON — normalize it immediately into your own DTO. That single decision insulates you from the library's churn.

Implementation Patterns

Wrap it, always. The library talks to an undocumented YouTube endpoint; treat it as a volatile dependency behind your own interface.

namespace App\Services\Transcript;

interface TranscriptFetcher
{
    /** @return TranscriptSegment[] */
    public function fetch(string $videoId, array $languages = ['en']): array;
}
namespace App\Services\Transcript;

use MrMySQL\YoutubeTranscript\YoutubeTranscript;
use App\Exceptions\TranscriptUnavailable;

class YouTubeTranscriptFetcher implements TranscriptFetcher
{
    public function __construct(private YoutubeTranscript $client) {}

    public function fetch(string $videoId, array $languages = ['en']): array
    {
        try {
            $raw = $this->client->fetch($videoId, $languages);
        } catch (\Throwable $e) {
            throw new TranscriptUnavailable("No transcript for {$videoId}", previous: $e);
        }

        return array_map(
            fn ($s) => new TranscriptSegment(
                text: html_entity_decode($s['text'], ENT_QUOTES | ENT_HTML5, 'UTF-8'),
                start: (float) $s['start'],
                duration: (float) ($s['duration'] ?? 0),
            ),
            $raw
        );
    }
}

Bind it in a service provider and never reference the vendor namespace outside that one class. When YouTube breaks the library (it will), you swap one class.

Language fallback chain. Manual captions beat auto-generated ones for quality, and regional codes muddy everything. Ask for a list, and record which language you actually got:

$segments = $fetcher->fetch($videoId, ['en', 'en-US', 'en-GB']);
// persist: $transcript->language, $transcript->is_auto_generated

Queue it — never fetch synchronously in a request. It's an outbound HTTP call to a host that rate-limits and sometimes hangs.

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

    public function __construct(public string $videoId) {}

    public function handle(TranscriptFetcher $fetcher): void
    {
        $segments = $fetcher->fetch($this->videoId);
        Transcript::updateOrCreate(
            ['video_id' => $this->videoId],
            ['segments' => $segments, 'fetched_at' => now()]
        );
    }

    public function failed(\Throwable $e): void
    {
        Transcript::updateOrCreate(
            ['video_id' => $this->videoId],
            ['failed_at' => now(), 'failure_reason' => $e->getMessage()]
        );
    }
}

Cache aggressively — transcripts are immutable. Once a video's captions are published they essentially never change. Cache forever with a manual bust button:

$segments = Cache::rememberForever(
    "yt-transcript:{$videoId}:{$lang}",
    fn () => $fetcher->fetch($videoId, [$lang])
);

Store the full segment list as JSON in a transcripts table rather than re-fetching. Search and embedding work should read from your DB, not from YouTube.

Batch/backfill with rate limiting. If you're ingesting a channel, throttle hard and jitter:

foreach ($videoIds as $id) {
    FetchTranscriptJob::dispatch($id)->delay(now()->addSeconds(rand(2, 8)));
}

Use a dedicated queue with low concurrency (--max-jobs=1) so you don't get your server IP soft-banned.

Presentation pattern. Segments map cleanly to timestamped deep links:

$url = "https://youtu.be/{$videoId}?t=" . (int) floor($segment->start);

For a transcript pane in Livewire/Inertia, send the whole segment array once and do active-segment highlighting client-side off a timeupdate listener — don't round-trip per line.

Testing. Never let your test suite hit YouTube. Bind a fake in your test case:

$this->app->bind(TranscriptFetcher::class, fn () => new FakeTranscriptFetcher());

Keep a couple of real response fixtures (captured once, committed to the repo) and have your fake return those.

Gotchas and Tips

This endpoint is unofficial and will break. YouTube periodically changes how the watch page serves caption tracks. Pin the version in composer.json, and add a health check / alert on TranscriptUnavailable rate — an uptick usually means the library needs a bump, not that videos lost captions.

Expect these failure classes and handle them distinctly (not all as a generic 500):

  • Captions genuinely disabled on the video — permanent, don't retry.
  • Age-restricted / region-blocked / members-only — may need cookies or won't work at all.
  • Rate limited or "confirm you're not a bot" — transient, back off and retry later.
  • Video deleted/private — permanent.

Datacenter IPs get blocked fast. AWS/GCP/DigitalOcean egress ranges are the first thing YouTube throttles. If you're doing volume, run the fetcher on a residential-ish egress or accept a low success rate and a retry queue. Don't debug your code for an hour before checking whether it's an IP issue — try the same video from your laptop.

HTML entities and markup in caption text. Auto-generated captions arrive with &#39;, &amp;, and sometimes inline <c> tags or timestamps. Run everything through html_entity_decode() and strip tags in your normalizer, not at render time.

Timestamps are floats, segments can overlap or duplicate. Don't assume monotonic, non-overlapping, or clean sentence boundaries. Merge for readability, keep raw segments for deep-linking.

Auto-generated vs. uploaded. Auto captions have no punctuation consistency and mishear names; manual ones are clean but scarce. If your product exposes transcripts to users, record is_auto_generated so you can caveat the quality.

Empty or music-only transcripts. Some videos return a single segment or an empty array. Decide up-front whether a zero-length transcript is a success or a failure, and cache the outcome either way so you don't hammer the endpoint re-probing a hopeless video.

Set an explicit HTTP timeout on whatever Guzzle/HTTP client the library uses (via constructor, if it accepts one). A stalled request on a queue worker with a 60s timeout will eat a worker slot. If the client isn't injectable, cap it with $job->timeout and consider forking the library's transport.

Extension points to look for in src/: whether the HTTP client is constructor-injectable (lets you add logging, proxies, retries), whether exceptions are a meaningful hierarchy or a single \Exception, and whether there's a method to list available transcripts before fetching — that's what you want for language fallback rather than guessing codes.

Legal/ToS. This scrapes YouTube rather than using the official Data API. For videos you own, prefer the official Data API captions endpoint — it's supported and stable. For third-party videos at any real scale, get a legal read before shipping a product feature on it.

Log the raw response on failure (video ID, HTTP status, body prefix) in a rotating channel. When the library breaks, those logs are what tells you whether YouTube changed the payload shape or you got throttled — and they're what you'll paste into the GitHub issue.

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.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums