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. 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:
200 znamená, že klíč funguje. 401 znamená, že klíč je nesprávný nebo chybí. V PowerShellu nastavte proměnnou pomocí $env:AMODB_. Stránka autentizace popisuje parametr dotazu api_key jako alternativu k hlavičce.
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"Krok 2: Vytvořte projekt
Š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.
dotnet new console -n MovieLookup -f net8.0
cd MovieLookupKrok 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ě.
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í.
Objekt ratings se objeví, protože volání detailů přidává append_. 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.
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);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. |
| 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. |
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ů a chyby.
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
}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.
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.
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ší.
Řá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.
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.jpgDalší kroky
- Hledejte s rokem nebo stránkováním na stránce vyhledávání a filtrujte seznamy pomocí discover.
- Zobrazte každé skóre a jeho pravidla pro null na stránce hodnocení.
- Přesuňte konzolový kód do služby ASP.NET Core s
AddHttpClientaIMemoryCache. - Porovnejte stejnou úlohu v Javě nebo v Go.