use futures_util::stream::BoxStream;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use crate::api::gameplay::games::LichessGame;
use crate::client::LichessClient;
use crate::config::Host;
use crate::error::Result;
use crate::http;
use crate::model::{GameExportOptions, LichessColor, LichessLightUser};
#[derive(Debug)]
pub struct TvApi<'a> {
client: &'a LichessClient,
}
impl<'a> TvApi<'a> {
pub(crate) fn new(client: &'a LichessClient) -> Self {
Self { client }
}
pub async fn channels(&self) -> Result<LichessTvChannels> {
let request = self
.client
.request(Method::GET, Host::Default, "/api/tv/channels");
http::json(request, "LichessTvChannels").await
}
pub async fn feed(&self) -> Result<BoxStream<'static, Result<LichessTvFeedEvent>>> {
self.feed_at("/api/tv/feed").await
}
pub async fn channel_feed(
&self,
channel: &str,
) -> Result<BoxStream<'static, Result<LichessTvFeedEvent>>> {
let path = format!("/api/tv/{}/feed", http::segment(channel));
self.feed_at(&path).await
}
#[must_use]
pub fn channel_games(&self, channel: &'a str) -> TvChannelGamesRequest<'a> {
TvChannelGamesRequest::new(self.client, channel)
}
async fn feed_at(&self, path: &str) -> Result<BoxStream<'static, Result<LichessTvFeedEvent>>> {
let request = self.client.request(Method::GET, Host::Default, path);
http::stream(request, self.client.max_line_bytes()).await
}
}
#[derive(Debug)]
pub struct TvChannelGamesRequest<'a> {
client: &'a LichessClient,
channel: &'a str,
nb: Option<u32>,
export: GameExportOptions,
}
impl<'a> TvChannelGamesRequest<'a> {
pub(crate) fn new(client: &'a LichessClient, channel: &'a str) -> Self {
Self {
client,
channel,
nb: None,
export: GameExportOptions::default(),
}
}
#[must_use]
pub fn nb(mut self, nb: u32) -> Self {
self.nb = Some(nb);
self
}
#[must_use]
pub fn export(mut self, options: GameExportOptions) -> Self {
self.export = options;
self
}
pub async fn stream(self) -> Result<BoxStream<'static, Result<LichessGame>>> {
let request = self.request(http::ACCEPT_NDJSON);
http::stream(request, self.client.max_line_bytes()).await
}
pub async fn pgn(self) -> Result<String> {
http::text(self.request(http::ACCEPT_PGN)).await
}
fn request(&self, accept: &'static str) -> http::ApiRequest {
let path = format!("/api/tv/{}", http::segment(self.channel));
self.client
.request(Method::GET, Host::Default, &path)
.header(reqwest::header::ACCEPT, accept)
.query(&[("nb", self.nb)])
.query(&self.export)
}
}
impl LichessClient {
#[must_use]
pub fn tv(&self) -> TvApi<'_> {
TvApi::new(self)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct LichessTvGame {
pub user: LichessLightUser,
pub rating: i32,
pub game_id: String,
pub color: LichessColor,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct LichessTvChannels {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bot: Option<LichessTvGame>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub blitz: Option<LichessTvGame>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub racing_kings: Option<LichessTvGame>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ultra_bullet: Option<LichessTvGame>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bullet: Option<LichessTvGame>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub classical: Option<LichessTvGame>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub three_check: Option<LichessTvGame>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub antichess: Option<LichessTvGame>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub computer: Option<LichessTvGame>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub horde: Option<LichessTvGame>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rapid: Option<LichessTvGame>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub atomic: Option<LichessTvGame>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub crazyhouse: Option<LichessTvGame>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub chess960: Option<LichessTvGame>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub king_of_the_hill: Option<LichessTvGame>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub best: Option<LichessTvGame>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct LichessTvFeedPlayer {
pub color: LichessColor,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user: Option<LichessLightUser>,
pub rating: i32,
pub seconds: i32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct LichessTvFeatured {
pub id: String,
pub orientation: LichessColor,
pub players: Vec<LichessTvFeedPlayer>,
pub fen: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct LichessTvMove {
pub fen: String,
pub lm: String,
pub wc: i32,
pub bc: i32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "t", content = "d")]
#[non_exhaustive]
pub enum LichessTvFeedEvent {
#[serde(rename = "featured")]
Featured(LichessTvFeatured),
#[serde(rename = "fen")]
Fen(LichessTvMove),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_channels() {
let json = r#"{"bullet":{"user":{"id":"a","name":"A"},"rating":2900,
"gameId":"x","color":"white"}}"#;
let channels: LichessTvChannels = serde_json::from_str(json).unwrap();
assert_eq!(channels.bullet.unwrap().game_id, "x");
assert!(channels.blitz.is_none());
}
#[test]
fn parses_featured_feed_event() {
let json = r#"{"t":"featured","d":{"id":"g","orientation":"white",
"players":[{"color":"white","rating":1500,"seconds":60}],"fen":"startpos"}}"#;
let event: LichessTvFeedEvent = serde_json::from_str(json).unwrap();
match event {
LichessTvFeedEvent::Featured(f) => assert_eq!(f.id, "g"),
LichessTvFeedEvent::Fen(_) => panic!("expected featured"),
}
}
#[test]
fn parses_fen_feed_event() {
let json = r#"{"t":"fen","d":{"fen":"x","lm":"e2e4","wc":60,"bc":59}}"#;
let event: LichessTvFeedEvent = serde_json::from_str(json).unwrap();
match event {
LichessTvFeedEvent::Fen(m) => assert_eq!(m.lm, "e2e4"),
LichessTvFeedEvent::Featured(_) => panic!("expected fen"),
}
}
}