API client methods for fetching games from Lichess and creating game streams. It builds HTTP requests, enforces simple rate limit checks, parses PGN responses into Game objects, and exposes streaming helpers for various endpoints.
#nullable enable annotations
using System.Text;
using LichessNET.Entities.Game;
namespace LichessNET.API;
public partial class LichessApiClient
{
/// <summary>
/// Retrieves a chess game using its unique identifier from the Lichess API.
/// </summary>
/// <param name="gameId">The unique identifier of the game to retrieve.</param>
public async Task<Game> GetGameAsync(string gameId)
{
await _ratelimitController.Consume();
var request = GetRequestScaffold("game/export/" + gameId);
var response = await SendRequest(request);
var content = await response.Content.ReadAsStringAsync();
return Game.FromPgn(content);
}
/// <summary>
/// Retrieves a list of chess games for a specified user from the Lichess API.
/// </summary>
/// <param name="username">The username of the player whose games are to be retrieved.</param>
/// <param name="max">The maximum number of games to retrieve. Default is 10.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of games.</returns>
public async Task<List<Game>> GetGamesAsync(string username, int max = 10)
{
await _ratelimitController.Consume();
var request = GetRequestScaffold("api/games/user/" + username,
Tuple.Create("max", max.ToString()));
var response = await SendRequest(request);
var content = await response.Content.ReadAsStringAsync();
var list = new List<Game>();
var gamepgns = content.Split("\n\n\n");
foreach (var gamepgn in gamepgns)
{
try
{
if (gamepgn.Length < 10) continue;
list.Add(Game.FromPgn(gamepgn.Trim()));
}
catch (Exception)
{
_logger.Warning("Failed to parse a pgn: " + gamepgn);
throw;
}
}
return list;
}
/// <summary>
/// Retrieves multiple chess games from the Lichess API using a list of unique identifiers.
/// </summary>
/// <param name="ids">An array of unique game identifiers to retrieve.</param>
/// <returns>A list of <see cref="Game"/> objects representing the retrieved chess games.</returns>
public async Task<List<Game>> GetGamesAsync(params string[] ids)
{
var request = GetRequestScaffold("api/games/export/_ids");
request.Query["ids"] = string.Join(",", ids);
var response = await SendRequest(request, "POST");
var content = await response.Content.ReadAsStringAsync();
var list = new List<Game>();
var gamepgns = content.Split("\n\n\n");
foreach (var gamepgn in gamepgns)
{
try
{
if (gamepgn.Length < 10) continue;
list.Add(Game.FromPgn(gamepgn.Trim()));
}
catch (Exception)
{
_logger.Warning("Failed to parse a pgn: " + gamepgn);
throw;
}
}
return list;
}
/// <summary>
/// Retrieves a list of chess games that have been imported to the Lichess platform.
/// </summary>
/// <returns>A list of imported chess games.</returns>
public async Task<List<Game>> GetImportedGamesAsync()
{
await _ratelimitController.Consume();
var request = GetRequestScaffold("api/games/export/import");
var response = await SendRequest(request);
var content = await response.Content.ReadAsStringAsync();
var list = new List<Game>();
var gamepgns = content.Split("\n\n\n");
foreach (var gamepgn in gamepgns)
{
try
{
if (gamepgn.Length < 10) continue;
list.Add(Game.FromPgn(gamepgn.Trim()));
}
catch (Exception)
{
_logger.Warning("Failed to parse a pgn: " + gamepgn);
throw;
}
}
return list;
}
/// <summary>
/// Retrieves a list of chess games from a specified arena using the provided arena identifier.
/// </summary>
/// <param name="ArenaID">The unique identifier of the arena from which to retrieve the games.</param>
/// <returns>A task representing the asynchronous operation that returns a list of games retrieved from the specified arena.</returns>
public async Task<List<Game>> GetArenaGames(string ArenaID)
{
await _ratelimitController.Consume();
var request = GetRequestScaffold($"api/tournament/{ArenaID}/games");
var response = await SendRequest(request);
var content = await response.Content.ReadAsStringAsync();
var list = new List<Game>();
var gamepgns = content.Split("\n\n\n");
foreach (var gamepgn in gamepgns)
{
try
{
if (gamepgn.Length < 10) continue;
list.Add(Game.FromPgn(gamepgn.Trim()));
}
catch (Exception)
{
_logger.Warning("Failed to parse a pgn: " + gamepgn);
throw;
}
}
return list;
}
/// <summary>
/// Retrieves a list of games from a Swiss tournament using the Swiss ID from the Lichess API.
/// </summary>
/// <param name="SwissID">The unique identifier of the Swiss tournament to retrieve games from.</param>
/// <returns>A task representing the asynchronous operation, with a list of Swiss tournament games as the result.</returns>
public async Task<List<Game>> GetSwissGames(string SwissID)
{
await _ratelimitController.Consume();
var request = GetRequestScaffold($"api/swiss/{SwissID}/games");
var response = await SendRequest(request);
var content = await response.Content.ReadAsStringAsync();
var list = new List<Game>();
var gamepgns = content.Split("\n\n\n");
foreach (var gamepgn in gamepgns)
{
try
{
if (gamepgn.Length < 10) continue;
list.Add(Game.FromPgn(gamepgn.Trim()));
}
catch (Exception)
{
_logger.Warning("Failed to parse a pgn: " + gamepgn);
throw;
}
}
return list;
}
/// <summary>
/// Initializes a real-time stream of a chess game using its unique identifier from the Lichess API.
/// </summary>
/// <param name="gameId">The unique identifier of the game to stream.</param>
/// <returns>A GameStream object that provides updates as the game progresses.</returns>
public async Task<GameStream> GetGameStreamAsync(string gameId)
{
return new GameStream("https://lichess.org/api/stream/game/" + Uri.EscapeDataString(gameId), "POST");
}
/// <summary>
/// Stream the games played between a list of users, in real time.
/// Only games where both players are part of the list are included.
/// The stream emits an event each time a game is started or finished.
/// To also get all current ongoing games at the beginning of the stream, use the withCurrentGames flag.
/// </summary>
/// <param name="UserIDs"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public async Task<GameStream> GetGameStreamByUserAsync(params string[] UserIDs)
{
if (UserIDs.Length > 300) throw new Exception("Lichess only allows up to 300 users to be tracked at once.");
var users = Uri.EscapeDataString(string.Join(",", UserIDs));
return new GameStream("https://lichess.org/api/stream/games-by-users?ids=" + users, "POST");
}
public async Task<GameStream> GetGameStreamByIDsAsync(params string[] GameIDs)
{
if (GameIDs.Length > 500) throw new Exception("Lichess only allows up to 500 games to be tracked at once.");
var games = Uri.EscapeDataString(string.Join(",", GameIDs));
return new GameStream("https://lichess.org/api/stream/games/" + games, "POST");
}
/// <summary>
/// Fetches the ongoing games for the current user.
/// </summary>
/// <param name="maxGames">The maximum number of ongoing games to fetch (default is 9, max 50).</param>
/// <returns>A list of OngoingGame objects representing the current ongoing games.</returns>
public async Task<List<OngoingGame>> GetOngoingGamesAsync(int maxGames = 9)
{
await _ratelimitController.Consume("api/account", false);
if (maxGames < 1 || maxGames > 50)
throw new ArgumentOutOfRangeException(nameof(maxGames), "The number of games must be between 1 and 50.");
var request = GetRequestScaffold("api/account/playing", Tuple.Create("nb", maxGames.ToString()));
var response = await SendRequest(request);
var content = await response.Content.ReadAsStringAsync();
var json = LichessJson.Parse(content);
return json.TryGetProperty("nowPlaying", out var nowPlaying)
? nowPlaying.Deserialize<List<OngoingGame>>(LichessJson.Options) ?? new List<OngoingGame>()
: new List<OngoingGame>();
}
}