Files
BoilR/src/steamgriddb/downloader.rs
T
Philip KristoffersenandGitHub 27b429adcd Inline print variables (#328)
* Inline format messages

* Cargo auto fixes
2023-02-02 21:33:42 +01:00

450 lines
16 KiB
Rust

use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::{collections::HashMap, path::Path};
use futures::{stream, StreamExt};
use serde::{Deserialize, Serialize};
use std::error::Error;
use steamgriddb_api::query_parameters::{
GridDimentions, MimeType, MimeTypeIcon, MimeTypeLogo, Nsfw,
};
use tokio::sync::watch::Sender; // 0.3.1
use steam_shortcuts_util::shortcut::ShortcutOwned;
use steamgriddb_api::Client;
use super::CachedSearch;
use crate::settings::Settings;
use crate::steam::{get_shortcuts_for_user, get_users_images, SteamUsersInfo};
use crate::steamgriddb::ImageType;
use crate::sync::IsBoilRShortcut;
use crate::sync::SyncProgress;
const CONCURRENT_REQUESTS: usize = 10;
impl SearchSettings for Settings {
fn download_animated(&self) -> bool {
self.steamgrid_db.prefer_animated
}
fn download_big_picture(&self) -> bool {
self.steam.optimize_for_big_picture
}
fn allow_nsfw(&self) -> bool {
self.steamgrid_db.allow_nsfw
}
fn only_download_boilr_images(&self) -> bool {
self.steamgrid_db.only_download_boilr_images
}
fn is_image_banned(&self, image_type: &ImageType, app_id: u32) -> bool {
self.steamgrid_db.is_image_banned(image_type, app_id)
}
}
pub async fn download_images_for_users<'b>(
settings: &Settings,
users: &[SteamUsersInfo],
sender: &mut Option<Sender<SyncProgress>>,
) {
let auth_key = &settings.steamgrid_db.auth_key;
if let Some(auth_key) = auth_key {
println!("Checking for game images");
let start_time = std::time::Instant::now();
let client = steamgriddb_api::Client::new(auth_key);
let search = CachedSearch::new(&client);
let search = &search;
let client = &client;
if let Some(sender) = sender {
let _ = sender.send(SyncProgress::FindingImages);
}
let users_info = users.iter().filter_map(|user| {
let shortcut_info = get_shortcuts_for_user(user);
shortcut_info
.map(|shortcut_info| {
let data_folder = &user.steam_user_data_folder;
(shortcut_info, data_folder)
})
.ok()
});
let to_downloads = stream::iter(users_info)
.map(|(shortcut_info, data_folder)| async move {
let known_images = get_users_images(data_folder).unwrap_or_default();
let res = search_for_images_to_download(
known_images,
data_folder.as_str(),
&shortcut_info.shortcuts,
search,
client,
settings,
)
.await;
res.unwrap_or_default()
})
.buffer_unordered(CONCURRENT_REQUESTS)
.collect::<Vec<Vec<ToDownload>>>()
.await;
let to_downloads = to_downloads.iter().flatten().collect::<Vec<&ToDownload>>();
let total = to_downloads.len();
if !to_downloads.is_empty() {
if let Some(sender) = sender {
let _ = sender.send(SyncProgress::DownloadingImages { to_download: total });
}
search.save();
stream::iter(&to_downloads)
.map(|to_download| async move {
if let Err(e) = download_to_download(to_download).await {
println!("Error downloading {:?}: {}", &to_download.path, e);
}
})
.buffer_unordered(CONCURRENT_REQUESTS)
.collect::<Vec<()>>()
.await;
let duration = start_time.elapsed();
println!("Finished getting images in: {duration:?}");
//Validate that the downloads where ok
for to_download in to_downloads {
let file_length = std::fs::metadata(&to_download.path)
.map(|m| m.len())
.unwrap_or_default();
if file_length < 2 {
// Image is too small, something went wrong
//Try to delete file again, don't care if it fails
let _ = std::fs::remove_file(&to_download.path);
}
}
} else {
println!("No images needed");
}
} else {
println!("Steamgrid DB Auth Key not found, please add one as described here: https://github.com/PhilipK/steam_shortcuts_sync#configuration");
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PublicGameResponseMetadata {
store_asset_mtime: Option<u64>,
clienticon: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PublicGameResponseSteam {
id: String,
metadata: Option<PublicGameResponseMetadata>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PublicGameResponsePlatforms {
steam: Option<PublicGameResponseSteam>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PublicGameResponseData {
platforms: Option<PublicGameResponsePlatforms>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PublicGameResponse {
success: bool,
data: Option<PublicGameResponseData>,
}
pub trait SearchSettings {
fn download_animated(&self) -> bool;
fn download_big_picture(&self) -> bool;
fn allow_nsfw(&self) -> bool;
fn only_download_boilr_images(&self) -> bool;
fn is_image_banned(&self, image_type: &ImageType, app_id: u32) -> bool;
}
async fn search_for_images_to_download<T: SearchSettings>(
known_images: Vec<String>,
user_data_folder: &str,
shortcuts: &[ShortcutOwned],
search: &CachedSearch<'_>,
client: &Client,
search_settins: &T,
) -> Result<Vec<ToDownload>, Box<dyn Error>> {
let types = {
let mut types = vec![
ImageType::Logo,
ImageType::Hero,
ImageType::Grid,
ImageType::WideGrid,
ImageType::Icon,
];
if search_settins.download_big_picture() {
types.push(ImageType::BigPicture);
}
types
};
let shortcuts_to_search_for = shortcuts
.iter()
.filter(|s| !search_settins.only_download_boilr_images() || s.is_boilr_shortcut())
.filter(|s| {
// if we are missing any of the images we need to search for them
types
.iter()
.map(|t| t.file_name_no_extension(s.app_id))
.any(|image| !known_images.contains(&image))
&& !s.app_name.is_empty()
});
let shortcuts_to_search_for: Vec<&ShortcutOwned> = shortcuts_to_search_for.collect();
if shortcuts_to_search_for.is_empty() {
return Ok(vec![]);
}
let mut search_results = HashMap::new();
let search_results_a = stream::iter(shortcuts_to_search_for)
.map(|s| async move {
let search_result = search.search(s.app_id, &s.app_name).await;
match search_result {
Ok(Some(search_result)) => Some((s.app_id, search_result)),
_ => None,
}
})
.buffer_unordered(CONCURRENT_REQUESTS)
.collect::<Vec<Option<(u32, usize)>>>()
.await;
for (app_id, search) in search_results_a.into_iter().flatten() {
search_results.insert(app_id, search);
}
let mut to_download = vec![];
let grid_folder = Path::new(user_data_folder).join("config").join("grid");
for image_type in types {
let images_needed = shortcuts
.iter()
.filter(|s| search_results.contains_key(&s.app_id))
.filter(|s| !search_settins.is_image_banned(&image_type, s.app_id))
.filter(|s| !known_images.contains(&image_type.file_name_no_extension(s.app_id)));
let image_ids: Vec<usize> = images_needed
.clone()
.filter_map(|s| search_results.get(&s.app_id))
.copied()
.collect();
let shortcuts: Vec<&ShortcutOwned> = images_needed.collect();
for image_ids in image_ids.chunks(99) {
let image_search_result = get_images_for_ids(
client,
image_ids,
&image_type,
search_settins.download_animated(),
search_settins.allow_nsfw(),
)
.await;
match image_search_result {
Ok(images) => {
let images = images.iter().enumerate().filter_map(|(index, image)| {
if let (Some(shortcut), Some(image_id)) =
(shortcuts.get(index), image_ids.get(index))
{
Some((image, shortcut, image_id))
} else {
None
}
});
let download_for_this_type = stream::iter(images)
.filter_map(|(image, shortcut, game_id)| {
let extension = image
.as_ref()
.map(|image| get_image_extension(&image.mime))
.unwrap_or("png");
let path =
grid_folder.join(image_type.file_name(shortcut.app_id, extension));
async move {
let image_url = match image {
Ok(img) => Some(img.url.clone()),
Err(_) => get_steam_image_url(*game_id, &image_type).await,
};
image_url.map(|url| ToDownload {
path,
url,
app_name: shortcut.app_name.clone(),
image_type,
})
}
})
.collect::<Vec<ToDownload>>()
.await;
to_download.extend(download_for_this_type);
}
Err(err) => eprintln!("Error getting images: {err}"),
}
}
}
Ok(to_download)
}
pub fn get_image_extension(mime_type: &steamgriddb_api::images::MimeTypes) -> &'static str {
match mime_type {
steamgriddb_api::images::MimeTypes::Default(MimeType::Jpeg) => "jpg",
steamgriddb_api::images::MimeTypes::Default(MimeType::Png) => "png",
steamgriddb_api::images::MimeTypes::Default(MimeType::Webp) => "webp",
steamgriddb_api::images::MimeTypes::Logo(MimeTypeLogo::Png) => "png",
steamgriddb_api::images::MimeTypes::Logo(MimeTypeLogo::Webp) => "webp",
steamgriddb_api::images::MimeTypes::Icon(MimeTypeIcon::Icon) => "ico",
steamgriddb_api::images::MimeTypes::Icon(MimeTypeIcon::Png) => "png",
}
}
async fn get_images_for_ids(
client: &Client,
image_ids: &[usize],
image_type: &ImageType,
download_animated: bool,
allow_nsfw: bool,
) -> Result<Vec<steamgriddb_api::response::SteamGridDbResult<steamgriddb_api::images::Image>>, String>
{
let query_type = get_query_type(download_animated, image_type, allow_nsfw);
let image_search_result = client.get_images_for_ids(image_ids, &query_type).await;
image_search_result.map_err(|e| format!("Image search failed {e:?}"))
}
const BIG_PICTURE_DIMS: [GridDimentions; 2] = [GridDimentions::D920x430, GridDimentions::D460x215];
pub fn get_query_type(
download_animated: bool,
image_type: &ImageType,
allow_nsfw: bool,
) -> steamgriddb_api::QueryType {
let anymation_type = if download_animated {
Some(&[steamgriddb_api::query_parameters::AnimtionType::Animated][..])
} else {
None
};
use steamgriddb_api::query_parameters::GridQueryParameters;
let allow_nsfw_enum = match allow_nsfw {
true => Some(&Nsfw::Any),
false => Some(&Nsfw::False),
};
let big_picture_parameters = GridQueryParameters {
dimentions: Some(&BIG_PICTURE_DIMS),
types: anymation_type,
nsfw: allow_nsfw_enum,
..Default::default()
};
use steamgriddb_api::query_parameters::HeroQueryParameters;
let hero_parameters = HeroQueryParameters {
types: anymation_type,
nsfw: allow_nsfw_enum,
..Default::default()
};
let grid_parameters = GridQueryParameters {
types: anymation_type,
nsfw: allow_nsfw_enum,
..Default::default()
};
use steamgriddb_api::query_parameters::LogoQueryParameters;
let logo_parameters = LogoQueryParameters {
types: anymation_type,
nsfw: allow_nsfw_enum,
..Default::default()
};
use steamgriddb_api::query_parameters::IconQueryParameters;
let icon_parameters = IconQueryParameters {
nsfw: allow_nsfw_enum,
..Default::default()
};
let query_type = match image_type {
ImageType::Hero => steamgriddb_api::QueryType::Hero(Some(hero_parameters)),
ImageType::BigPicture => steamgriddb_api::QueryType::Grid(Some(big_picture_parameters)),
ImageType::Grid => steamgriddb_api::QueryType::Grid(Some(grid_parameters)),
ImageType::WideGrid => steamgriddb_api::QueryType::Grid(Some(big_picture_parameters)),
ImageType::Logo => steamgriddb_api::QueryType::Logo(Some(logo_parameters)),
ImageType::Icon => steamgriddb_api::QueryType::Icon(Some(icon_parameters)),
};
query_type
}
async fn get_steam_image_url(game_id: usize, image_type: &ImageType) -> Option<String> {
if let ImageType::Icon = image_type {
if let Some(url) = get_steam_icon_url(game_id).await {
return Some(url);
}
}
let steamgriddb_page_url = format!("https://www.steamgriddb.com/api/public/game/{game_id}/");
let response = reqwest::get(steamgriddb_page_url).await;
if let Ok(response) = response {
let text_response = response.json::<PublicGameResponse>().await;
if let Ok(response) = text_response {
let game_id = response
.data
.clone()
.map(|d| d.platforms.map(|p| p.steam.map(|s| s.id)));
let mtime = response.data.map(|d| {
d.platforms
.map(|p| p.steam.map(|s| s.metadata.map(|m| m.store_asset_mtime)))
});
if let (Some(Some(Some(steam_app_id))), Some(Some(Some(Some(Some(mtime)))))) =
(game_id, mtime)
{
return Some(image_type.steam_url(steam_app_id, mtime));
}
}
}
None
}
async fn get_steam_icon_url(game_id: usize) -> Option<String> {
let steamgriddb_page_url = format!("https://www.steamgriddb.com/api/public/game/{game_id}/");
let response = reqwest::get(steamgriddb_page_url).await;
if let Ok(response) = response {
let text_response = response.json::<PublicGameResponse>().await;
if let Ok(response) = text_response {
let game_id = response
.data
.clone()
.map(|d| d.platforms.map(|p| p.steam.map(|s| s.id)));
let mtime = response.data.map(|d| {
d.platforms
.map(|p| p.steam.map(|s| s.metadata.map(|m| m.clienticon)))
});
if let (Some(Some(Some(steam_app_id))), Some(Some(Some(Some(Some(mtime)))))) =
(game_id, mtime)
{
return Some(icon_url(&steam_app_id, &mtime));
}
}
}
None
}
fn icon_url(steam_app_id: &str, icon_id: &str) -> String {
format!(
"https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/{steam_app_id}/{icon_id}.ico"
)
}
pub async fn download_to_download(to_download: &ToDownload) -> eyre::Result<()> {
println!(
"Downloading {:?} for {} to {:?}",
to_download.image_type, to_download.app_name, to_download.path
);
let path = &to_download.path;
let url = &to_download.url;
let mut file = File::create(path)?;
let response = reqwest::get(url).await?;
let content = response.bytes().await?;
file.write_all(&content)?;
Ok(())
}
pub struct ToDownload {
pub path: PathBuf,
pub url: String,
pub app_name: String,
pub image_type: ImageType,
}