API/GamesAPI.cs

API client methods for fetching games and game streams from the Lichess API. It builds HTTP requests, respects a rate limit controller, parses PGN responses into Game objects, and returns stream wrappers for real-time endpoints.

NetworkingHttp Calls
🌐 https://lichess.org/api/stream/game/, https://lichess.org/api/stream/games-by-users?ids=, https://lichess.org/api/stream/games/, game/export/, api/games/user/, api/games/export/_ids, api/games/export/import, api/tournament/{ArenaID}/games, api/swiss/{SwissID}/games, api/account/playing
#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>();
    }
}