# Návod na filmové API v C# pro .NET 8

> Tento návod vytvoří konzolovou aplikaci .NET 8, která převezme název filmu, vyhledá ho v amodb, načte nejlepší shodu s hodnoceními a vypíše krátkou kartu. Používá jen HttpClient a System.Text.Json, čte klíč z AMODB_KEY, při 429 opakuje pokus a ukládá odpovědi do souborů. Příprava trvá asi 15 minut.

## Co vytvoříte

Vytvoříte `MovieLookup`, konzolovou aplikaci, která převezme název z příkazového řádku, vyhledá ho v amodb, načte nejlepší shodu s hodnoceními a vypíše krátkou kartu s odkazem na plakát. Potřebujete .NET 8 SDK, klíč amodb a asi 15 minut. Program používá jen `HttpClient` a `System.Text.Json`, takže neinstalujete žádné balíčky NuGet.

## Krok 1: Získejte API klíč

Zvolte tarif na stránce [ceník](https://amodb.dev/cs/pricing/). Tarify se liší jen rychlostí, a to 20, 40 nebo 120 požadavků za minutu. Vytvořte klíč v panelu účtu, je to řetězec z 32 malých hexadecimálních znaků. Exportujte ho a vyzkoušejte jedním voláním:

```bash
export AMODB_KEY=YOUR_API_KEY
curl -s -o /dev/null -w "%{http_code}\n" \
  -H "Authorization: Bearer $AMODB_KEY" \
  "https://api.amodb.dev/3/movie/550"
```

`200` znamená, že klíč funguje. `401` znamená, že klíč je nesprávný nebo chybí. V PowerShellu nastavte proměnnou pomocí `$env:AMODB_KEY = "YOUR_API_KEY"`. Stránka [autentizace](https://amodb.dev/cs/authentication/) popisuje parametr dotazu `api_key` jako alternativu k hlavičce.

## Krok 2: Vytvořte projekt

```bash
dotnet new console -n MovieLookup -f net8.0
cd MovieLookup
```

Šablona .NET 8 zapíná implicitní using a nullable referenční typy. Tím se už importují `System`, `System.IO`, `System.Linq` a `System.Net.Http`. Ostatní řádky `using` přidáte v souboru.

## Krok 3: Odešlete první požadavek

Vytvořte jeden `HttpClient` se základní adresou, 10sekundovým timeoutem a hlavičkou Bearer. Základní adresu zakončete lomítkem a relativní cesty pište bez úvodního lomítka. Úvodní lomítko by nahradilo část `/3` v základní cestě.

```csharp
using System.Net.Http.Headers;

using var http = new HttpClient
{
    BaseAddress = new Uri("https://api.amodb.dev/3/"),
    Timeout = TimeSpan.FromSeconds(10),
};
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AMODB_KEY"));

var response = await http.GetAsync("search/movie?query=fight%20club&language=en-US");
Console.WriteLine((int)response.StatusCode);
```

## Krok 4: Převeďte JSON na záznamy

Odpověď používá názvy ve snake case, například `release_date`. Nastavte `JsonNamingPolicy.SnakeCaseLower`, které je nové v .NET 8, a píšte členy v PascalCase. Pole, která nedeklarujete, se ignorují.

```csharp
record SearchResponse(List<SearchHit> Results);
record SearchHit(int Id, string Title, string? ReleaseDate);
record Movie(int Id, string Title, string? Tagline, string? ReleaseDate, int? Runtime,
    string? PosterPath, double VoteAverage, RatingSet? Ratings);
record RatingSet(Score? Imdb, Score? Kinopoisk, Score? Metacritic, Score? RottenTomatoes, Score? Tmdb);
record Score(double Value, int? Votes);
```

Objekt `ratings` se objeví, protože volání detailů přidává `append_to_response=ratings`. Každý z jeho pěti zdrojů je objekt s `value` a `votes`, nebo `null`, když skóre chybí. `votes` může být také `null`: živá odpověď pro Fight Club nemá počet hlasů pro Kinopoisk, Metacritic ani Rotten Tomatoes. Všechna ostatní pole uvádí stránka [filmy](https://amodb.dev/cs/movies/).

## Krok 5: Ošetřete chyby a limity požadavků

Obalte volání smyčkou, která reaguje na stavový kód.

| Stav         | Význam                                    | Co dělá aplikace                                                                      |
| ------------ | ----------------------------------------- | ------------------------------------------------------------------------------------- |
| 401          | Klíč chybí, je nesprávný nebo pozastavený | Vypíše zprávu a skončí, opakování nepomůže.                                           |
| 402          | Platnost tarifu skončila                  | Vypíše zprávu a skončí. Obnovte ho na stránce [ceník](https://amodb.dev/cs/pricing/). |
| 404          | Pro toto ID neexistuje záznam             | Nahlásí to a skončí.                                                                  |
| 429          | Překročili jste požadavky za minutu       | Počká `Retry-After` sekund, nejvýše 30, a opakuje pokus nejvýše dvakrát.              |
| 500 nebo 504 | Selhání na straně serveru                 | Nahlásí stav, příkaz spusťte znovu později.                                           |

```csharp
for (var attempt = 0; ; attempt++)
{
    using var response = await http.GetAsync(path);
    if (response.StatusCode == HttpStatusCode.TooManyRequests && attempt < 2)
    {
        var seconds = Math.Min(response.Headers.RetryAfter?.Delta?.TotalSeconds ?? 5, 30);
        await Task.Delay(TimeSpan.FromSeconds(seconds));
        continue;
    }
    if (response.StatusCode == HttpStatusCode.Unauthorized)
        throw new AmodbException("The API key was rejected (401). Check AMODB_KEY.");
    // ...handle other statuses, then read the body
}
```

Síťové chyby se projeví jako `HttpRequestException` a timeout jako `TaskCanceledException`. Obě zachyťte na nejvyšší úrovni. Každá odpověď s klíčem nese také `X-RateLimit-Remaining`, takže dlouhá úloha může zpomalit, než dosáhne 429. Úplné seznamy najdete na stránkách [limity požadavků](https://amodb.dev/cs/rate-limits/) a [chyby](https://amodb.dev/cs/errors/).

## Krok 6: Ukládejte odpovědi na disk

Kontrola limitu proběhne před cache na edge, takže odpověď doručená z edge se do požadavků za minutu stále počítá. Jedinou cestou, jak požadavky ušetřit, je cache ve vašem vlastním programu. Edge drží detaily 6 hodin a výsledky hledání 10 minut, aplikace proto používá stejné doby platnosti. Soubor ve složce temp je čerstvý, když je čas jeho zápisu uvnitř TTL.

```csharp
class FileCache
{
    readonly string dir = Path.Combine(Path.GetTempPath(), "amodb-cache");

    string PathFor(string key) =>
        Path.Combine(dir, Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(key))) + ".json");

    public string? Get(string key, TimeSpan ttl)
    {
        var file = PathFor(key);
        return File.Exists(file) && DateTime.UtcNow - File.GetLastWriteTimeUtc(file) < ttl
            ? File.ReadAllText(file)
            : null;
    }
}
```

## Krok 7: Složte to dohromady

Toto je kompletní `Program.cs`. K cache přidává metodu `Set` a třídu `AmodbClient`, která spojuje cache, smyčku opakování a nastavení JSON.

```csharp
using System.Globalization;
using System.Net;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; // same number format on every machine

var key = Environment.GetEnvironmentVariable("AMODB_KEY");
if (string.IsNullOrWhiteSpace(key))
{
    Console.Error.WriteLine("Set AMODB_KEY first.");
    return 1;
}

var title = string.Join(' ', args).Trim();
if (title.Length == 0)
{
    Console.Error.WriteLine("Usage: dotnet run -- \"movie title\"");
    return 1;
}

using var http = new HttpClient
{
    BaseAddress = new Uri("https://api.amodb.dev/3/"),
    Timeout = TimeSpan.FromSeconds(10),
};
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", key);
var client = new AmodbClient(http, new FileCache());

try
{
    var search = await client.GetAsync<SearchResponse>(
        $"search/movie?query={Uri.EscapeDataString(title)}&language=en-US", TimeSpan.FromMinutes(10));
    var hit = search.Results.FirstOrDefault();
    if (hit is null)
    {
        Console.WriteLine($"No movie found for \"{title}\".");
        return 0;
    }

    var movie = await client.GetAsync<Movie>(
        $"movie/{hit.Id}?language=en-US&append_to_response=ratings", TimeSpan.FromHours(6));
    Print(movie);
    return 0;
}
catch (AmodbException ex)
{
    Console.Error.WriteLine(ex.Message);
    return 1;
}
catch (HttpRequestException ex)
{
    Console.Error.WriteLine($"Network error: {ex.Message}");
    return 1;
}
catch (TaskCanceledException)
{
    Console.Error.WriteLine("The request timed out after 10 seconds.");
    return 1;
}

static void Print(Movie m)
{
    var year = m.ReleaseDate is { Length: >= 4 } d ? d[..4] : "n/a";
    Console.WriteLine($"{m.Title} ({year}), {m.Runtime?.ToString() ?? "?"} min");
    if (!string.IsNullOrEmpty(m.Tagline)) Console.WriteLine(m.Tagline);
    Console.WriteLine($"IMDb: {Describe(m.Ratings?.Imdb)}");
    Console.WriteLine($"TMDB: {Describe(m.Ratings?.Tmdb)}");
    if (m.PosterPath is not null)
        Console.WriteLine($"Poster: https://img.amodb.dev/t/p/w342{m.PosterPath}");
}

static string Describe(Score? s) =>
    s is null ? "not known" : $"{s.Value:0.0}" + (s.Votes is { } v ? $" ({v:N0} votes)" : "");

record SearchResponse(List<SearchHit> Results);
record SearchHit(int Id, string Title, string? ReleaseDate);
record Movie(int Id, string Title, string? Tagline, string? ReleaseDate, int? Runtime,
    string? PosterPath, double VoteAverage, RatingSet? Ratings);
record RatingSet(Score? Imdb, Score? Kinopoisk, Score? Metacritic, Score? RottenTomatoes, Score? Tmdb);
record Score(double Value, int? Votes);

class AmodbException : Exception
{
    public AmodbException(string message) : base(message) { }
}

class FileCache
{
    readonly string dir = Path.Combine(Path.GetTempPath(), "amodb-cache");

    string PathFor(string key) =>
        Path.Combine(dir, Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(key))) + ".json");

    public string? Get(string key, TimeSpan ttl)
    {
        var file = PathFor(key);
        return File.Exists(file) && DateTime.UtcNow - File.GetLastWriteTimeUtc(file) < ttl
            ? File.ReadAllText(file)
            : null;
    }

    public void Set(string key, string json)
    {
        Directory.CreateDirectory(dir);
        File.WriteAllText(PathFor(key), json);
    }
}

class AmodbClient(HttpClient http, FileCache cache)
{
    static readonly JsonSerializerOptions Options = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
    };

    public async Task<T> GetAsync<T>(string path, TimeSpan ttl)
    {
        var cached = cache.Get(path, ttl);
        if (cached is not null) return JsonSerializer.Deserialize<T>(cached, Options)!;

        for (var attempt = 0; ; attempt++)
        {
            using var response = await http.GetAsync(path);
            if (response.StatusCode == HttpStatusCode.TooManyRequests && attempt < 2)
            {
                var seconds = Math.Min(response.Headers.RetryAfter?.Delta?.TotalSeconds ?? 5, 30);
                await Task.Delay(TimeSpan.FromSeconds(seconds));
                continue;
            }
            if (response.StatusCode == HttpStatusCode.Unauthorized)
                throw new AmodbException("The API key was rejected (401). Check AMODB_KEY.");
            if (!response.IsSuccessStatusCode)
                throw new AmodbException($"amodb returned status {(int)response.StatusCode}.");

            var body = await response.Content.ReadAsStringAsync();
            cache.Set(path, body);
            return JsonSerializer.Deserialize<T>(body, Options)!;
        }
    }
}
```

Spusťte ho s názvem filmu. Toto je výstup pro první nalezený film, film 550. Počty hlasů se časem zvyšují, váš výsledek se proto mírně liší.

```bash
dotnet run -- "fight club"
# Fight Club (1999), 139 min
# Mischief. Mayhem. Soap.
# IMDb: 8.8 (2,669,989 votes)
# TMDB: 8.4 (33,039 votes)
# Poster: https://img.amodb.dev/t/p/w342/jSziioSwPVrOy9Yow3XhWIBDjq1.jpg
```

Řádek TMDB ukazuje 8.4, protože kód vypisuje jedno desetinné místo z 8.437 v `ratings.tmdb.value`. Hostitel plakátů `img.amodb.dev` nepotřebuje klíč, URL plakátu proto můžete předat přímo prohlížeči nebo ovládacímu prvku obrázku. Seznam velikostí viz [obrázky](https://amodb.dev/cs/images-api/).

## Další kroky

- Hledejte s rokem nebo stránkováním na stránce [vyhledávání](https://amodb.dev/cs/search/) a filtrujte seznamy pomocí [discover](https://amodb.dev/cs/discover/).
- Zobrazte každé skóre a jeho pravidla pro null na stránce [hodnocení](https://amodb.dev/cs/ratings/).
- Přesuňte konzolový kód do služby ASP.NET Core s `AddHttpClient` a `IMemoryCache`.
- Porovnejte stejnou úlohu v [Javě](https://amodb.dev/cs/java-movie-api/) nebo v [Go](https://amodb.dev/cs/go-movie-api/).

## Otázky a odpovědi

### Potřebuji balíček NuGet pro volání API z C#?

Ne. HttpClient a System.Text.Json jsou součástí .NET 8, takže tento návod nepřidává žádné balíčky. Pošlete klíč v hlavičce Authorization Bearer, nebo ho případně předejte jako parametr dotazu api_key.

### Jak v .NET přečtu hlavičku Retry-After?

Použijte response.Headers.RetryAfter. Když server pošle počet sekund, jeho vlastnost Delta drží TimeSpan. Počkejte tak dlouho, nejvýše 30 sekund, a opakujte pokus nejvýše dvakrát, než nahlásíte 429.

### Mám pro každý požadavek vytvořit nový HttpClient?

Ne. Vytvořte jednu instanci a používejte ji opakovaně, protože každá nová instance otevírá nová připojení. V ASP.NET Core zaregistrujte typovaného klienta přes AddHttpClient a vložte ho. Konzolová aplikace může držet jednu instanci po celý běh.

### Mohu znovu použít modely C# psané pro TMDB v3?

Ano. U endpointů, které amodb nabízí, cesty, parametry a pole JSON odpovídají TMDB v3, takže modely, které už máte, se deserializují beze změny. Nasměrujte základní adresu na https://api.amodb.dev/3/ a použijte klíč amodb.

### Proč karta u hodnocení vypíše not known?

Zdroj hodnocení je null, když amodb pro daný titul nemá žádné skóre. Berte null jako neznámé, nikdy jako nulu, a vypisujte jen zdroje, které jsou přítomné. Časem se mohou objevit nová pole, ignorujte proto pole, která neznáte.

## Související stránky

- [Návod na filmové API v Javě](https://amodb.dev/cs/java-movie-api/): Vytvořte v Javě průvodce epizodami s JDK HttpClient a záznamy Jackson: načtěte sezónu, respektujte Retry-After při 429, ukládejte do souborů. Java 17+.
- [API vyhledávání](https://amodb.dev/cs/search/): API vyhledávání se stejnými cestami a JSON jako TMDB v3. Najděte filmy, seriály, osoby, kolekce, společnosti a klíčová slova podle textu s filtrem roku.
- [API detailu filmu](https://amodb.dev/cs/movies/): API detailu filmu se stejnými cestami a JSON jako TMDB v3. Načtěte film podle ID, přidejte obsazení, hodnocení a nálady přes append_to_response, 14 jazyků.
- [Hodnocení filmů a seriálů](https://amodb.dev/cs/ratings/): API hodnocení filmů, které vrací skóre z IMDb, Kinopoisk, Metacritic, Rotten Tomatoes a TMDB pro titul jedním voláním, s null u každého neznámého zdroje.
- [Autentizace](https://amodb.dev/cs/authentication/): Jak poslat klíč amodb: api_key v dotazu, hlavička Authorization Bearer nebo apikey pro volání OMDb. Formát klíče, uložení, rotace a chyby 401.
- [Limity požadavků](https://amodb.dev/cs/rate-limits/): amodb omezuje klíč počtem požadavků za minutu v klouzavém okně 60 sekund: 20, 40 nebo 120 podle tarifu. Čtěte hlavičky limitu a řešte 429 pomocí Retry-After.

Aktualizováno: 2026-10-18. Source: https://amodb.dev/cs/csharp-movie-api/
