Падручнік па C# і API фільмаў для .NET 8
Гэты падручнік будуе кансольную праграму .NET 8, якая бярэ назву фільма, шукае яе ў amodb, загружае найлепшае супадзенне з рэйтынгамі і друкуе кароткую картку. Яна выкарыстоўвае толькі HttpClient і System.Text.Json, чытае ключ з AMODB_KEY, паўтарае запыт пры 429 і кэшуе адказы ў файлах. Налада займае каля 15 хвілін.
Што вы пабудуеце
Вы пабудуеце MovieLookup, кансольную праграму, якая бярэ назву з каманднага радка, шукае яе ў amodb, загружае найлепшае супадзенне з рэйтынгамі і друкуе кароткую картку са спасылкай на постэр. Вам патрэбны .NET 8 SDK, ключ amodb і каля 15 хвілін. Праграма выкарыстоўвае толькі HttpClient і System.Text.Json, таму ставіць пакеты NuGet не трэба.
Крок 1: Атрымайце ключ API
Абярыце план на старонцы цэны. Планы адрозніваюцца толькі частатой запытаў: 20, 40 або 120 у хвіліну. Стварыце ключ у асабістым кабінеце, радок з 32 малых шаснаццатковых сімвалаў. Экспартуйце яго і праверце адным выклікам:
200 азначае, што ключ працуе. 401 азначае, што ключ няправільны або адсутнічае. У PowerShell задайце зменную праз $env:AMODB_. Старонка аўтэнтыфікацыя апісвае параметр запыту api_key як альтэрнатыву загалоўку.
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"Крок 2: Стварыце праект
Шаблон .NET 8 уключае неяўныя using і nullable-тыпы спасылак. Гэта ўжо імпартуе System, System.IO, System.Linq і System.Net.Http. Астатнія радкі using вы дадаяце ў файле.
dotnet new console -n MovieLookup -f net8.0
cd MovieLookupКрок 3: Адпраўце першы запыт
Стварыце адзін HttpClient з базавым адрасам, тайм-аўтам 10 секунд і загалоўкам Bearer. Скончыце базавы адрас косай рыскай, а адносныя шляхі пішыце без пачатковай косай рыскі. Пачатковая рыска заменіць частку /3 у базавым шляху.
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);Крок 4: Разбярыце JSON у records
Адказ выкарыстоўвае назвы ў snake case, напрыклад release_date. Задайце JsonNamingPolicy.SnakeCaseLower, які з’явіўся ў .NET 8, і пішыце члены ў PascalCase. Палі, якіх вы не аб’явілі, ігнаруюцца.
Аб’ект ratings з’яўляецца, бо выклік дэталяў дадае append_. Кожная з яго пяці крыніц уяўляе сабой аб’ект з value і votes або null, калі ацэнкі няма. votes таксама можа быць null: у жывым адказе для Fight Club няма колькасці галасоў для Kinopoisk, Metacritic і Rotten Tomatoes. Усе іншыя палі пералічаны на старонцы фільмы.
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);Крок 5: Апрацоўка памылак і лімітаў
Загарніце выклік у цыкл, які рэагуе на код статусу.
| Статус | Значэнне | Што робіць праграма |
|---|---|---|
| 401 | Ключ адсутнічае, няправільны або прыпынены | Друкуе паведамленне і спыняецца, паўтор не дапаможа. |
| 402 | Тэрмін дзеяння плана скончыўся | Друкуе паведамленне і спыняецца. Прадоўжыце доступ на старонцы цэны. |
| 404 | Для гэтага id няма запісу | Паведамляе пра гэта і завяршаецца. |
| 429 | Перавышаны ліміт запытаў у хвіліну | Чакае Retry-After секунд, не болей за 30, і паўтарае не болей за двойчы. |
| 500 або 504 | Збой на баку сервера | Паведамляе статус; запусціце каманду пазней. |
Сеткавыя збоі выяўляюцца як HttpRequestException, а тайм-аўт як TaskCanceledException. Лавіце абодва на верхнім узроўні. Кожны адказ з ключом таксама нясе X-RateLimit-Remaining, таму доўгая задача можа запаволіцца да таго, як дойдзе да 429. Поўныя спісы гл. на старонках ліміты запытаў і памылкі.
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
}Крок 6: Кэшуйце адказы на дыску
Праверка ліміту запытаў выконваецца да edge-кэшу, таму адказ, выдадзены з edge, усё роўна ўлічваецца ў вашых запытах у хвіліну. Кэш у вашай праграме з’яўляецца адзіным спосабам зэканоміць запыты. Edge трымае дэталі 6 гадзін, а вынікі пошуку 10 хвілін, таму праграма выкарыстоўвае тыя ж тэрміны. Файл у часовай тэчцы свежы, калі час яго запісу знаходзіцца ў межах 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;
}
}Крок 7: Збярыце ўсё разам
Гэта поўны Program.cs. Ён дадае метад Set у кэш і клас AmodbClient, які аб’ядноўвае кэш, цыкл паўтораў і налады 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)!;
}
}
}Запусціце яго з назвай. Вось вынік для першага знойдзенага фільма, 550. Колькасць галасоў з часам расце, таму ваша будзе крыху адрознівацца.
Радок TMDB паказвае 8.4, бо код друкуе адзін знак пасля коскі ад 8.437 у ratings.tmdb.value. Хост постэраў img.amodb.dev не патрабуе ключа, таму URL постэра можна перадаць прама ў браўзер або элемент выявы. Спіс памераў гл. на старонцы выявы.
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Наступныя крокі
- Шукайце з годам або пагінацыяй у пошуку, а спісы фільтруйце праз пошук па фільтрах.
- Паказвайце кожную ацэнку і правілы для null праз рэйтынгі.
- Перанясіце кансольны код у сэрвіс ASP.NET Core з
AddHttpClientіIMemoryCache. - Параўнайце тую ж задачу на Java або Go.