mirror of
https://codeberg.org/icewind/ugc-scaper.git
synced 2026-06-03 10:14:11 +02:00
work
This commit is contained in:
parent
0f5ea2ebda
commit
53cc7822c4
26 changed files with 31748 additions and 73 deletions
24
Cargo.lock
generated
24
Cargo.lock
generated
|
|
@ -607,29 +607,6 @@ version = "2.6.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
|
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "miette"
|
|
||||||
version = "5.10.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e"
|
|
||||||
dependencies = [
|
|
||||||
"miette-derive",
|
|
||||||
"once_cell",
|
|
||||||
"thiserror",
|
|
||||||
"unicode-width",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "miette-derive"
|
|
||||||
version = "5.10.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.39",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mime"
|
name = "mime"
|
||||||
version = "0.3.17"
|
version = "0.3.17"
|
||||||
|
|
@ -1566,7 +1543,6 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"insta",
|
"insta",
|
||||||
"main_error",
|
"main_error",
|
||||||
"miette",
|
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"scraper",
|
"scraper",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@ name = "ugc-scraper"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.67.0"
|
rust-version = "1.67.0"
|
||||||
|
description = "Scraper for ugcleague.com"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = "1.34.0"
|
tokio = "1.34.0"
|
||||||
reqwest = "0.11.22"
|
reqwest = "0.11.22"
|
||||||
scraper = "0.18.1"
|
scraper = "0.18.1"
|
||||||
miette = "5.10.0"
|
|
||||||
thiserror = "1.0.50"
|
thiserror = "1.0.50"
|
||||||
time = { version = "0.3.30", features = ["parsing", "macros"] }
|
time = { version = "0.3.30", features = ["parsing", "macros"] }
|
||||||
steamid-ng = "1.0.0"
|
steamid-ng = "1.0.0"
|
||||||
|
|
|
||||||
25
README.md
Normal file
25
README.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# ugc-scraper
|
||||||
|
|
||||||
|
*We have ugc api at home*
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use ugc_scraper::{Result, SteamID, UgcClient};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
let client = UgcClient::new();
|
||||||
|
let id = SteamID::from(76561198024494988);
|
||||||
|
let player = client.player(id).await?;
|
||||||
|
println!("{}", player.name);
|
||||||
|
for team in player.teams {
|
||||||
|
println!(
|
||||||
|
" {} playing {} since {}",
|
||||||
|
team.team.name, team.league, team.since
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
33
examples/player.rs
Normal file
33
examples/player.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
use main_error::MainResult;
|
||||||
|
use std::env::args;
|
||||||
|
use steamid_ng::SteamID;
|
||||||
|
use ugc_scraper::UgcClient;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> MainResult {
|
||||||
|
let client = UgcClient::new();
|
||||||
|
let id = args().nth(1).expect("no steam id provided");
|
||||||
|
let id = SteamID::try_from(id.as_str()).expect("invalid steam id provided");
|
||||||
|
let player = client.player(id).await?;
|
||||||
|
println!("{}", player.name);
|
||||||
|
for team in player.teams {
|
||||||
|
println!(
|
||||||
|
" {} playing {} since {}",
|
||||||
|
team.team.name, team.league, team.since
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
println!("previous teams:");
|
||||||
|
let membership = client.player_team_history(id).await?;
|
||||||
|
for team in membership {
|
||||||
|
if let Some(left) = team.left {
|
||||||
|
println!(
|
||||||
|
" {} in {} from {} till {}",
|
||||||
|
team.team.name, team.division, team.joined, left
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
17
examples/readme.rs
Normal file
17
examples/readme.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
use ugc_scraper::{Result, SteamID, UgcClient};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
let client = UgcClient::new();
|
||||||
|
let id = SteamID::from(76561198024494988);
|
||||||
|
let player = client.player(id).await?;
|
||||||
|
println!("{}", player.name);
|
||||||
|
for team in player.teams {
|
||||||
|
println!(
|
||||||
|
" {} playing {} since {}",
|
||||||
|
team.team.name, team.league, team.since
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
45
examples/team.rs
Normal file
45
examples/team.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
use main_error::MainResult;
|
||||||
|
use std::env::args;
|
||||||
|
use ugc_scraper::UgcClient;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> MainResult {
|
||||||
|
let client = UgcClient::new();
|
||||||
|
let id = args().nth(1).expect("no team id provided");
|
||||||
|
let id = id.parse().expect("invalid team id provided");
|
||||||
|
let team = client.team(id).await?;
|
||||||
|
println!("{} - {}", team.tag, team.name);
|
||||||
|
println!("playing {} in {}", team.format, team.division);
|
||||||
|
println!();
|
||||||
|
println!("with: ");
|
||||||
|
for member in team.members {
|
||||||
|
println!(" {} since {}", member.name, member.since);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
println!("previous players ");
|
||||||
|
let roster_history = client.team_roster_history(id).await?;
|
||||||
|
for roster_item in roster_history {
|
||||||
|
if let Some(left) = roster_item.left {
|
||||||
|
println!(
|
||||||
|
" {} joined at {} and left at {}",
|
||||||
|
roster_item.name, roster_item.joined, left
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
println!("name changes:");
|
||||||
|
for name_change in team.name_changes {
|
||||||
|
println!(
|
||||||
|
" {} - {} to {} - {} at {}",
|
||||||
|
name_change.from_tag,
|
||||||
|
name_change.from,
|
||||||
|
name_change.to_tag,
|
||||||
|
name_change.to,
|
||||||
|
name_change.date
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
use main_error::MainResult;
|
|
||||||
use std::env::args;
|
|
||||||
use steamid_ng::SteamID;
|
|
||||||
use ugc_scraper::UgcClient;
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> MainResult {
|
|
||||||
let client = UgcClient::new();
|
|
||||||
let id = args().nth(1).expect("no steam id provided");
|
|
||||||
let id = SteamID::try_from(id.as_str()).expect("invalid steam id provided");
|
|
||||||
let player = client.player(id).await?;
|
|
||||||
dbg!(player.teams);
|
|
||||||
|
|
||||||
let membership = client.player_team_history(id).await?;
|
|
||||||
dbg!(membership);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
93
src/data.rs
93
src/data.rs
|
|
@ -1,5 +1,5 @@
|
||||||
use steamid_ng::SteamID;
|
pub use steamid_ng::SteamID;
|
||||||
use time::Date;
|
use time::{Date, OffsetDateTime};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||||
|
|
@ -42,3 +42,92 @@ pub struct MembershipHistory {
|
||||||
pub joined: Date,
|
pub joined: Date,
|
||||||
pub left: Option<Date>,
|
pub left: Option<Date>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||||
|
pub struct Team {
|
||||||
|
pub name: String,
|
||||||
|
pub tag: String,
|
||||||
|
pub image: String,
|
||||||
|
pub format: String,
|
||||||
|
pub timezone: String,
|
||||||
|
pub division: String,
|
||||||
|
pub description: String,
|
||||||
|
pub titles: Vec<String>,
|
||||||
|
pub members: Vec<Membership>,
|
||||||
|
pub results: Vec<Record>,
|
||||||
|
pub name_changes: Vec<NameChange>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||||
|
pub struct NameChange {
|
||||||
|
pub from_tag: String,
|
||||||
|
pub from: String,
|
||||||
|
pub to_tag: String,
|
||||||
|
pub to: String,
|
||||||
|
pub date: Date,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||||
|
pub struct Membership {
|
||||||
|
pub name: String,
|
||||||
|
pub steam_id: SteamID,
|
||||||
|
pub role: String,
|
||||||
|
pub since: OffsetDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||||
|
pub struct Record {
|
||||||
|
pub season: u32,
|
||||||
|
pub division: String,
|
||||||
|
pub wins: u8,
|
||||||
|
pub losses: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||||
|
pub struct RosterHistory {
|
||||||
|
pub name: String,
|
||||||
|
pub steam_id: SteamID,
|
||||||
|
pub joined: Date,
|
||||||
|
pub left: Option<Date>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||||
|
pub struct TeamSeason {
|
||||||
|
pub season: u32,
|
||||||
|
pub matches: Vec<TeamSeasonMatch>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||||
|
pub struct TeamSeasonMatch {
|
||||||
|
pub division: String,
|
||||||
|
pub week: u8,
|
||||||
|
pub date: String,
|
||||||
|
pub side: String,
|
||||||
|
pub result: MatchResult,
|
||||||
|
pub map: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||||
|
pub enum MatchResult {
|
||||||
|
Played {
|
||||||
|
opponent: TeamRef,
|
||||||
|
score: u8,
|
||||||
|
score_opponent: u8,
|
||||||
|
match_points: f32,
|
||||||
|
match_points_opponent: f32,
|
||||||
|
},
|
||||||
|
Pending {
|
||||||
|
opponent: TeamRef,
|
||||||
|
score: u8,
|
||||||
|
score_opponent: u8,
|
||||||
|
},
|
||||||
|
ByeWeek,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,14 @@
|
||||||
use miette::Diagnostic;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Debug, Error, Diagnostic)]
|
#[derive(Debug, Error)]
|
||||||
pub enum ScrapeError {
|
pub enum ScrapeError {
|
||||||
#[error("Failed to request data: {0:#}")]
|
#[error("Failed to request data: {0:#}")]
|
||||||
Request(#[from] reqwest::Error),
|
Request(#[from] reqwest::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
#[diagnostic(transparent)]
|
|
||||||
Parse(#[from] ParseError),
|
Parse(#[from] ParseError),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error, Diagnostic, Clone)]
|
#[derive(Debug, Error, Clone)]
|
||||||
pub enum ParseError {
|
pub enum ParseError {
|
||||||
#[error("Couldn't find expected element '{selector}' for {role}")]
|
#[error("Couldn't find expected element '{selector}' for {role}")]
|
||||||
ElementNotFound {
|
ElementNotFound {
|
||||||
|
|
@ -22,6 +20,8 @@ pub enum ParseError {
|
||||||
selector: &'static str,
|
selector: &'static str,
|
||||||
role: &'static str,
|
role: &'static str,
|
||||||
},
|
},
|
||||||
|
#[error("Invalid text for {role}: {text}")]
|
||||||
|
InvalidText { text: String, role: &'static str },
|
||||||
#[error("Invalid link for {role}: {link}")]
|
#[error("Invalid link for {role}: {link}")]
|
||||||
InvalidLink { link: String, role: &'static str },
|
InvalidLink { link: String, role: &'static str },
|
||||||
#[error("Invalid date for {role}: {date}")]
|
#[error("Invalid date for {role}: {date}")]
|
||||||
|
|
|
||||||
63
src/lib.rs
63
src/lib.rs
|
|
@ -3,11 +3,14 @@ mod error;
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
|
|
||||||
use crate::data::{MembershipHistory, Player};
|
use crate::data::{MembershipHistory, Player, RosterHistory, Team, TeamSeason};
|
||||||
use crate::parser::{Parser, PlayerDetailsParser, PlayerParser};
|
use crate::parser::{
|
||||||
|
Parser, PlayerDetailsParser, PlayerParser, TeamMatchesParser, TeamParser,
|
||||||
|
TeamRosterHistoryParser,
|
||||||
|
};
|
||||||
pub use error::*;
|
pub use error::*;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use steamid_ng::SteamID;
|
pub use steamid_ng::SteamID;
|
||||||
|
|
||||||
pub type Result<T, E = ScrapeError> = std::result::Result<T, E>;
|
pub type Result<T, E = ScrapeError> = std::result::Result<T, E>;
|
||||||
|
|
||||||
|
|
@ -16,17 +19,25 @@ pub struct UgcClient {
|
||||||
client: Client,
|
client: Client,
|
||||||
player_parser: PlayerParser,
|
player_parser: PlayerParser,
|
||||||
player_detail_parser: PlayerDetailsParser,
|
player_detail_parser: PlayerDetailsParser,
|
||||||
|
team_parser: TeamParser,
|
||||||
|
team_roster_history_parser: TeamRosterHistoryParser,
|
||||||
|
team_matches_parser: TeamMatchesParser,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// "API client" for ugc by scraping the website
|
||||||
impl UgcClient {
|
impl UgcClient {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
UgcClient {
|
UgcClient {
|
||||||
client: Client::default(),
|
client: Client::default(),
|
||||||
player_parser: PlayerParser::new(),
|
player_parser: PlayerParser::new(),
|
||||||
player_detail_parser: PlayerDetailsParser::new(),
|
player_detail_parser: PlayerDetailsParser::new(),
|
||||||
|
team_parser: TeamParser::new(),
|
||||||
|
team_roster_history_parser: TeamRosterHistoryParser::new(),
|
||||||
|
team_matches_parser: TeamMatchesParser::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieve player information
|
||||||
pub async fn player(&self, steam_id: SteamID) -> Result<Player> {
|
pub async fn player(&self, steam_id: SteamID) -> Result<Player> {
|
||||||
let body = self
|
let body = self
|
||||||
.client
|
.client
|
||||||
|
|
@ -41,6 +52,7 @@ impl UgcClient {
|
||||||
self.player_parser.parse(&body)
|
self.player_parser.parse(&body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieve team membership history for a player
|
||||||
pub async fn player_team_history(&self, steam_id: SteamID) -> Result<Vec<MembershipHistory>> {
|
pub async fn player_team_history(&self, steam_id: SteamID) -> Result<Vec<MembershipHistory>> {
|
||||||
let body = self
|
let body = self
|
||||||
.client
|
.client
|
||||||
|
|
@ -54,4 +66,49 @@ impl UgcClient {
|
||||||
.await?;
|
.await?;
|
||||||
self.player_detail_parser.parse(&body)
|
self.player_detail_parser.parse(&body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieve team information
|
||||||
|
pub async fn team(&self, id: u32) -> Result<Team> {
|
||||||
|
let body = self
|
||||||
|
.client
|
||||||
|
.get(&format!(
|
||||||
|
"https://www.ugcleague.com/team_page.cfm?clan_id={}",
|
||||||
|
id
|
||||||
|
))
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.text()
|
||||||
|
.await?;
|
||||||
|
self.team_parser.parse(&body)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve team roster history
|
||||||
|
pub async fn team_roster_history(&self, id: u32) -> Result<Vec<RosterHistory>> {
|
||||||
|
let body = self
|
||||||
|
.client
|
||||||
|
.get(&format!(
|
||||||
|
"https://www.ugcleague.com/team_page_rosterhistory.cfm?clan_id={}",
|
||||||
|
id
|
||||||
|
))
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.text()
|
||||||
|
.await?;
|
||||||
|
self.team_roster_history_parser.parse(&body)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve team match history
|
||||||
|
pub async fn team_matches(&self, id: u32) -> Result<Vec<TeamSeason>> {
|
||||||
|
let body = self
|
||||||
|
.client
|
||||||
|
.get(&format!(
|
||||||
|
"https://www.ugcleague.com/team_page_matches.cfm?clan_id={}",
|
||||||
|
id
|
||||||
|
))
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.text()
|
||||||
|
.await?;
|
||||||
|
self.team_matches_parser.parse(&body)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,20 @@
|
||||||
use crate::{ParseError, Result};
|
use crate::{ParseError, Result};
|
||||||
use scraper::{ElementRef, Selector};
|
use scraper::{ElementRef, Selector};
|
||||||
|
use steamid_ng::SteamID;
|
||||||
use time::format_description::FormatItem;
|
use time::format_description::FormatItem;
|
||||||
use time::macros::format_description;
|
use time::macros::format_description;
|
||||||
|
|
||||||
mod player;
|
mod player;
|
||||||
mod player_details;
|
mod player_details;
|
||||||
|
mod team;
|
||||||
|
mod team_matches;
|
||||||
|
mod team_roster_history;
|
||||||
|
|
||||||
pub use player::*;
|
pub use player::*;
|
||||||
pub use player_details::*;
|
pub use player_details::*;
|
||||||
|
pub use team::*;
|
||||||
|
pub use team_matches::*;
|
||||||
|
pub use team_roster_history::*;
|
||||||
|
|
||||||
pub trait Parser {
|
pub trait Parser {
|
||||||
type Output;
|
type Output;
|
||||||
|
|
@ -47,6 +54,11 @@ fn select_last_text<'a>(el: ElementRef<'a>, selector: &Selector) -> Option<&'a s
|
||||||
|
|
||||||
const DATE_FORMAT: &[FormatItem<'static>] =
|
const DATE_FORMAT: &[FormatItem<'static>] =
|
||||||
format_description!("[month padding:none]/[day padding:none]/[year]");
|
format_description!("[month padding:none]/[day padding:none]/[year]");
|
||||||
|
const MEMBER_DATE_FORMAT: &[FormatItem<'static>] = format_description!(
|
||||||
|
"[month repr:short] [day padding:none], [year]\n/\n[hour padding:none]:[minute] [period]\n(ET)"
|
||||||
|
);
|
||||||
|
const ROSTER_HISTORY_DATE_FORMAT: &[FormatItem<'static>] =
|
||||||
|
format_description!("[month repr:short] [day padding:none], [year]");
|
||||||
|
|
||||||
fn team_id_from_link(link: &str) -> Result<u32, ParseError> {
|
fn team_id_from_link(link: &str) -> Result<u32, ParseError> {
|
||||||
link.rsplit_once('=')
|
link.rsplit_once('=')
|
||||||
|
|
@ -56,3 +68,13 @@ fn team_id_from_link(link: &str) -> Result<u32, ParseError> {
|
||||||
role: "team id",
|
role: "team id",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn steam_id_from_link(link: &str) -> Result<SteamID, ParseError> {
|
||||||
|
link.rsplit_once('=')
|
||||||
|
.and_then(|part| part.1.parse::<u64>().ok())
|
||||||
|
.ok_or_else(|| ParseError::InvalidLink {
|
||||||
|
link: link.to_string(),
|
||||||
|
role: "user id",
|
||||||
|
})
|
||||||
|
.map(SteamID::from)
|
||||||
|
}
|
||||||
|
|
|
||||||
341
src/parser/team.rs
Normal file
341
src/parser/team.rs
Normal file
|
|
@ -0,0 +1,341 @@
|
||||||
|
use super::{ElementExt, Parser};
|
||||||
|
use crate::data::{Membership, NameChange, Record, Team};
|
||||||
|
use crate::parser::{select_text, steam_id_from_link, DATE_FORMAT, MEMBER_DATE_FORMAT};
|
||||||
|
use crate::{ParseError, Result};
|
||||||
|
use scraper::{Html, Selector};
|
||||||
|
use time::{Date, PrimitiveDateTime, UtcOffset};
|
||||||
|
|
||||||
|
const SELECTOR_TEAM_NAME: &str = ".container .col-md-12 h1 > b";
|
||||||
|
const SELECTOR_TEAM_TAG: &str = ".container .col-md-12 h1 > span";
|
||||||
|
const SELECTOR_TEAM_IMAGE: &str = ".container .col-md-12 a > img";
|
||||||
|
|
||||||
|
const SELECTOR_TEAM_FORMAT: &str = ".container .col-md-3 .white-row-small h5 .text-danger b";
|
||||||
|
const SELECTOR_TEAM_DIVISION: &str = ".container .col-md-3 .white-row-small h5 > b";
|
||||||
|
const SELECTOR_TEAM_TIMEZONE: &str = ".container .col-md-3 .white-row-small p > small > b";
|
||||||
|
const SELECTOR_TEAM_DESCRIPTION: &str =
|
||||||
|
".container .col-md-3 .white-row-small p:nth-child(4) > small";
|
||||||
|
const SELECTOR_TEAM_TITLES: &str = ".container .col-md-3 .white-row-small p > .text-warning";
|
||||||
|
|
||||||
|
const SELECTOR_TEAM_MEMBER_ROW: &str =
|
||||||
|
".container .white-row-small > .row-fluid > .col-md-12 > .white-row-light-small";
|
||||||
|
const SELECTOR_TEAM_MEMBER_LINK: &str = "b > a[href^=\"players_page\"]";
|
||||||
|
const SELECTOR_TEAM_MEMBER_ROLE: &str = ".tinytext";
|
||||||
|
const SELECTOR_TEAM_MEMBER_SINCE: &str = ".tinytext > em";
|
||||||
|
|
||||||
|
const SELECTOR_TEAM_RECORDS: &str =
|
||||||
|
".container .col-md-3 .white-row-small .table-responsive > table tbody tr";
|
||||||
|
const SELECTOR_TEAM_RECORD_SEASON: &str = "td:nth-child(1) small span b";
|
||||||
|
const SELECTOR_TEAM_RECORD_DIVISION: &str = "td:nth-child(2) small";
|
||||||
|
const SELECTOR_TEAM_RECORD_RESULT: &str = "td:nth-child(3)";
|
||||||
|
|
||||||
|
const SELECTOR_TEAM_NAME_CHANGE: &str =
|
||||||
|
".white-row-small:nth-child(3) .table-responsive table tbody tr";
|
||||||
|
const SELECTOR_TEAM_NAME_FROM_TAG: &str = "td:nth-child(1) small";
|
||||||
|
const SELECTOR_TEAM_NAME_FROM_NAME: &str = "td:nth-child(2) small";
|
||||||
|
const SELECTOR_TEAM_NAME_TO_TAG: &str = "td:nth-child(3) small";
|
||||||
|
const SELECTOR_TEAM_NAME_TO_NAME: &str = "td:nth-child(4) small";
|
||||||
|
const SELECTOR_TEAM_NAME_DATE: &str = "td:nth-child(5) small";
|
||||||
|
|
||||||
|
pub struct TeamParser {
|
||||||
|
selector_name: Selector,
|
||||||
|
selector_tag: Selector,
|
||||||
|
selector_image: Selector,
|
||||||
|
|
||||||
|
selector_team_format: Selector,
|
||||||
|
selector_team_division: Selector,
|
||||||
|
selector_team_timezone: Selector,
|
||||||
|
selector_team_description: Selector,
|
||||||
|
selector_team_titles: Selector,
|
||||||
|
|
||||||
|
selector_team_member_row: Selector,
|
||||||
|
selector_team_member_link: Selector,
|
||||||
|
selector_team_member_role: Selector,
|
||||||
|
selector_team_member_since: Selector,
|
||||||
|
|
||||||
|
selector_team_records: Selector,
|
||||||
|
selector_team_record_season: Selector,
|
||||||
|
selector_team_record_division: Selector,
|
||||||
|
selector_team_record_result: Selector,
|
||||||
|
|
||||||
|
selector_team_name_item: Selector,
|
||||||
|
selector_team_name_from_tag: Selector,
|
||||||
|
selector_team_name_from_name: Selector,
|
||||||
|
selector_team_name_to_tag: Selector,
|
||||||
|
selector_team_name_to_name: Selector,
|
||||||
|
selector_team_name_date: Selector,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TeamParser {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TeamParser {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
TeamParser {
|
||||||
|
selector_name: Selector::parse(SELECTOR_TEAM_NAME).unwrap(),
|
||||||
|
selector_tag: Selector::parse(SELECTOR_TEAM_TAG).unwrap(),
|
||||||
|
selector_image: Selector::parse(SELECTOR_TEAM_IMAGE).unwrap(),
|
||||||
|
|
||||||
|
selector_team_format: Selector::parse(SELECTOR_TEAM_FORMAT).unwrap(),
|
||||||
|
selector_team_division: Selector::parse(SELECTOR_TEAM_DIVISION).unwrap(),
|
||||||
|
selector_team_timezone: Selector::parse(SELECTOR_TEAM_TIMEZONE).unwrap(),
|
||||||
|
selector_team_description: Selector::parse(SELECTOR_TEAM_DESCRIPTION).unwrap(),
|
||||||
|
selector_team_titles: Selector::parse(SELECTOR_TEAM_TITLES).unwrap(),
|
||||||
|
|
||||||
|
selector_team_member_row: Selector::parse(SELECTOR_TEAM_MEMBER_ROW).unwrap(),
|
||||||
|
selector_team_member_link: Selector::parse(SELECTOR_TEAM_MEMBER_LINK).unwrap(),
|
||||||
|
selector_team_member_role: Selector::parse(SELECTOR_TEAM_MEMBER_ROLE).unwrap(),
|
||||||
|
selector_team_member_since: Selector::parse(SELECTOR_TEAM_MEMBER_SINCE).unwrap(),
|
||||||
|
|
||||||
|
selector_team_records: Selector::parse(SELECTOR_TEAM_RECORDS).unwrap(),
|
||||||
|
selector_team_record_season: Selector::parse(SELECTOR_TEAM_RECORD_SEASON).unwrap(),
|
||||||
|
selector_team_record_division: Selector::parse(SELECTOR_TEAM_RECORD_DIVISION).unwrap(),
|
||||||
|
selector_team_record_result: Selector::parse(SELECTOR_TEAM_RECORD_RESULT).unwrap(),
|
||||||
|
|
||||||
|
selector_team_name_item: Selector::parse(SELECTOR_TEAM_NAME_CHANGE).unwrap(),
|
||||||
|
selector_team_name_from_tag: Selector::parse(SELECTOR_TEAM_NAME_FROM_TAG).unwrap(),
|
||||||
|
selector_team_name_from_name: Selector::parse(SELECTOR_TEAM_NAME_FROM_NAME).unwrap(),
|
||||||
|
selector_team_name_to_tag: Selector::parse(SELECTOR_TEAM_NAME_TO_TAG).unwrap(),
|
||||||
|
selector_team_name_to_name: Selector::parse(SELECTOR_TEAM_NAME_TO_NAME).unwrap(),
|
||||||
|
selector_team_name_date: Selector::parse(SELECTOR_TEAM_NAME_DATE).unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parser for TeamParser {
|
||||||
|
type Output = Team;
|
||||||
|
|
||||||
|
fn parse(&self, document: &str) -> Result<Self::Output> {
|
||||||
|
let document = Html::parse_document(document);
|
||||||
|
let root = document.root_element();
|
||||||
|
let name = select_text(root, &self.selector_name)
|
||||||
|
.ok_or(ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_TEAM_NAME,
|
||||||
|
role: "team name",
|
||||||
|
})?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let tag = select_text(root, &self.selector_tag)
|
||||||
|
.ok_or(ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_TEAM_TAG,
|
||||||
|
role: "team tag",
|
||||||
|
})?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let image =
|
||||||
|
document
|
||||||
|
.select(&self.selector_image)
|
||||||
|
.next()
|
||||||
|
.ok_or(ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_TEAM_IMAGE,
|
||||||
|
role: "team image",
|
||||||
|
})?;
|
||||||
|
let image = image
|
||||||
|
.attr("data-cfsrc")
|
||||||
|
.or_else(|| image.attr("src"))
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let format = select_text(root, &self.selector_team_format)
|
||||||
|
.ok_or(ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_TEAM_FORMAT,
|
||||||
|
role: "team format",
|
||||||
|
})?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let division = select_text(root, &self.selector_team_division)
|
||||||
|
.ok_or(ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_TEAM_DIVISION,
|
||||||
|
role: "team division",
|
||||||
|
})?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let timezone = select_text(root, &self.selector_team_timezone)
|
||||||
|
.ok_or(ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_TEAM_TIMEZONE,
|
||||||
|
role: "team timzone",
|
||||||
|
})?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let description = select_text(root, &self.selector_team_description)
|
||||||
|
.ok_or(ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_TEAM_DESCRIPTION,
|
||||||
|
role: "team description",
|
||||||
|
})?
|
||||||
|
.replace('\n', " ");
|
||||||
|
|
||||||
|
let titles = document
|
||||||
|
.select(&self.selector_team_titles)
|
||||||
|
.next()
|
||||||
|
.ok_or(ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_TEAM_TITLES,
|
||||||
|
role: "team titles",
|
||||||
|
})?
|
||||||
|
.text()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(String::from)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let results = document
|
||||||
|
.select(&self.selector_team_records)
|
||||||
|
.map(|record| {
|
||||||
|
let season = select_text(record, &self.selector_team_record_season).ok_or(
|
||||||
|
ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_TEAM_RECORD_SEASON,
|
||||||
|
role: "team record season",
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
let division = select_text(record, &self.selector_team_record_division)
|
||||||
|
.ok_or(ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_TEAM_RECORD_DIVISION,
|
||||||
|
role: "team record division",
|
||||||
|
})?
|
||||||
|
.to_string();
|
||||||
|
let result = select_text(record, &self.selector_team_record_result).ok_or(
|
||||||
|
ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_TEAM_RECORD_RESULT,
|
||||||
|
role: "team record result",
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let (wins, losses) =
|
||||||
|
result
|
||||||
|
.split_once('-')
|
||||||
|
.ok_or_else(|| ParseError::InvalidText {
|
||||||
|
text: result.to_string(),
|
||||||
|
role: "team record result",
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Record {
|
||||||
|
season: season.parse().map_err(|_| ParseError::InvalidText {
|
||||||
|
text: season.to_string(),
|
||||||
|
role: "team record season",
|
||||||
|
})?,
|
||||||
|
division,
|
||||||
|
wins: wins.parse().map_err(|_| ParseError::InvalidText {
|
||||||
|
text: wins.to_string(),
|
||||||
|
role: "team record wins",
|
||||||
|
})?,
|
||||||
|
losses: losses.parse().map_err(|_| ParseError::InvalidText {
|
||||||
|
text: losses.to_string(),
|
||||||
|
role: "team record losses",
|
||||||
|
})?,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>>>()?;
|
||||||
|
|
||||||
|
let members = document
|
||||||
|
.select(&self.selector_team_member_row)
|
||||||
|
.map(|row| {
|
||||||
|
let link = row.select(&self.selector_team_member_link).next().ok_or(
|
||||||
|
ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_TEAM_MEMBER_LINK,
|
||||||
|
role: "team member link",
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
let name = link
|
||||||
|
.first_text()
|
||||||
|
.ok_or(ParseError::EmptyText {
|
||||||
|
selector: SELECTOR_TEAM_MEMBER_LINK,
|
||||||
|
role: "team member link",
|
||||||
|
})?
|
||||||
|
.to_string();
|
||||||
|
let link = link.attr("href").unwrap_or_default();
|
||||||
|
|
||||||
|
let role = select_text(row, &self.selector_team_member_role)
|
||||||
|
.ok_or(ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_TEAM_MEMBER_ROLE,
|
||||||
|
role: "team member role",
|
||||||
|
})?
|
||||||
|
.split('\n')
|
||||||
|
.next()
|
||||||
|
.unwrap();
|
||||||
|
let since = select_text(row, &self.selector_team_member_since).ok_or(
|
||||||
|
ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_TEAM_MEMBER_SINCE,
|
||||||
|
role: "team member since",
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
let role = role.trim().to_string();
|
||||||
|
let since = since.trim();
|
||||||
|
let since = PrimitiveDateTime::parse(since, MEMBER_DATE_FORMAT)
|
||||||
|
.map_err(|_| ParseError::InvalidDate {
|
||||||
|
role: "member join date",
|
||||||
|
date: since.to_string(),
|
||||||
|
})?
|
||||||
|
.assume_offset(UtcOffset::from_hms(-5, 0, 0).unwrap());
|
||||||
|
|
||||||
|
Ok(Membership {
|
||||||
|
name,
|
||||||
|
steam_id: steam_id_from_link(link)?,
|
||||||
|
role,
|
||||||
|
since,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>>>()?;
|
||||||
|
|
||||||
|
let name_changes = document
|
||||||
|
.select(&self.selector_team_name_item)
|
||||||
|
.map(|row| {
|
||||||
|
let from_tag = select_text(row, &self.selector_team_name_from_tag).ok_or(
|
||||||
|
ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_TEAM_NAME_FROM_TAG,
|
||||||
|
role: "team name change from tag",
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
let from_name = select_text(row, &self.selector_team_name_from_name).ok_or(
|
||||||
|
ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_TEAM_NAME_FROM_NAME,
|
||||||
|
role: "team name change from name",
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
let to_tag = select_text(row, &self.selector_team_name_to_tag).ok_or(
|
||||||
|
ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_TEAM_NAME_TO_TAG,
|
||||||
|
role: "team name change to tag",
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
let to_name = select_text(row, &self.selector_team_name_to_name).ok_or(
|
||||||
|
ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_TEAM_NAME_TO_NAME,
|
||||||
|
role: "team name change from name",
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
let date = select_text(row, &self.selector_team_name_date).ok_or(
|
||||||
|
ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_TEAM_NAME_DATE,
|
||||||
|
role: "team name change date",
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
let date = Date::parse(date, DATE_FORMAT).map_err(|_| ParseError::InvalidDate {
|
||||||
|
date: date.to_string(),
|
||||||
|
role: "team name change date",
|
||||||
|
})?;
|
||||||
|
Ok(NameChange {
|
||||||
|
from_tag: from_tag.to_string(),
|
||||||
|
from: from_name.to_string(),
|
||||||
|
to_tag: to_tag.to_string(),
|
||||||
|
to: to_name.to_string(),
|
||||||
|
date,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<_>>()?;
|
||||||
|
|
||||||
|
Ok(Team {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
division,
|
||||||
|
timezone,
|
||||||
|
format,
|
||||||
|
image,
|
||||||
|
tag,
|
||||||
|
titles,
|
||||||
|
results,
|
||||||
|
members,
|
||||||
|
name_changes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
212
src/parser/team_matches.rs
Normal file
212
src/parser/team_matches.rs
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
use super::Parser;
|
||||||
|
use crate::data::{MatchResult, TeamRef, TeamSeason, TeamSeasonMatch};
|
||||||
|
use crate::parser::{select_text, team_id_from_link, ElementExt};
|
||||||
|
use crate::{ParseError, Result};
|
||||||
|
use scraper::{Html, Selector};
|
||||||
|
|
||||||
|
const SELECTOR_SEASON_TITLE: &str =
|
||||||
|
".container table.table.table-condensed.table-striped thead h4 b";
|
||||||
|
const SELECTOR_SEASON_MATCHES: &str =
|
||||||
|
".container table.table.table-condensed.table-striped tbody:nth-child(3n)";
|
||||||
|
const SELECTOR_SEASON_MATCH: &str = "tr:not(:last-child)";
|
||||||
|
const SELECTOR_SEASON_DIVISION: &str = "td:nth-child(1) small";
|
||||||
|
const SELECTOR_SEASON_WEEK: &str = "td:nth-child(2) small";
|
||||||
|
const SELECTOR_SEASON_DATE: &str = "td:nth-child(3) small";
|
||||||
|
const SELECTOR_SEASON_SIDE: &str = "td:nth-child(4) small";
|
||||||
|
const SELECTOR_SEASON_OPPONENT: &str = "td:nth-child(6) a";
|
||||||
|
const SELECTOR_SEASON_MAP: &str = "td:nth-child(7)";
|
||||||
|
const SELECTOR_SEASON_SCORES: &str = "td:nth-child(8)";
|
||||||
|
const SELECTOR_SEASON_POINTS: &str = "td:nth-child(9) small";
|
||||||
|
const SELECTOR_SEASON_POINTS_OPPONENTS: &str = "td:nth-child(10) small";
|
||||||
|
|
||||||
|
pub struct TeamMatchesParser {
|
||||||
|
selector_title: Selector,
|
||||||
|
selector_matches: Selector,
|
||||||
|
selector_match: Selector,
|
||||||
|
selector_division: Selector,
|
||||||
|
selector_week: Selector,
|
||||||
|
selector_date: Selector,
|
||||||
|
selector_side: Selector,
|
||||||
|
selector_opponent: Selector,
|
||||||
|
selector_map: Selector,
|
||||||
|
selector_scores: Selector,
|
||||||
|
selector_points: Selector,
|
||||||
|
selector_points_opponent: Selector,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TeamMatchesParser {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TeamMatchesParser {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
TeamMatchesParser {
|
||||||
|
selector_title: Selector::parse(SELECTOR_SEASON_TITLE).unwrap(),
|
||||||
|
selector_matches: Selector::parse(SELECTOR_SEASON_MATCHES).unwrap(),
|
||||||
|
selector_match: Selector::parse(SELECTOR_SEASON_MATCH).unwrap(),
|
||||||
|
selector_division: Selector::parse(SELECTOR_SEASON_DIVISION).unwrap(),
|
||||||
|
selector_week: Selector::parse(SELECTOR_SEASON_WEEK).unwrap(),
|
||||||
|
selector_date: Selector::parse(SELECTOR_SEASON_DATE).unwrap(),
|
||||||
|
selector_side: Selector::parse(SELECTOR_SEASON_SIDE).unwrap(),
|
||||||
|
selector_opponent: Selector::parse(SELECTOR_SEASON_OPPONENT).unwrap(),
|
||||||
|
selector_map: Selector::parse(SELECTOR_SEASON_MAP).unwrap(),
|
||||||
|
selector_scores: Selector::parse(SELECTOR_SEASON_SCORES).unwrap(),
|
||||||
|
selector_points: Selector::parse(SELECTOR_SEASON_POINTS).unwrap(),
|
||||||
|
selector_points_opponent: Selector::parse(SELECTOR_SEASON_POINTS_OPPONENTS).unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parser for TeamMatchesParser {
|
||||||
|
type Output = Vec<TeamSeason>;
|
||||||
|
|
||||||
|
fn parse(&self, document: &str) -> Result<Self::Output> {
|
||||||
|
let document = Html::parse_document(document);
|
||||||
|
|
||||||
|
document
|
||||||
|
.select(&self.selector_title)
|
||||||
|
.zip(document.select(&self.selector_matches))
|
||||||
|
.map(|(title, matches)| {
|
||||||
|
let title = title.first_text().ok_or(ParseError::EmptyText {
|
||||||
|
selector: SELECTOR_SEASON_TITLE,
|
||||||
|
role: "season title",
|
||||||
|
})?;
|
||||||
|
let season: u32 = title.trim_start_matches("Season ").parse().map_err(|_| {
|
||||||
|
ParseError::InvalidText {
|
||||||
|
text: title.to_string(),
|
||||||
|
role: "season title",
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let matches = matches
|
||||||
|
.select(&self.selector_match)
|
||||||
|
.map(|game| {
|
||||||
|
let division = select_text(game, &self.selector_division).ok_or(
|
||||||
|
ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_SEASON_DIVISION,
|
||||||
|
role: "match division",
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
let week = select_text(game, &self.selector_week).ok_or(
|
||||||
|
ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_SEASON_WEEK,
|
||||||
|
role: "match week",
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
let week = week.parse().map_err(|_| ParseError::InvalidText {
|
||||||
|
text: week.to_string(),
|
||||||
|
role: "match week",
|
||||||
|
})?;
|
||||||
|
let date = select_text(game, &self.selector_date).ok_or(
|
||||||
|
ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_SEASON_DATE,
|
||||||
|
role: "match date",
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
let side = select_text(game, &self.selector_side).ok_or(
|
||||||
|
ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_SEASON_SIDE,
|
||||||
|
role: "match side",
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
let opponent_link = game.select(&self.selector_opponent).next();
|
||||||
|
let map = select_text(game, &self.selector_map).ok_or(
|
||||||
|
ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_SEASON_MAP,
|
||||||
|
role: "match map",
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
let scores = select_text(game, &self.selector_scores)
|
||||||
|
.ok_or(ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_SEASON_SCORES,
|
||||||
|
role: "match scores",
|
||||||
|
})?
|
||||||
|
.trim_start_matches('(')
|
||||||
|
.trim_end_matches(')');
|
||||||
|
let points = select_text(game, &self.selector_points);
|
||||||
|
let points_opponent = select_text(game, &self.selector_points_opponent);
|
||||||
|
|
||||||
|
let points = points
|
||||||
|
.map(|points| {
|
||||||
|
points.parse().map_err(|_| ParseError::InvalidText {
|
||||||
|
text: points.to_string(),
|
||||||
|
role: "match points",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
let points_opponent = points_opponent
|
||||||
|
.map(|points| {
|
||||||
|
points.parse().map_err(|_| ParseError::InvalidText {
|
||||||
|
text: points.to_string(),
|
||||||
|
role: "match points opponent",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
let (score, score_opponent) =
|
||||||
|
scores
|
||||||
|
.split_once(" -\n")
|
||||||
|
.ok_or_else(|| ParseError::InvalidText {
|
||||||
|
text: scores.to_string(),
|
||||||
|
role: "match scores",
|
||||||
|
})?;
|
||||||
|
let score = score.parse().map_err(|_| ParseError::InvalidText {
|
||||||
|
text: scores.to_string(),
|
||||||
|
role: "match scores",
|
||||||
|
});
|
||||||
|
let score_opponent =
|
||||||
|
score_opponent.parse().map_err(|_| ParseError::InvalidText {
|
||||||
|
text: scores.to_string(),
|
||||||
|
role: "match scores",
|
||||||
|
});
|
||||||
|
|
||||||
|
let opponent = opponent_link
|
||||||
|
.map(|link| {
|
||||||
|
let name = link.first_text().ok_or(ParseError::EmptyText {
|
||||||
|
selector: SELECTOR_SEASON_OPPONENT,
|
||||||
|
role: "match opponent",
|
||||||
|
})?;
|
||||||
|
let id = team_id_from_link(link.attr("href").unwrap_or_default())?;
|
||||||
|
Result::<_, ParseError>::Ok(TeamRef {
|
||||||
|
name: name.to_string(),
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
let result = match (opponent, points, points_opponent) {
|
||||||
|
(Some(opponent), Some(point), Some(points_opponent)) => {
|
||||||
|
MatchResult::Played {
|
||||||
|
opponent,
|
||||||
|
score: score?,
|
||||||
|
score_opponent: score_opponent?,
|
||||||
|
match_points: point,
|
||||||
|
match_points_opponent: points_opponent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Some(opponent), None, None) => MatchResult::Pending {
|
||||||
|
opponent,
|
||||||
|
score: score?,
|
||||||
|
score_opponent: score_opponent?,
|
||||||
|
},
|
||||||
|
_ => MatchResult::ByeWeek,
|
||||||
|
};
|
||||||
|
Ok(TeamSeasonMatch {
|
||||||
|
week,
|
||||||
|
date: date.to_string(),
|
||||||
|
side: side.to_string(),
|
||||||
|
map: map.to_string(),
|
||||||
|
division: division.to_string(),
|
||||||
|
result,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<_>>()?;
|
||||||
|
|
||||||
|
Ok(TeamSeason { season, matches })
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>>>()
|
||||||
|
}
|
||||||
|
}
|
||||||
97
src/parser/team_roster_history.rs
Normal file
97
src/parser/team_roster_history.rs
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
use super::Parser;
|
||||||
|
use crate::data::RosterHistory;
|
||||||
|
use crate::parser::{select_text, ROSTER_HISTORY_DATE_FORMAT};
|
||||||
|
use crate::{ParseError, Result};
|
||||||
|
use scraper::{Html, Selector};
|
||||||
|
use steamid_ng::SteamID;
|
||||||
|
use time::Date;
|
||||||
|
|
||||||
|
const SELECTOR_ROSTER_ITEM: &str =
|
||||||
|
".container .white-row-small .row-fluid > .col-md-12 > .clearfix";
|
||||||
|
const SELECTOR_ROSTER_NAME: &str = "h5 b";
|
||||||
|
const SELECTOR_ROSTER_ID: &str = "h5 small";
|
||||||
|
const SELECTOR_ROSTER_JOINED: &str = "span.text-success small";
|
||||||
|
const SELECTOR_ROSTER_LEFT: &str = "span.text-danger small";
|
||||||
|
|
||||||
|
pub struct TeamRosterHistoryParser {
|
||||||
|
selector_item: Selector,
|
||||||
|
selector_name: Selector,
|
||||||
|
selector_id: Selector,
|
||||||
|
selector_joined: Selector,
|
||||||
|
selector_left: Selector,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TeamRosterHistoryParser {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TeamRosterHistoryParser {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
TeamRosterHistoryParser {
|
||||||
|
selector_item: Selector::parse(SELECTOR_ROSTER_ITEM).unwrap(),
|
||||||
|
selector_name: Selector::parse(SELECTOR_ROSTER_NAME).unwrap(),
|
||||||
|
selector_id: Selector::parse(SELECTOR_ROSTER_ID).unwrap(),
|
||||||
|
selector_joined: Selector::parse(SELECTOR_ROSTER_JOINED).unwrap(),
|
||||||
|
selector_left: Selector::parse(SELECTOR_ROSTER_LEFT).unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parser for TeamRosterHistoryParser {
|
||||||
|
type Output = Vec<RosterHistory>;
|
||||||
|
|
||||||
|
fn parse(&self, document: &str) -> Result<Self::Output> {
|
||||||
|
let document = Html::parse_document(document);
|
||||||
|
|
||||||
|
document
|
||||||
|
.select(&self.selector_item)
|
||||||
|
.map(|item| {
|
||||||
|
let name =
|
||||||
|
select_text(item, &self.selector_name).ok_or(ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_ROSTER_NAME,
|
||||||
|
role: "member name",
|
||||||
|
})?;
|
||||||
|
let steam_id =
|
||||||
|
select_text(item, &self.selector_id).ok_or(ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_ROSTER_ID,
|
||||||
|
role: "member steam id",
|
||||||
|
})?;
|
||||||
|
let joined = select_text(item, &self.selector_joined).ok_or(
|
||||||
|
ParseError::ElementNotFound {
|
||||||
|
selector: SELECTOR_ROSTER_JOINED,
|
||||||
|
role: "member joined date",
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
let left = select_text(item, &self.selector_left);
|
||||||
|
|
||||||
|
Ok(RosterHistory {
|
||||||
|
name: name.to_string(),
|
||||||
|
steam_id: SteamID::from_steam3(steam_id).map_err(|_| {
|
||||||
|
ParseError::InvalidText {
|
||||||
|
text: steam_id.to_string(),
|
||||||
|
role: "member steam id",
|
||||||
|
}
|
||||||
|
})?,
|
||||||
|
joined: Date::parse(joined, ROSTER_HISTORY_DATE_FORMAT).map_err(|_| {
|
||||||
|
ParseError::InvalidDate {
|
||||||
|
date: steam_id.to_string(),
|
||||||
|
role: "member join date",
|
||||||
|
}
|
||||||
|
})?,
|
||||||
|
left: left
|
||||||
|
.map(|left| {
|
||||||
|
Date::parse(left, ROSTER_HISTORY_DATE_FORMAT).map_err(|_| {
|
||||||
|
ParseError::InvalidDate {
|
||||||
|
date: steam_id.to_string(),
|
||||||
|
role: "member join date",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.transpose()?,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>>>()
|
||||||
|
}
|
||||||
|
}
|
||||||
3297
tests/data/team_6929.html
Normal file
3297
tests/data/team_6929.html
Normal file
File diff suppressed because it is too large
Load diff
3327
tests/data/team_7861.html
Normal file
3327
tests/data/team_7861.html
Normal file
File diff suppressed because it is too large
Load diff
9347
tests/data/team_matches_7861.html
Normal file
9347
tests/data/team_matches_7861.html
Normal file
File diff suppressed because it is too large
Load diff
6862
tests/data/team_roster_history_7861.html
Normal file
6862
tests/data/team_roster_history_7861.html
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,19 +0,0 @@
|
||||||
use insta::assert_json_snapshot;
|
|
||||||
use std::fs::read_to_string;
|
|
||||||
use ugc_scraper::parser::{Parser, PlayerDetailsParser, PlayerParser};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_player_html() {
|
|
||||||
let body = read_to_string("tests/data/player_76561198024494988.html").unwrap();
|
|
||||||
let parser = PlayerParser::new();
|
|
||||||
let parsed = parser.parse(&body).unwrap();
|
|
||||||
assert_json_snapshot!(parsed);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_player_details_html() {
|
|
||||||
let body = read_to_string("tests/data/player_details_76561198024494988.html").unwrap();
|
|
||||||
let parser = PlayerDetailsParser::new();
|
|
||||||
let parsed = parser.parse(&body).unwrap();
|
|
||||||
assert_json_snapshot!(parsed);
|
|
||||||
}
|
|
||||||
54
tests/snapshot.rs
Normal file
54
tests/snapshot.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
use insta::assert_json_snapshot;
|
||||||
|
use std::fs::read_to_string;
|
||||||
|
use ugc_scraper::parser::{
|
||||||
|
Parser, PlayerDetailsParser, PlayerParser, TeamMatchesParser, TeamParser,
|
||||||
|
TeamRosterHistoryParser,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_player_html() {
|
||||||
|
let body = read_to_string("tests/data/player_76561198024494988.html").unwrap();
|
||||||
|
let parser = PlayerParser::new();
|
||||||
|
let parsed = parser.parse(&body).unwrap();
|
||||||
|
assert_json_snapshot!(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_player_details_html() {
|
||||||
|
let body = read_to_string("tests/data/player_details_76561198024494988.html").unwrap();
|
||||||
|
let parser = PlayerDetailsParser::new();
|
||||||
|
let parsed = parser.parse(&body).unwrap();
|
||||||
|
assert_json_snapshot!(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_team_html() {
|
||||||
|
let body = read_to_string("tests/data/team_7861.html").unwrap();
|
||||||
|
let parser = TeamParser::new();
|
||||||
|
let parsed = parser.parse(&body).unwrap();
|
||||||
|
assert_json_snapshot!(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_team_changed_name_html() {
|
||||||
|
let body = read_to_string("tests/data/team_6929.html").unwrap();
|
||||||
|
let parser = TeamParser::new();
|
||||||
|
let parsed = parser.parse(&body).unwrap();
|
||||||
|
assert_json_snapshot!(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_team_roster_history_html() {
|
||||||
|
let body = read_to_string("tests/data/team_roster_history_7861.html").unwrap();
|
||||||
|
let parser = TeamRosterHistoryParser::new();
|
||||||
|
let parsed = parser.parse(&body).unwrap();
|
||||||
|
assert_json_snapshot!(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_team_matches_html() {
|
||||||
|
let body = read_to_string("tests/data/team_matches_7861.html").unwrap();
|
||||||
|
let parser = TeamMatchesParser::new();
|
||||||
|
let parsed = parser.parse(&body).unwrap();
|
||||||
|
assert_json_snapshot!(parsed);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
source: tests/player.rs
|
source: tests/snapshot.rs
|
||||||
expression: parsed
|
expression: parsed
|
||||||
---
|
---
|
||||||
[
|
[
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
source: tests/player.rs
|
source: tests/snapshot.rs
|
||||||
expression: parsed
|
expression: parsed
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
583
tests/snapshots/snapshot__parse_team_changed_name_html.snap
Normal file
583
tests/snapshots/snapshot__parse_team_changed_name_html.snap
Normal file
|
|
@ -0,0 +1,583 @@
|
||||||
|
---
|
||||||
|
source: tests/snapshot.rs
|
||||||
|
expression: parsed
|
||||||
|
---
|
||||||
|
{
|
||||||
|
"name": "UGC 6s",
|
||||||
|
"tag": "Europe",
|
||||||
|
"image": "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/09/096a30b1025c586f9d41c686077129f6e86998d0_full.jpg",
|
||||||
|
"format": "TF2 6vs6",
|
||||||
|
"timezone": "West-Euro",
|
||||||
|
"division": "Europe",
|
||||||
|
"description": "Giel is Jesus",
|
||||||
|
"titles": [
|
||||||
|
"TF2 6v6 S38 Europe 3rd Place",
|
||||||
|
"TF2 6v6 EU Steel Champions Season 14"
|
||||||
|
],
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"name": "GCKimo",
|
||||||
|
"steam_id": 76561197992327511,
|
||||||
|
"role": "Leader",
|
||||||
|
"since": [
|
||||||
|
2013,
|
||||||
|
127,
|
||||||
|
9,
|
||||||
|
31,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gielewiel9",
|
||||||
|
"steam_id": 76561198061174419,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2013,
|
||||||
|
254,
|
||||||
|
1,
|
||||||
|
21,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SUZY Sacrénom d'un",
|
||||||
|
"steam_id": 76561198004734774,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2013,
|
||||||
|
276,
|
||||||
|
1,
|
||||||
|
45,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Vipe",
|
||||||
|
"steam_id": 76561198059011634,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2014,
|
||||||
|
141,
|
||||||
|
3,
|
||||||
|
32,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "spreijer tf2lt",
|
||||||
|
"steam_id": 76561198032234067,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2014,
|
||||||
|
155,
|
||||||
|
1,
|
||||||
|
6,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Herpa",
|
||||||
|
"steam_id": 76561198183437643,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2016,
|
||||||
|
173,
|
||||||
|
3,
|
||||||
|
5,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Icewind demostf",
|
||||||
|
"steam_id": 76561198024494988,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2017,
|
||||||
|
52,
|
||||||
|
3,
|
||||||
|
52,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Vclox",
|
||||||
|
"steam_id": 76561198056783619,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2018,
|
||||||
|
59,
|
||||||
|
2,
|
||||||
|
29,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Fish",
|
||||||
|
"steam_id": 76561198052362074,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2018,
|
||||||
|
171,
|
||||||
|
2,
|
||||||
|
12,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Bobbert",
|
||||||
|
"steam_id": 76561198071877015,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2019,
|
||||||
|
170,
|
||||||
|
1,
|
||||||
|
59,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Kaga",
|
||||||
|
"steam_id": 76561198040965137,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2020,
|
||||||
|
42,
|
||||||
|
6,
|
||||||
|
35,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GMsU CreepsiliusM",
|
||||||
|
"steam_id": 76561198071903356,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2022,
|
||||||
|
284,
|
||||||
|
4,
|
||||||
|
43,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "DelT",
|
||||||
|
"steam_id": 76561198204007537,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2023,
|
||||||
|
60,
|
||||||
|
3,
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Deity",
|
||||||
|
"steam_id": 76561198076020012,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2023,
|
||||||
|
228,
|
||||||
|
4,
|
||||||
|
31,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ikaros",
|
||||||
|
"steam_id": 76561198158482651,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2023,
|
||||||
|
228,
|
||||||
|
4,
|
||||||
|
32,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"season": 42,
|
||||||
|
"division": "Europe",
|
||||||
|
"wins": 3,
|
||||||
|
"losses": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 40,
|
||||||
|
"division": "Europe",
|
||||||
|
"wins": 4,
|
||||||
|
"losses": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 39,
|
||||||
|
"division": "Europe",
|
||||||
|
"wins": 3,
|
||||||
|
"losses": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 38,
|
||||||
|
"division": "Europe",
|
||||||
|
"wins": 5,
|
||||||
|
"losses": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 37,
|
||||||
|
"division": "Europe",
|
||||||
|
"wins": 4,
|
||||||
|
"losses": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 36,
|
||||||
|
"division": "Europe",
|
||||||
|
"wins": 3,
|
||||||
|
"losses": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 35,
|
||||||
|
"division": "Europe",
|
||||||
|
"wins": 5,
|
||||||
|
"losses": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 34,
|
||||||
|
"division": "Europe",
|
||||||
|
"wins": 6,
|
||||||
|
"losses": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 33,
|
||||||
|
"division": "Europe",
|
||||||
|
"wins": 3,
|
||||||
|
"losses": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 32,
|
||||||
|
"division": "Europe",
|
||||||
|
"wins": 5,
|
||||||
|
"losses": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 31,
|
||||||
|
"division": "Europe",
|
||||||
|
"wins": 7,
|
||||||
|
"losses": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 30,
|
||||||
|
"division": "Europe",
|
||||||
|
"wins": 3,
|
||||||
|
"losses": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 29,
|
||||||
|
"division": "Europe",
|
||||||
|
"wins": 4,
|
||||||
|
"losses": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 28,
|
||||||
|
"division": "Europe",
|
||||||
|
"wins": 3,
|
||||||
|
"losses": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 27,
|
||||||
|
"division": "Europe",
|
||||||
|
"wins": 5,
|
||||||
|
"losses": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 26,
|
||||||
|
"division": "Europe",
|
||||||
|
"wins": 4,
|
||||||
|
"losses": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 25,
|
||||||
|
"division": "Europe",
|
||||||
|
"wins": 3,
|
||||||
|
"losses": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 24,
|
||||||
|
"division": "Europe",
|
||||||
|
"wins": 4,
|
||||||
|
"losses": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 23,
|
||||||
|
"division": "Europe",
|
||||||
|
"wins": 4,
|
||||||
|
"losses": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 22,
|
||||||
|
"division": "Euro Platinum",
|
||||||
|
"wins": 2,
|
||||||
|
"losses": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 21,
|
||||||
|
"division": "Euro Gold",
|
||||||
|
"wins": 3,
|
||||||
|
"losses": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 20,
|
||||||
|
"division": "Euro Steel",
|
||||||
|
"wins": 4,
|
||||||
|
"losses": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 19,
|
||||||
|
"division": "Euro Steel",
|
||||||
|
"wins": 5,
|
||||||
|
"losses": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 18,
|
||||||
|
"division": "Euro Steel",
|
||||||
|
"wins": 2,
|
||||||
|
"losses": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 17,
|
||||||
|
"division": "Euro Steel",
|
||||||
|
"wins": 5,
|
||||||
|
"losses": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 16,
|
||||||
|
"division": "Euro Platinum",
|
||||||
|
"wins": 4,
|
||||||
|
"losses": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 15,
|
||||||
|
"division": "Euro Platinum",
|
||||||
|
"wins": 3,
|
||||||
|
"losses": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 14,
|
||||||
|
"division": "Euro Steel",
|
||||||
|
"wins": 11,
|
||||||
|
"losses": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 13,
|
||||||
|
"division": "Euro Platinum",
|
||||||
|
"wins": 3,
|
||||||
|
"losses": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 12,
|
||||||
|
"division": "Euro Steel",
|
||||||
|
"wins": 5,
|
||||||
|
"losses": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name_changes": [
|
||||||
|
{
|
||||||
|
"from_tag": "by Kimo",
|
||||||
|
"from": "Xenon 2",
|
||||||
|
"to_tag": "Europe",
|
||||||
|
"to": "UGC 6s",
|
||||||
|
"date": [
|
||||||
|
2023,
|
||||||
|
60
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_tag": "John 2",
|
||||||
|
"from": "Let's Claim the Default",
|
||||||
|
"to_tag": "by Kimo",
|
||||||
|
"to": "Xenon 2",
|
||||||
|
"date": [
|
||||||
|
2022,
|
||||||
|
278
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_tag": "John 2",
|
||||||
|
"from": "https://youtu.be/iio-P3ubZtE",
|
||||||
|
"to_tag": "John 2",
|
||||||
|
"to": "Let's Claim the Default",
|
||||||
|
"date": [
|
||||||
|
2022,
|
||||||
|
170
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_tag": "... I guess?",
|
||||||
|
"from": "Let's Claim the Default",
|
||||||
|
"to_tag": "John 2",
|
||||||
|
"to": "https://youtu.be/iio-P3ubZtE",
|
||||||
|
"date": [
|
||||||
|
2022,
|
||||||
|
137
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_tag": "360",
|
||||||
|
"from": "Controller Gamers",
|
||||||
|
"to_tag": "... I guess?",
|
||||||
|
"to": "Let's Claim the Default",
|
||||||
|
"date": [
|
||||||
|
2021,
|
||||||
|
181
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_tag": "bye",
|
||||||
|
"from": "Bye week",
|
||||||
|
"to_tag": "360",
|
||||||
|
"to": "Controller Gamers",
|
||||||
|
"date": [
|
||||||
|
2021,
|
||||||
|
55
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_tag": "| meta.tf",
|
||||||
|
"from": "meta.tf",
|
||||||
|
"to_tag": "bye",
|
||||||
|
"to": "Bye week",
|
||||||
|
"date": [
|
||||||
|
2020,
|
||||||
|
281
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_tag": "Giele!",
|
||||||
|
"from": "Giel and the 9wiels",
|
||||||
|
"to_tag": "| meta.tf",
|
||||||
|
"to": "meta.tf",
|
||||||
|
"date": [
|
||||||
|
2017,
|
||||||
|
151
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_tag": "pFp",
|
||||||
|
"from": "Popping for points",
|
||||||
|
"to_tag": "Giele!",
|
||||||
|
"to": "Giel and the 9wiels",
|
||||||
|
"date": [
|
||||||
|
2016,
|
||||||
|
167
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_tag": "HLL",
|
||||||
|
"from": "HL Lite",
|
||||||
|
"to_tag": "pFp",
|
||||||
|
"to": "Popping for points",
|
||||||
|
"date": [
|
||||||
|
2016,
|
||||||
|
20
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_tag": "-AA-",
|
||||||
|
"from": "Anti-Anime",
|
||||||
|
"to_tag": "HLL",
|
||||||
|
"to": "HL Lite",
|
||||||
|
"date": [
|
||||||
|
2015,
|
||||||
|
280
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_tag": "Frequent",
|
||||||
|
"from": "Frequently Missing",
|
||||||
|
"to_tag": "-AA-",
|
||||||
|
"to": "Anti-Anime",
|
||||||
|
"date": [
|
||||||
|
2015,
|
||||||
|
138
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_tag": "pinky|",
|
||||||
|
"from": "Frequently Missing",
|
||||||
|
"to_tag": "Frequent",
|
||||||
|
"to": "Frequently Missing",
|
||||||
|
"date": [
|
||||||
|
2015,
|
||||||
|
17
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from_tag": "pinky|",
|
||||||
|
"from": "Pinky",
|
||||||
|
"to_tag": "pinky|",
|
||||||
|
"to": "Frequently Missing",
|
||||||
|
"date": [
|
||||||
|
2015,
|
||||||
|
17
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
531
tests/snapshots/snapshot__parse_team_html.snap
Normal file
531
tests/snapshots/snapshot__parse_team_html.snap
Normal file
|
|
@ -0,0 +1,531 @@
|
||||||
|
---
|
||||||
|
source: tests/snapshot.rs
|
||||||
|
expression: parsed
|
||||||
|
---
|
||||||
|
{
|
||||||
|
"name": "Xenon",
|
||||||
|
"tag": "-Xe-",
|
||||||
|
"image": "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/db/dbabbd8bab7ccf6d27a9d4ca2e73a76e085bb201_full.jpg",
|
||||||
|
"format": "TF2 Highlander",
|
||||||
|
"timezone": "West-Euro",
|
||||||
|
"division": "Euro Platinum",
|
||||||
|
"description": "Contact Kimo or Icewind for everything.",
|
||||||
|
"titles": [
|
||||||
|
"TF2 HL EU Silver 3rd Place S18"
|
||||||
|
],
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"name": "Icewind demostf",
|
||||||
|
"steam_id": 76561198024494988,
|
||||||
|
"role": "Leader",
|
||||||
|
"since": [
|
||||||
|
2013,
|
||||||
|
221,
|
||||||
|
3,
|
||||||
|
23,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Fish",
|
||||||
|
"steam_id": 76561198052362074,
|
||||||
|
"role": "Leader",
|
||||||
|
"since": [
|
||||||
|
2014,
|
||||||
|
120,
|
||||||
|
10,
|
||||||
|
37,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GCKimo",
|
||||||
|
"steam_id": 76561197992327511,
|
||||||
|
"role": "Leader",
|
||||||
|
"since": [
|
||||||
|
2016,
|
||||||
|
171,
|
||||||
|
11,
|
||||||
|
24,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NoSocks",
|
||||||
|
"steam_id": 76561198012110404,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2013,
|
||||||
|
218,
|
||||||
|
5,
|
||||||
|
21,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Shoosh",
|
||||||
|
"steam_id": 76561198049593717,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2014,
|
||||||
|
255,
|
||||||
|
9,
|
||||||
|
37,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Dirty Sneeds Done",
|
||||||
|
"steam_id": 76561198049312442,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2015,
|
||||||
|
266,
|
||||||
|
12,
|
||||||
|
24,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Deity",
|
||||||
|
"steam_id": 76561198076020012,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2015,
|
||||||
|
363,
|
||||||
|
2,
|
||||||
|
52,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "jojo",
|
||||||
|
"steam_id": 76561197995029224,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2016,
|
||||||
|
17,
|
||||||
|
10,
|
||||||
|
47,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bigdog",
|
||||||
|
"steam_id": 76561198076014163,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2016,
|
||||||
|
146,
|
||||||
|
4,
|
||||||
|
8,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "musTard",
|
||||||
|
"steam_id": 76561197990486664,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2017,
|
||||||
|
17,
|
||||||
|
7,
|
||||||
|
43,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Kaga",
|
||||||
|
"steam_id": 76561198040965137,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2018,
|
||||||
|
312,
|
||||||
|
6,
|
||||||
|
42,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "STEEEEEEEEEEELAZ",
|
||||||
|
"steam_id": 76561198036824480,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2019,
|
||||||
|
272,
|
||||||
|
8,
|
||||||
|
35,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Derakusa",
|
||||||
|
"steam_id": 76561198011495003,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2020,
|
||||||
|
275,
|
||||||
|
4,
|
||||||
|
28,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Kireek",
|
||||||
|
"steam_id": 76561198052694464,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2022,
|
||||||
|
27,
|
||||||
|
4,
|
||||||
|
17,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Royal Flush",
|
||||||
|
"steam_id": 76561198052084714,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2022,
|
||||||
|
128,
|
||||||
|
6,
|
||||||
|
42,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "BaaBo",
|
||||||
|
"steam_id": 76561198004331478,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2023,
|
||||||
|
9,
|
||||||
|
10,
|
||||||
|
18,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "drew",
|
||||||
|
"steam_id": 76561198012304706,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2023,
|
||||||
|
43,
|
||||||
|
5,
|
||||||
|
54,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Raipe",
|
||||||
|
"steam_id": 76561198061082936,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2023,
|
||||||
|
78,
|
||||||
|
5,
|
||||||
|
27,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Teroantero2007",
|
||||||
|
"steam_id": 76561197996902035,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2023,
|
||||||
|
177,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "taskmast33r",
|
||||||
|
"steam_id": 76561198218881647,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2023,
|
||||||
|
203,
|
||||||
|
4,
|
||||||
|
46,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "marko",
|
||||||
|
"steam_id": 76561198274165935,
|
||||||
|
"role": "Member",
|
||||||
|
"since": [
|
||||||
|
2023,
|
||||||
|
292,
|
||||||
|
1,
|
||||||
|
13,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-5,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"season": 40,
|
||||||
|
"division": "Euro Platinum",
|
||||||
|
"wins": 3,
|
||||||
|
"losses": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 39,
|
||||||
|
"division": "Euro Platinum",
|
||||||
|
"wins": 4,
|
||||||
|
"losses": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 38,
|
||||||
|
"division": "Euro Silver",
|
||||||
|
"wins": 4,
|
||||||
|
"losses": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 37,
|
||||||
|
"division": "Euro Silver",
|
||||||
|
"wins": 9,
|
||||||
|
"losses": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 36,
|
||||||
|
"division": "Euro Platinum",
|
||||||
|
"wins": 2,
|
||||||
|
"losses": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 35,
|
||||||
|
"division": "Euro Platinum",
|
||||||
|
"wins": 2,
|
||||||
|
"losses": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 34,
|
||||||
|
"division": "Euro Platinum",
|
||||||
|
"wins": 3,
|
||||||
|
"losses": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 32,
|
||||||
|
"division": "Euro Platinum",
|
||||||
|
"wins": 3,
|
||||||
|
"losses": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 31,
|
||||||
|
"division": "Euro Platinum",
|
||||||
|
"wins": 4,
|
||||||
|
"losses": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 30,
|
||||||
|
"division": "Euro Platinum",
|
||||||
|
"wins": 3,
|
||||||
|
"losses": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 29,
|
||||||
|
"division": "Euro Platinum",
|
||||||
|
"wins": 4,
|
||||||
|
"losses": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 28,
|
||||||
|
"division": "Euro Platinum",
|
||||||
|
"wins": 3,
|
||||||
|
"losses": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 27,
|
||||||
|
"division": "Premium EU",
|
||||||
|
"wins": 4,
|
||||||
|
"losses": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 26,
|
||||||
|
"division": "Premium EU",
|
||||||
|
"wins": 3,
|
||||||
|
"losses": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 25,
|
||||||
|
"division": "Euro Platinum",
|
||||||
|
"wins": 5,
|
||||||
|
"losses": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 24,
|
||||||
|
"division": "Euro Platinum",
|
||||||
|
"wins": 4,
|
||||||
|
"losses": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 23,
|
||||||
|
"division": "Euro Platinum",
|
||||||
|
"wins": 5,
|
||||||
|
"losses": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 22,
|
||||||
|
"division": "Euro Platinum",
|
||||||
|
"wins": 4,
|
||||||
|
"losses": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 21,
|
||||||
|
"division": "Euro Platinum",
|
||||||
|
"wins": 4,
|
||||||
|
"losses": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 20,
|
||||||
|
"division": "Euro Platinum",
|
||||||
|
"wins": 2,
|
||||||
|
"losses": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 19,
|
||||||
|
"division": "Euro Gold",
|
||||||
|
"wins": 6,
|
||||||
|
"losses": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 18,
|
||||||
|
"division": "Euro Silver",
|
||||||
|
"wins": 7,
|
||||||
|
"losses": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 17,
|
||||||
|
"division": "Euro Silver",
|
||||||
|
"wins": 3,
|
||||||
|
"losses": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 16,
|
||||||
|
"division": "Euro Silver",
|
||||||
|
"wins": 3,
|
||||||
|
"losses": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 15,
|
||||||
|
"division": "Euro Silver",
|
||||||
|
"wins": 2,
|
||||||
|
"losses": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 14,
|
||||||
|
"division": "Euro Silver",
|
||||||
|
"wins": 3,
|
||||||
|
"losses": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 13,
|
||||||
|
"division": "Euro Silver",
|
||||||
|
"wins": 3,
|
||||||
|
"losses": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 12,
|
||||||
|
"division": "Euro Silver",
|
||||||
|
"wins": 4,
|
||||||
|
"losses": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"season": 11,
|
||||||
|
"division": "Euro Steel",
|
||||||
|
"wins": 4,
|
||||||
|
"losses": 4
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name_changes": []
|
||||||
|
}
|
||||||
4970
tests/snapshots/snapshot__parse_team_matches_html.snap
Normal file
4970
tests/snapshots/snapshot__parse_team_matches_html.snap
Normal file
File diff suppressed because it is too large
Load diff
1827
tests/snapshots/snapshot__parse_team_roster_history_html.snap
Normal file
1827
tests/snapshots/snapshot__parse_team_roster_history_html.snap
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue