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

Thetvdbapi Laravel Package

canihavesomecoffee/thetvdbapi

PHP client for TheTVDB API, giving your app simple access to TV series, episode, and artwork data. Wraps the API endpoints in an object-oriented interface so you can search shows and fetch series metadata with minimal setup.

Deep Wiki
Context7

Getting Started

There is no Laravel service provider here — this is a plain PHP library (ISC, ~10 stars, last release 2025-12-17) that wraps TheTVDB's v2 REST API. Budget ~30 minutes to wire it in yourself, then treat it as an internal dependency behind your own adapter.

  1. Install and pin it:
composer require canihavesomecoffee/thetvdbapi:^2.0

Pin a minor version. With a small maintainer surface, a loose constraint is how you get a surprise breaking change on a Tuesday deploy.

  1. Read vendor/canihavesomecoffee/thetvdbapi/src/ first. Two files matter:

    • the Client class — its constructor tells you exactly which credentials it wants (v2 typically needs an API key plus username/userkey to /login, unlike v4's single bearer key);
    • the Authentication/token handling — you need to know whether the client logs in for you or whether you must call a login method first and pass the JWT.
  2. Put credentials in config/services.php, not in a facade call:

// config/services.php
'tvdb' => [
    'api_key'  => env('TVDB_API_KEY'),
    'username' => env('TVDB_USERNAME'),
    'userkey'  => env('TVDB_USERKEY'),
    'base_uri' => env('TVDB_BASE_URI', 'https://api.thetvdb.com'),
],
  1. Bind it once as a singleton so the token isn't re-fetched per resolution:
// app/Providers/AppServiceProvider.php
$this->app->singleton(\TvDb\Client::class, function ($app) {
    $cfg = $app['config']->get('services.tvdb');
    return new \TvDb\Client($cfg['api_key'], $cfg['username'], $cfg['userkey']);
});
  1. First use case: resolve a show from an ID you already have (your shows.tvdb_id, imported from TMDB/IMDb), pull series metadata + episode list, and persist it locally. Run it as an artisan command first, not from a controller — you want to see the raw payload before you design tables.
$client = app(\TvDb\Client::class);
$series = $client->getSeries(81189); // method name is version-dependent; isolate it

Implementation Patterns

Wrap the client behind your own adapter. This is the single most important pattern given the package's age and the v2→v4 migration pressure. One class, one place to fix when the API moves.

namespace App\Services;

class TvdbService
{
    public function __construct(private \TvDb\Client $client) {}

    public function series(int $tvdbId): array
    {
        return Cache::remember("tvdb:series:{$tvdbId}", now()->addDays(3), function () use ($tvdbId) {
            $raw = $this->client->getSeries($tvdbId);
            return $this->normalizeSeries($raw);
        });
    }

    public function episodes(int $tvdbId, string $order = 'default'): array
    {
        return Cache::remember("tvdb:episodes:{$tvdbId}:{$order}", now()->addDay(), function () use ($tvdbId, $order) {
            return $this->normalizeEpisodes($this->client->getSeriesEpisodes($tvdbId, $order));
        });
    }
}

Normalize to your own arrays/DTOs immediately. TheTVDB payloads carry dozens of fields, half of them nullable and named inconsistently (seriesName, airsDayOfWeek, imdbId as a string). Never let those keys ripple into Blade templates or Eloquent casts.

Cache aggressively, and version your keys. Series metadata changes weekly at most; episode air dates change daily. tvdb:v1:series:{id} lets you bust everything by bumping the prefix when your normalizer changes shape.

Throttle the queue, not the request. TheTVDB enforces per-second limits; a 50-show backfill will blow through them. Use a rate-limited job:

class SyncSeriesJob implements ShouldQueue
{
    public $middleware = [\Illuminate\Queue\Middleware\RateLimited::class . ':tvdb'];

    public function handle(TvdbService $tvdb) { /* upsert */ }
}

Register the limiter in a service provider with RateLimiter::for('tvdb', fn ($job) => Limit::perSecond(1)->by('global')), and add retryUntil/backoff for 429s.

Upsert, don't mirror. Show::updateOrCreate(['tvdb_id' => $id], [...]) keyed on the external ID. You will re-run the sync; make it idempotent. Keep tvdb_id indexed and nullable-unique.

Store image paths, build URLs at render time. The API returns relative artwork paths against a banners host. Store /posters/xxx.jpg and compose with a config value in a Blade accessor — the host changes between API versions and you don't want a migration.

Use the updates endpoint for incremental sync. If your adapter exposes it, a nightly job pulling changes since your last cursor is dramatically cheaper than re-fetching everything. Persist the cursor in cache or a sync_state table.

Test at the adapter boundary. Don't mock Guzzle internals. Bind a fake TvdbService in the container, or return fixture JSON from a subclass of your adapter. For integration tests, one Http::fake won't help unless the package uses Laravel's HTTP client — check its Guzzle version and use Guzzle's MockHandler instead.

Gotchas and Tips

  • The v2 sunset is the real risk. TheTVDB has been steering integrators to v4 (api4.thetvdb.com, single bearer token). Before you build anything, confirm your API key still returns data from the v2 base URI. If not, this package is a dead end and you want a v4 client. Writing the adapter first means that swap costs you one class.
  • Two credential models. v2 login needs apikey + username + userkey. If the constructor only takes one argument in your pinned version, check whether it expects a pre-fetched token instead. Getting this wrong yields a 401 that looks like a bad key.
  • JWTs expire (roughly daily). Log in once per process is fine for HTTP, wrong for long-running workers. If you run queue:work for hours, catch 401s in your adapter, re-authenticate, retry once. Never log the token — Guzzle's debug output will happily print the Authorization header.
  • data can be null without being an error. v2 commonly returns HTTP 200 with {"data": null} or an Error key. Check array_key_exists('data', $response) and null-guard before indexing into it; otherwise you'll get Undefined index in production on shows with no aired episodes.
  • Absolute vs aired order. Episode numbering is a parameter, not a property. Anime libraries need absolute; everything else wants default. Cache them under separate keys, and make the order an explicit argument in your service signature so nobody guesses.
  • Language matters more than you think. Set Accept-Language (or the equivalent query param) for translated titles; the default is English and translated series names are frequently empty strings rather than falling back. Store the language alongside the record if you're multi-locale.
  • Air times are network-local strings. airsTime plus airsDayOfWeek in the series' own timezone — no timezone field is guaranteed. If you're building a calendar, treat the raw values as display strings until you've confirmed the timezone from your own data.
  • No Laravel baggage is a feature. There's no facade, no config publish, no migration. Everything the package does is visible in ~20 lines of your provider and adapter, which is exactly what you want from a 10-star dependency: cheap to audit, cheap to replace.
  • Extension point: if the client accepts a Guzzle client or handler stack, inject one with Middleware::retry() on 429/5xx plus a HandlerStack tap that logs latency. That's your only observability hook — take it, because the library ships none.
  • Run composer audit and check the repo's issue cadence before committing. A single-maintainer package with a low release cadence is fine as a thin HTTP wrapper, not fine as something you build core domain behavior on top of.
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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource