(needs tripple /// not double) And clean up some commented out code. Also update caches for viaplay to make a bit more sense
391 lines
11 KiB
Rust
391 lines
11 KiB
Rust
use crate::{commands::utils, Context, Error};
|
|
use std::collections::HashMap;
|
|
|
|
use cached::proc_macro::cached;
|
|
use chrono::{DateTime, Utc};
|
|
use log::*;
|
|
use reqwest::header::{HeaderName, ACCEPT_LANGUAGE, COOKIE};
|
|
use serde::Deserialize;
|
|
|
|
use super::Timeframe;
|
|
|
|
mod es_date_time {
|
|
use chrono::{DateTime, TimeZone, Utc};
|
|
use serde::{self, Deserialize, Deserializer};
|
|
|
|
const FORMAT: &'static str = "%Y-%m-%dT%H:%M:%SZ";
|
|
|
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let s = String::deserialize(deserializer)?;
|
|
Utc.datetime_from_str(&s, FORMAT)
|
|
.map_err(serde::de::Error::custom)
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
struct VideoAttributes {
|
|
name: String,
|
|
#[serde(rename = "secondaryTitle")]
|
|
#[serde(default)]
|
|
secondary: String,
|
|
#[serde(rename = "scheduleEnd")]
|
|
#[serde(with = "es_date_time")]
|
|
schedule_end: DateTime<Utc>,
|
|
#[serde(rename = "scheduleStart")]
|
|
#[serde(with = "es_date_time")]
|
|
schedule_start: DateTime<Utc>,
|
|
#[serde(default)]
|
|
description: String,
|
|
#[serde(default)]
|
|
#[serde(rename = "broadcastType")]
|
|
broadcast_type: String,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
struct SportVal {
|
|
id: String,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
struct SportID {
|
|
data: Vec<SportVal>,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
struct VideoRelations {
|
|
#[serde(alias = "txSports")]
|
|
#[serde(alias = "txOlympicssport")]
|
|
sport_id: Option<SportID>,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
struct TaxonomyNodeAttributes {
|
|
name: String,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
#[serde(tag = "type")]
|
|
enum Includes {
|
|
#[serde(rename = "video")]
|
|
Video {
|
|
attributes: VideoAttributes,
|
|
id: String,
|
|
relationships: VideoRelations,
|
|
},
|
|
#[serde(rename = "taxonomyNode")]
|
|
TaxonomyNode {
|
|
attributes: TaxonomyNodeAttributes,
|
|
id: String,
|
|
},
|
|
#[serde(rename = "channel")]
|
|
Channels,
|
|
#[serde(rename = "collection")]
|
|
Collection,
|
|
#[serde(rename = "collectionItem")]
|
|
CollectionItem,
|
|
#[serde(rename = "image")]
|
|
Image,
|
|
#[serde(rename = "link")]
|
|
Link,
|
|
#[serde(rename = "package")]
|
|
Package,
|
|
#[serde(rename = "page")]
|
|
Page,
|
|
#[serde(rename = "pageItem")]
|
|
PageItem,
|
|
#[serde(rename = "route")]
|
|
Route,
|
|
#[serde(rename = "show")]
|
|
Show,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
struct Eurosport {
|
|
included: Vec<Includes>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ESEvents {
|
|
id: String,
|
|
sport: Option<String>,
|
|
name: String,
|
|
secondary: String,
|
|
description: String,
|
|
start: DateTime<Utc>,
|
|
end: DateTime<Utc>,
|
|
}
|
|
|
|
impl ESEvents {
|
|
pub fn filter(&self, filter: &str) -> bool {
|
|
if self.name.to_lowercase().contains(filter) {
|
|
return true;
|
|
};
|
|
if self.description.to_lowercase().contains(filter) {
|
|
return true;
|
|
};
|
|
if self.secondary.to_lowercase().contains(filter) {
|
|
return true;
|
|
};
|
|
if let Some(sport) = &self.sport {
|
|
if sport.to_lowercase().contains(filter) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
pub fn comp(&self, when: &Timeframe) -> bool {
|
|
let now = Utc::now();
|
|
match when {
|
|
Timeframe::Everything => true,
|
|
Timeframe::Current => self.start <= now && self.end >= now,
|
|
Timeframe::Future => self.end >= now,
|
|
Timeframe::Past => self.end <= now,
|
|
// _ => self.end >= now,
|
|
}
|
|
}
|
|
|
|
pub fn to_string(&self) -> String {
|
|
match &self.sport {
|
|
None => format!(
|
|
"```md\n({}) {}```(<t:{}:R>-<t:{}:R>) {}\n https://tom.al/ms/euro/{}",
|
|
self.name,
|
|
self.secondary,
|
|
self.start.timestamp(),
|
|
self.end.timestamp(),
|
|
self.description,
|
|
self.id,
|
|
),
|
|
Some(sport) => format!(
|
|
"```md\n[{}]({}) {}```(<t:{}:R>-<t:{}:R>) {}\n https://tom.al/ms/euro/{}",
|
|
sport,
|
|
self.name,
|
|
self.secondary,
|
|
self.start.timestamp(),
|
|
self.end.timestamp(),
|
|
self.description,
|
|
self.id,
|
|
),
|
|
}
|
|
}
|
|
|
|
pub fn get_key(&self) -> (DateTime<Utc>, String) {
|
|
(self.start, self.name.clone())
|
|
}
|
|
}
|
|
|
|
fn get_events(v: Eurosport) -> Result<Vec<ESEvents>, serde_json::Error> {
|
|
let mut sports: HashMap<String, String> = HashMap::new();
|
|
|
|
let nodes = v.included.iter().filter(|x| match x {
|
|
Includes::TaxonomyNode { .. } => true,
|
|
_ => false,
|
|
});
|
|
for node in nodes {
|
|
if let Includes::TaxonomyNode { attributes, id } = node {
|
|
sports.insert(id.to_string(), attributes.name.to_string());
|
|
}
|
|
}
|
|
debug!("Sports: {:?}", sports);
|
|
|
|
let mut events: Vec<ESEvents> = vec![];
|
|
|
|
let nodes = v.included.iter().filter(|x| match x {
|
|
Includes::Video { .. } => true,
|
|
_ => false,
|
|
});
|
|
for node in nodes {
|
|
if let Includes::Video {
|
|
attributes,
|
|
id,
|
|
relationships,
|
|
} = node
|
|
{
|
|
// Skip videos tagged as replay, these don't play as nicely
|
|
if attributes.broadcast_type.eq_ignore_ascii_case("replay") {
|
|
continue;
|
|
};
|
|
let sport_name = match &relationships.sport_id {
|
|
None => None,
|
|
Some(sport_id) => match sport_id.data.get(0) {
|
|
None => None,
|
|
Some(sv) => match sports.get(&sv.id) {
|
|
None => None,
|
|
Some(s) => Some(s.to_owned()),
|
|
},
|
|
},
|
|
};
|
|
events.push(ESEvents {
|
|
id: id.to_string(),
|
|
sport: sport_name,
|
|
name: attributes.name.to_string(),
|
|
secondary: attributes.secondary.to_string(),
|
|
description: attributes.description.to_string(),
|
|
start: attributes.schedule_start,
|
|
end: attributes.schedule_end,
|
|
});
|
|
}
|
|
}
|
|
events.sort_by(|a, b| b.start.cmp(&a.start));
|
|
debug!("Events: {:?}", events);
|
|
return Ok(events);
|
|
}
|
|
|
|
#[cached(time = 3600)]
|
|
#[allow(dead_code)]
|
|
pub async fn get_eurosport_events(url: String) -> Option<Vec<ESEvents>> {
|
|
let cookie = super::super::super::SETTINGS
|
|
.read()
|
|
.unwrap()
|
|
.get_table("eurosport")
|
|
.expect("Expecting an eurosport section in the config")
|
|
.get("cookie")
|
|
.expect("Config error, please set the eurosport[cookie] value")
|
|
.clone()
|
|
.into_string()
|
|
.expect("Config error, please make sure eurosport[cookie] is a string");
|
|
let client = reqwest::Client::new();
|
|
let x_disco_client = HeaderName::from_lowercase(b"x-disco-client").unwrap();
|
|
let req = client
|
|
.get(url)
|
|
.header(COOKIE, cookie)
|
|
.header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
|
|
.header(x_disco_client, "WEB:UNKNOWN:esplayer:prod")
|
|
.send()
|
|
.await;
|
|
|
|
let result: Option<Vec<ESEvents>> = match req {
|
|
Err(e) => {
|
|
warn!("Error getting Eurosport schedule {}", e);
|
|
None
|
|
}
|
|
Ok(req) if req.status().as_u16() == 404 => {
|
|
warn!("404 on getting ES events");
|
|
return None;
|
|
}
|
|
Ok(req) if req.status().as_u16() == 200 => {
|
|
let data = req.json::<Eurosport>().await;
|
|
match data {
|
|
Err(e) => {
|
|
warn!("Error getting Eurosport schedule {}", e);
|
|
None
|
|
}
|
|
Ok(es) => {
|
|
debug!("Eurosport data: {:?}", es);
|
|
match get_events(es) {
|
|
Err(e) => {
|
|
warn!("Error getting Eurosport schedule {}", e);
|
|
None
|
|
}
|
|
Ok(events) => Some(events),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(req) => {
|
|
warn!(
|
|
"Eurosport Unhandled request result {}",
|
|
req.status().as_u16()
|
|
);
|
|
None
|
|
}
|
|
};
|
|
return result;
|
|
}
|
|
|
|
/// Eurosport player events
|
|
#[poise::command(slash_command, ephemeral)]
|
|
pub async fn eurosport(
|
|
ctx: Context<'_>,
|
|
#[description = "Filter sessions for when they are/were happening, defaults to future"]
|
|
timeframe: Option<Timeframe>,
|
|
#[description = "Content to filter on"] filter: Option<String>,
|
|
) -> Result<(), Error> {
|
|
let tf = match timeframe {
|
|
None => Timeframe::Future,
|
|
Some(tf) => tf,
|
|
};
|
|
|
|
let url = super::super::super::SETTINGS
|
|
.read()
|
|
.unwrap()
|
|
.get_table("eurosport")
|
|
.expect("Expecting an eurosport section in the config")
|
|
.get("url")
|
|
.expect("Config error, please set the eurosport[url] value")
|
|
.clone()
|
|
.into_string()
|
|
.expect("Config error, please make sure eurosport[url] is a string");
|
|
let events = get_eurosport_events(url).await;
|
|
match events {
|
|
None => {
|
|
ctx.say("Oh no something went wrong").await?;
|
|
}
|
|
Some(evs) => {
|
|
info!("Found {} events from eurosport", evs.len());
|
|
let strings = evs
|
|
.into_iter()
|
|
.filter(|e| e.comp(&tf))
|
|
.filter(|e| match &filter {
|
|
None => true,
|
|
Some(f) => e.filter(f.as_str()),
|
|
})
|
|
.map(|e| e.to_string())
|
|
.collect();
|
|
let pages = utils::paginator(strings, 1900, "\n".to_string());
|
|
|
|
utils::paginate_string(ctx, pages).await?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Eurosport olympics events
|
|
#[allow(dead_code)]
|
|
pub async fn proc_olympics(
|
|
ctx: Context<'_>,
|
|
timeframe: Option<Timeframe>,
|
|
filter: Option<String>,
|
|
) -> Result<(), Error> {
|
|
let tf = match timeframe {
|
|
None => Timeframe::Future,
|
|
Some(tf) => tf,
|
|
};
|
|
let url = super::super::super::SETTINGS
|
|
.read()
|
|
.unwrap()
|
|
.get_table("eurosport")
|
|
.expect("Expecting an eurosport section in the config")
|
|
.get("olympics")
|
|
.expect("Config error, please set the eurosport[olympics] value")
|
|
.clone()
|
|
.into_string()
|
|
.expect("Config error, please make sure eurosport[olympics] is a string");
|
|
let events = get_eurosport_events(url).await;
|
|
match events {
|
|
None => {
|
|
ctx.say("Oh no something went wrong").await?;
|
|
}
|
|
Some(evs) => {
|
|
info!("Found {} events from eurosport olympics ", evs.len());
|
|
let strings = evs
|
|
.into_iter()
|
|
.filter(|e| e.comp(&tf))
|
|
.filter(|e| match &filter {
|
|
None => true,
|
|
Some(f) => e.filter(f.as_str()),
|
|
})
|
|
.map(|e| e.to_string())
|
|
.collect();
|
|
let pages = utils::paginator(strings, 1900, "\n".to_string());
|
|
|
|
utils::paginate_string(ctx, pages).await?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|