The API returns gmid/gmid2 as quoted strings ("258104532423147520")
to avoid JavaScript precision loss. Changed types to Option<String>.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
244 lines
8.5 KiB
Rust
244 lines
8.5 KiB
Rust
use anyhow::Result;
|
|
use sqlx::SqlitePool;
|
|
|
|
use super::client::LeagueApiClient;
|
|
|
|
#[derive(Debug)]
|
|
pub struct SyncResult {
|
|
pub teams: i64,
|
|
pub players: i64,
|
|
pub transactions: i64,
|
|
}
|
|
|
|
/// Sync all teams from the league API for the given season.
|
|
/// Uses INSERT OR REPLACE to handle both inserts and updates atomically.
|
|
pub async fn sync_teams(pool: &SqlitePool, season: i64, client: &LeagueApiClient) -> Result<i64> {
|
|
let response = client.get_teams(Some(season), None, true, false).await?;
|
|
|
|
let mut tx = pool.begin().await?;
|
|
let mut count: i64 = 0;
|
|
let synced_at = chrono::Utc::now().naive_utc();
|
|
|
|
for data in response.teams {
|
|
let manager1_name = data.manager1.and_then(|m| m.name);
|
|
let manager2_name = data.manager2.and_then(|m| m.name);
|
|
let gm_discord_id = data.gm_discord_id;
|
|
let gm2_discord_id = data.gm2_discord_id;
|
|
let division_id = data.division.as_ref().map(|d| d.id).flatten();
|
|
let division_name = data.division.as_ref().and_then(|d| d.division_name.clone());
|
|
let league_abbrev = data.division.as_ref().and_then(|d| d.league_abbrev.clone());
|
|
|
|
sqlx::query(
|
|
"INSERT OR REPLACE INTO teams \
|
|
(id, abbrev, short_name, long_name, season, manager1_name, manager2_name, \
|
|
gm_discord_id, gm2_discord_id, division_id, division_name, league_abbrev, \
|
|
thumbnail, color, dice_color, stadium, salary_cap, synced_at) \
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
)
|
|
.bind(data.id)
|
|
.bind(data.abbrev.unwrap_or_default())
|
|
.bind(data.short_name.unwrap_or_default())
|
|
.bind(data.long_name.unwrap_or_default())
|
|
.bind(season)
|
|
.bind(manager1_name)
|
|
.bind(manager2_name)
|
|
.bind(gm_discord_id)
|
|
.bind(gm2_discord_id)
|
|
.bind(division_id)
|
|
.bind(division_name)
|
|
.bind(league_abbrev)
|
|
.bind(data.thumbnail)
|
|
.bind(data.color)
|
|
.bind(data.dice_color)
|
|
.bind(data.stadium)
|
|
.bind(data.salary_cap)
|
|
.bind(synced_at)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
count += 1;
|
|
}
|
|
|
|
tx.commit().await?;
|
|
crate::db::queries::update_sync_status(pool, "teams", count, None).await?;
|
|
Ok(count)
|
|
}
|
|
|
|
/// Sync players from the league API.
|
|
/// Uses ON CONFLICT DO UPDATE with an explicit column list that intentionally omits `hand`,
|
|
/// since `hand` is populated only from CSV card imports and must not be overwritten by the API.
|
|
pub async fn sync_players(
|
|
pool: &SqlitePool,
|
|
season: i64,
|
|
team_id: Option<i64>,
|
|
client: &LeagueApiClient,
|
|
) -> Result<i64> {
|
|
let team_ids = team_id.map(|id| vec![id]);
|
|
let response = client
|
|
.get_players(Some(season), team_ids.as_deref(), None, None, false)
|
|
.await?;
|
|
|
|
let mut tx = pool.begin().await?;
|
|
let mut count: i64 = 0;
|
|
let synced_at = chrono::Utc::now().naive_utc();
|
|
|
|
for data in response.players {
|
|
let player_team_id = data.team.map(|t| t.id);
|
|
|
|
sqlx::query(
|
|
"INSERT INTO players \
|
|
(id, name, season, team_id, swar, card_image, card_image_alt, headshot, vanity_card, \
|
|
pos_1, pos_2, pos_3, pos_4, pos_5, pos_6, pos_7, pos_8, \
|
|
injury_rating, il_return, demotion_week, strat_code, bbref_id, sbaplayer_id, \
|
|
last_game, last_game2, synced_at) \
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) \
|
|
ON CONFLICT(id) DO UPDATE SET \
|
|
name=excluded.name, \
|
|
season=excluded.season, \
|
|
team_id=excluded.team_id, \
|
|
swar=excluded.swar, \
|
|
card_image=excluded.card_image, \
|
|
card_image_alt=excluded.card_image_alt, \
|
|
headshot=excluded.headshot, \
|
|
vanity_card=excluded.vanity_card, \
|
|
pos_1=excluded.pos_1, \
|
|
pos_2=excluded.pos_2, \
|
|
pos_3=excluded.pos_3, \
|
|
pos_4=excluded.pos_4, \
|
|
pos_5=excluded.pos_5, \
|
|
pos_6=excluded.pos_6, \
|
|
pos_7=excluded.pos_7, \
|
|
pos_8=excluded.pos_8, \
|
|
injury_rating=excluded.injury_rating, \
|
|
il_return=excluded.il_return, \
|
|
demotion_week=excluded.demotion_week, \
|
|
strat_code=excluded.strat_code, \
|
|
bbref_id=excluded.bbref_id, \
|
|
sbaplayer_id=excluded.sbaplayer_id, \
|
|
last_game=excluded.last_game, \
|
|
last_game2=excluded.last_game2, \
|
|
synced_at=excluded.synced_at",
|
|
)
|
|
.bind(data.id)
|
|
.bind(data.name.unwrap_or_default())
|
|
.bind(season)
|
|
.bind(player_team_id)
|
|
.bind(data.swar)
|
|
.bind(data.card_image)
|
|
.bind(data.card_image_alt)
|
|
.bind(data.headshot)
|
|
.bind(data.vanity_card)
|
|
.bind(data.pos_1)
|
|
.bind(data.pos_2)
|
|
.bind(data.pos_3)
|
|
.bind(data.pos_4)
|
|
.bind(data.pos_5)
|
|
.bind(data.pos_6)
|
|
.bind(data.pos_7)
|
|
.bind(data.pos_8)
|
|
.bind(data.injury_rating)
|
|
.bind(data.il_return)
|
|
.bind(data.demotion_week)
|
|
.bind(data.strat_code)
|
|
.bind(data.bbref_id)
|
|
.bind(data.sbaplayer_id)
|
|
.bind(data.last_game)
|
|
.bind(data.last_game2)
|
|
.bind(synced_at)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
count += 1;
|
|
}
|
|
|
|
tx.commit().await?;
|
|
crate::db::queries::update_sync_status(pool, "players", count, None).await?;
|
|
Ok(count)
|
|
}
|
|
|
|
/// Sync transactions from the league API for the given season/week range.
|
|
/// Skips rows where move_id or player is missing.
|
|
/// ON CONFLICT only updates mutable fields (cancelled, frozen, synced_at) to preserve original
|
|
/// season/week/team data.
|
|
pub async fn sync_transactions(
|
|
pool: &SqlitePool,
|
|
season: i64,
|
|
week_start: i64,
|
|
week_end: Option<i64>,
|
|
team_abbrev: Option<&str>,
|
|
client: &LeagueApiClient,
|
|
) -> Result<i64> {
|
|
let abbrev_vec: Vec<&str> = team_abbrev.into_iter().collect();
|
|
let abbrev_slice: Option<&[&str]> =
|
|
if abbrev_vec.is_empty() { None } else { Some(&abbrev_vec) };
|
|
|
|
let response = client
|
|
.get_transactions(season, week_start, week_end, abbrev_slice, false, false, false)
|
|
.await?;
|
|
|
|
let mut tx = pool.begin().await?;
|
|
let mut count: i64 = 0;
|
|
let synced_at = chrono::Utc::now().naive_utc();
|
|
|
|
for data in response.transactions {
|
|
let (Some(move_id), Some(player)) = (data.move_id, data.player) else {
|
|
continue;
|
|
};
|
|
|
|
let move_id_str = move_id.to_string();
|
|
let player_id = player.id;
|
|
// Schema has from_team_id/to_team_id as NOT NULL; use 0 as sentinel for missing teams
|
|
let from_team_id = data.oldteam.map(|t| t.id).unwrap_or(0);
|
|
let to_team_id = data.newteam.map(|t| t.id).unwrap_or(0);
|
|
let week = data.week.unwrap_or(0);
|
|
let cancelled = data.cancelled.unwrap_or(false);
|
|
let frozen = data.frozen.unwrap_or(false);
|
|
|
|
sqlx::query(
|
|
"INSERT INTO transactions \
|
|
(season, week, move_id, player_id, from_team_id, to_team_id, cancelled, frozen, \
|
|
synced_at) \
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) \
|
|
ON CONFLICT(move_id, player_id) DO UPDATE SET \
|
|
cancelled=excluded.cancelled, \
|
|
frozen=excluded.frozen, \
|
|
synced_at=excluded.synced_at",
|
|
)
|
|
.bind(season)
|
|
.bind(week)
|
|
.bind(move_id_str)
|
|
.bind(player_id)
|
|
.bind(from_team_id)
|
|
.bind(to_team_id)
|
|
.bind(cancelled)
|
|
.bind(frozen)
|
|
.bind(synced_at)
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
count += 1;
|
|
}
|
|
|
|
tx.commit().await?;
|
|
crate::db::queries::update_sync_status(pool, "transactions", count, None).await?;
|
|
Ok(count)
|
|
}
|
|
|
|
/// Orchestrate a full sync: teams, players, and transactions for the configured season.
|
|
pub async fn sync_all(pool: &SqlitePool, settings: &crate::config::Settings) -> Result<SyncResult> {
|
|
let client = LeagueApiClient::new(
|
|
&settings.api.base_url,
|
|
&settings.api.api_key,
|
|
settings.api.timeout,
|
|
)?;
|
|
|
|
let season = settings.team.season as i64;
|
|
|
|
let teams = sync_teams(pool, season, &client).await?;
|
|
let players = sync_players(pool, season, None, &client).await?;
|
|
// Start from week 1 (Python defaults to 0 which includes pre-season moves)
|
|
let transactions = sync_transactions(pool, season, 1, None, None, &client).await?;
|
|
|
|
Ok(SyncResult { teams, players, transactions })
|
|
}
|