170 lines
5.5 KiB
Rust
170 lines
5.5 KiB
Rust
use reqwest::Client as HttpClient;
|
|
use rick_and_morty::models::{Character, OriginOrLocation};
|
|
use serde::{Deserialize, Serialize};
|
|
use sqlx::{Row, SqlitePool};
|
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
struct ApiCharacter {
|
|
id: i32,
|
|
name: String,
|
|
status: String,
|
|
species: String,
|
|
#[serde(rename = "type")]
|
|
character_type: String,
|
|
gender: String,
|
|
origin: OriginOrLocation,
|
|
location: OriginOrLocation,
|
|
image: String,
|
|
episode: Vec<String>,
|
|
url: String,
|
|
created: String,
|
|
}
|
|
|
|
impl From<ApiCharacter> for Character {
|
|
fn from(api: ApiCharacter) -> Self {
|
|
Character {
|
|
id: 0, // Ignored for new/incoming data; SQLite autoincrements
|
|
rmid: api.id,
|
|
name: api.name,
|
|
status: api.status,
|
|
species: api.species,
|
|
r#type: api.character_type,
|
|
gender: api.gender,
|
|
origin_name: api.origin.name,
|
|
origin_url: api.origin.url,
|
|
location_name: api.location.name,
|
|
location_url: api.location.url,
|
|
image: api.image,
|
|
// Store as JSON string
|
|
episode: serde_json::to_string(&api.episode).unwrap(),
|
|
url: api.url,
|
|
created: api.created,
|
|
elo_rating: 1000.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn init_tracing() {
|
|
use tracing_subscriber::EnvFilter;
|
|
tracing_subscriber::fmt()
|
|
.with_env_filter(EnvFilter::from_default_env())
|
|
.with_target(true)
|
|
.with_level(true)
|
|
.init();
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> anyhow::Result<()> {
|
|
init_tracing();
|
|
dotenvy::dotenv().ok();
|
|
|
|
let db_url = std::env::var("DATABASE_URL").expect("DATABASE_URL not set");
|
|
let pool = SqlitePool::connect(&db_url).await?;
|
|
|
|
tracing::info!("Starting to fetch characters from Rick and Morty API");
|
|
let http = HttpClient::new();
|
|
|
|
let mut all_characters: Vec<Character> = Vec::new();
|
|
let mut next_url = "https://rickandmortyapi.com/api/character".to_string();
|
|
|
|
while !next_url.is_empty() {
|
|
tracing::info!(url = %next_url, "Fetching page");
|
|
let resp = http
|
|
.get(&next_url)
|
|
.send()
|
|
.await?
|
|
.json::<serde_json::Value>()
|
|
.await?;
|
|
let results = resp["results"].as_array().unwrap();
|
|
for c in results {
|
|
let c: ApiCharacter = serde_json::from_value(c.clone()).unwrap();
|
|
all_characters.push(c.into());
|
|
}
|
|
next_url = resp["info"]["next"].as_str().unwrap_or("").to_string();
|
|
}
|
|
|
|
tracing::info!(
|
|
count = all_characters.len(),
|
|
"Fetched all characters, starting DB upsert"
|
|
);
|
|
|
|
for character in &all_characters {
|
|
// Try to fetch existing character by rmid
|
|
let row = sqlx::query("SELECT id, elo_rating FROM characters WHERE rmid = ?")
|
|
.bind(character.rmid)
|
|
.fetch_optional(&pool)
|
|
.await?;
|
|
|
|
if let Some(row) = row {
|
|
// Exists: update all fields except elo_rating
|
|
let id: i64 = row.get("id");
|
|
sqlx::query(
|
|
"UPDATE characters SET
|
|
name = ?, status = ?, species = ?, type = ?, gender = ?,
|
|
origin_name = ?, origin_url = ?,
|
|
location_name = ?, location_url = ?,
|
|
image = ?, episode = ?, url = ?, created = ?
|
|
WHERE id = ?",
|
|
)
|
|
.bind(&character.name)
|
|
.bind(&character.status)
|
|
.bind(&character.species)
|
|
.bind(&character.r#type)
|
|
.bind(&character.gender)
|
|
.bind(&character.origin_name)
|
|
.bind(&character.origin_url)
|
|
.bind(&character.location_name)
|
|
.bind(&character.location_url)
|
|
.bind(&character.image)
|
|
.bind(&character.episode)
|
|
.bind(&character.url)
|
|
.bind(&character.created)
|
|
.bind(id)
|
|
.execute(&pool)
|
|
.await?;
|
|
tracing::info!(id = character.rmid, name = %character.name, "Updated character (no elo overwrite)");
|
|
} else {
|
|
// Insert new character with default elo_rating
|
|
sqlx::query(
|
|
"INSERT INTO characters (
|
|
rmid, name, status, species, type, gender,
|
|
origin_name, origin_url, location_name, location_url,
|
|
image, episode, url, created, elo_rating
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
)
|
|
.bind(character.rmid)
|
|
.bind(&character.name)
|
|
.bind(&character.status)
|
|
.bind(&character.species)
|
|
.bind(&character.r#type)
|
|
.bind(&character.gender)
|
|
.bind(&character.origin_name)
|
|
.bind(&character.origin_url)
|
|
.bind(&character.location_name)
|
|
.bind(&character.location_url)
|
|
.bind(&character.image)
|
|
.bind(&character.episode)
|
|
.bind(&character.url)
|
|
.bind(&character.created)
|
|
.bind(1000.0)
|
|
.execute(&pool)
|
|
.await?;
|
|
tracing::info!(id = character.rmid, name = %character.name, "Inserted new character");
|
|
}
|
|
}
|
|
|
|
// tracing::info!("Inserted {} characters", insert_result.inserted_ids.len());
|
|
|
|
let character_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM characters")
|
|
.fetch_one(&pool)
|
|
.await?;
|
|
tracing::info!(
|
|
count = character_count,
|
|
"Total characters in DB after import"
|
|
);
|
|
|
|
tracing::info!("Done! Imported/updated characters.");
|
|
|
|
Ok(())
|
|
}
|