feat: Add game thumbnail fetching from external APIs, migrate

This commit is contained in:
2025-12-04 15:15:47 +01:00
parent f49900adad
commit 04e21708b2
7 changed files with 882 additions and 59 deletions
+102 -21
View File
@@ -1,5 +1,5 @@
use rocket::fs::FileServer;
use std::sync::Mutex;
use rocket::futures::lock::Mutex;
use backend::auth;
use backend::items::{self, Game};
@@ -10,12 +10,12 @@ use backend::store::{self, User, save_state};
extern crate rocket;
#[get("/<name>")]
fn get_user(
async fn get_user(
_token: auth::Token,
user_list: &rocket::State<Mutex<Vec<User>>>,
name: &str,
) -> Option<items::Person> {
let users = user_list.lock().unwrap();
let users = user_list.lock().await;
users
.iter()
.find(|user| user.person.name == name)
@@ -23,42 +23,45 @@ fn get_user(
}
#[get("/")]
fn get_users(
async fn get_users(
_token: auth::Token,
user_list: &rocket::State<Mutex<Vec<User>>>,
) -> items::PersonList {
let users = user_list.lock().unwrap();
let users = user_list.lock().await;
items::PersonList {
person: users.iter().map(|u| u.person.clone()).collect(),
}
}
#[get("/game/<title>")]
fn get_game(
async fn get_game(
_token: auth::Token,
game_list: &rocket::State<Mutex<Vec<Game>>>,
title: &str,
) -> Option<items::Game> {
let games = game_list.lock().unwrap();
let games = game_list.lock().await;
games.iter().find(|g| g.title == title).cloned()
}
#[get("/games")]
fn get_games(_token: auth::Token, game_list: &rocket::State<Mutex<Vec<Game>>>) -> items::GameList {
let games = game_list.lock().unwrap();
async fn get_games(
_token: auth::Token,
game_list: &rocket::State<Mutex<Vec<Game>>>,
) -> items::GameList {
let games = game_list.lock().await;
items::GameList {
games: games.clone(),
}
}
#[post("/game", data = "<game>")]
fn add_game(
async fn add_game(
_token: auth::Token,
game_list: &rocket::State<Mutex<Vec<Game>>>,
user_list: &rocket::State<Mutex<Vec<User>>>,
game: proto_utils::Proto<items::Game>,
) -> Option<items::Game> {
let mut games = game_list.lock().unwrap();
let mut games = game_list.lock().await;
let game = game.into_inner();
if games.iter().any(|g| g.title == game.title) {
@@ -67,21 +70,21 @@ fn add_game(
games.push(game.clone());
let users = user_list.lock().unwrap();
let users = user_list.lock().await;
save_state(&games, &users);
Some(game)
}
#[post("/opinion", data = "<req>")]
fn add_opinion(
async fn add_opinion(
token: auth::Token,
user_list: &rocket::State<Mutex<Vec<User>>>,
game_list: &rocket::State<Mutex<Vec<Game>>>,
req: proto_utils::Proto<items::AddOpinionRequest>,
) -> Option<items::Person> {
let mut users = user_list.lock().unwrap();
let games = game_list.lock().unwrap();
let mut users = user_list.lock().await;
let games = game_list.lock().await;
let mut result = None;
// Validate game exists
@@ -115,6 +118,68 @@ fn add_opinion(
result
}
#[get("/game_thumbnail/<title>")]
async fn get_game_thumbnail(
title: &str,
game_list: &rocket::State<Mutex<Vec<Game>>>,
) -> Option<(rocket::http::ContentType, Vec<u8>)> {
let games = game_list.lock().await;
let game = games.iter().find(|g| g.title == title)?;
let url = match items::Source::try_from(game.source).ok()? {
items::Source::Steam => format!(
"https://cdn.cloudflare.steamstatic.com/steam/apps/{}/header.jpg",
game.remote_id
),
items::Source::Roblox => {
let universe_id = {
let api_url = format!(
"https://apis.roblox.com/universes/v1/places/{}/universe",
game.remote_id
);
reqwest::get(&api_url)
.await
.ok()?
.json::<serde_json::Value>()
.await
.ok()?
.get("universeId")?
.as_u64()
.unwrap()
};
let api_url = format!(
"https://thumbnails.roblox.com/v1/games/icons?universeIds={}&size=512x512&format=Webp&isCircular=false",
universe_id
);
match reqwest::get(&api_url).await {
Ok(resp) => {
if let Ok(json) = resp.json::<serde_json::Value>().await {
json["data"][0]["imageUrl"].as_str()?.to_string()
} else {
return None;
}
}
Err(_) => return None,
}
}
};
match reqwest::get(&url).await {
Ok(resp) => {
let content_type = resp
.headers()
.get(reqwest::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.and_then(rocket::http::ContentType::parse_flexible)
.unwrap_or(rocket::http::ContentType::Binary);
let bytes = resp.bytes().await.ok()?.to_vec();
Some((content_type, bytes))
}
Err(_) => None,
}
}
#[get("/<_..>", rank = 20)]
async fn index_fallback() -> Option<rocket::fs::NamedFile> {
// Try multiple paths for robustness
@@ -139,16 +204,31 @@ async fn main() -> Result<(), std::io::Error> {
min_players: 1,
max_players: 90,
price: 0,
remote_id: 0,
remote_id: 6032337657, // Universe ID for Naramo Nuclear Plant V2
});
game_list.push(Game {
title: "Terraria".to_string(),
source: items::Source::Steam.into(),
min_players: 1,
max_players: 8,
price: 999,
remote_id: 105600, // App ID for Terraria
});
user_list.push(User {
person: items::Person {
name: "John".to_string(),
opinion: vec![items::Opinion {
title: "Naramo Nuclear Plant V2".to_string(),
would_play: true,
}],
opinion: vec![
items::Opinion {
title: "Naramo Nuclear Plant V2".to_string(),
would_play: true,
},
items::Opinion {
title: "Terraria".to_string(),
would_play: true,
},
],
},
password_hash: bcrypt::hash("password123", bcrypt::DEFAULT_COST).unwrap(),
});
@@ -167,7 +247,8 @@ async fn main() -> Result<(), std::io::Error> {
get_game,
get_games,
add_opinion,
add_game
add_game,
get_game_thumbnail
],
)
.mount(