use super::chdman;
use super::ctrtool;
use super::database::*;
use super::dolphin;
use super::download_dats::find_redump_systems;
use super::flips;
use super::model::*;
use super::nsz;
use super::sevenzip;
use super::wit;
use super::xdelta3;
use super::xso;
use async_graphql::dataloader::{DataLoader, Loader};
use async_graphql::{ComplexObject, Context, Error, Object, Result};
use futures::stream::TryStreamExt;
use itertools::Itertools;
use num_traits::FromPrimitive;
use shiratsu_naming::naming::TokenizedName;
use shiratsu_naming::naming::nointro::{NoIntroName, NoIntroToken};
use shiratsu_naming::region::Region;
use sqlx::sqlite::SqliteRow;
use sqlx::{AssertSqlSafe, FromRow, SqlitePool};
use std::collections::HashMap;
use std::marker::PhantomData;
async fn system_size(
ctx: &Context<'_>,
system_id: i64,
actual: bool,
one_region: bool,
) -> Result<i64> {
let pool = ctx.data_unchecked::<SqlitePool>();
let sorting = if one_region {
"AND g.sorting = 1\n "
} else {
""
};
let sql = if actual {
format!(
"
SELECT COALESCE(SUM(rf.size), 0)
FROM romfiles AS rf
WHERE rf.id IN (
SELECT DISTINCT(r.romfile_id) FROM roms AS r
JOIN games AS g ON r.game_id = g.id
WHERE r.romfile_id IS NOT NULL
{}AND g.system_id = {}
);
",
sorting, system_id
)
} else {
format!(
"
SELECT COALESCE(SUM(r.size), 0)
FROM roms AS r
JOIN games AS g ON r.game_id = g.id
WHERE r.romfile_id IS NOT NULL
{}AND g.system_id = {};
",
sorting, system_id
)
};
let row: (i64,) = sqlx::query_as(AssertSqlSafe(sql.as_str()))
.fetch_one(&mut *pool.acquire().await.unwrap())
.await?;
Ok(row.0)
}
#[ComplexObject]
impl System {
async fn header(&self, ctx: &Context<'_>) -> Result<Option<Header>> {
let pool = ctx.data_unchecked::<SqlitePool>();
Ok(find_header_by_system_id(&mut pool.acquire().await.unwrap(), self.id).await)
}
}
#[ComplexObject]
impl Game {
async fn description(&self) -> Option<&str> {
if self.description == self.name {
None
} else {
Some(&self.description)
}
}
async fn system(&self, ctx: &Context<'_>) -> Result<Option<System>> {
ctx.data_unchecked::<DataLoader<SystemLoader>>()
.load_one(self.system_id)
.await
}
}
#[ComplexObject]
impl Rom {
async fn game(&self, ctx: &Context<'_>) -> Result<Option<Game>> {
ctx.data_unchecked::<DataLoader<GameLoader>>()
.load_one(self.game_id)
.await
}
async fn romfile(&self, ctx: &Context<'_>) -> Result<Option<Romfile>> {
Ok(match self.romfile_id {
Some(romfile_id) => {
ctx.data_unchecked::<DataLoader<RomfileLoader>>()
.load_one(romfile_id)
.await?
}
None => None,
})
}
async fn ignored(&self, ctx: &Context<'_>, system_id: i64) -> Result<bool> {
let pool = ctx.data_unchecked::<SqlitePool>();
let system_loader = ctx.data_unchecked::<DataLoader<SystemLoader>>();
let system = system_loader.load_one(system_id).await?.unwrap();
let game_loader = ctx.data_unchecked::<DataLoader<GameLoader>>();
let game = game_loader.load_one(self.game_id).await?.unwrap();
if game.sorting == Sorting::Ignored as i64 {
return Ok(true);
}
if !system.arcade || self.parent_id.is_none() {
return Ok(false);
}
let merging = Merging::from_i64(system.merging).unwrap();
let ignored = match merging {
Merging::Split => true,
Merging::NonMerged | Merging::Merged => {
let sql = format!(
"
SELECT g.bios
FROM roms AS r
JOIN games AS g ON r.game_id = g.id
WHERE r.id = {};
",
self.parent_id.unwrap()
);
let row: (bool,) = sqlx::query_as(AssertSqlSafe(sql.as_str()))
.fetch_one(&mut *pool.acquire().await.unwrap())
.await?;
row.0
}
Merging::FullNonMerged | Merging::FullMerged => false,
};
Ok(ignored)
}
}
pub struct IdLoader<T: TableRow> {
pub pool: SqlitePool,
_marker: PhantomData<T>,
}
impl<T: TableRow> IdLoader<T> {
pub fn new(pool: SqlitePool) -> Self {
IdLoader {
pool,
_marker: PhantomData,
}
}
}
pub type SystemLoader = IdLoader<System>;
pub type GameLoader = IdLoader<Game>;
pub type RomfileLoader = IdLoader<Romfile>;
pub(crate) trait TableRow:
for<'r> FromRow<'r, SqliteRow> + Clone + Send + Sync + Unpin + 'static
{
const TABLE: &'static str;
const SELECT: &'static str = "*";
fn row_id(&self) -> i64;
}
impl TableRow for System {
const TABLE: &'static str = "systems";
const SELECT: &'static str = "systems.*, \
(SELECT COUNT(*) FROM games WHERE games.system_id = systems.id AND games.completion = 2 AND games.sorting != 2) AS games_complete, \
(SELECT COUNT(*) FROM games WHERE games.system_id = systems.id AND games.sorting != 2) AS games_total";
fn row_id(&self) -> i64 {
self.id
}
}
impl TableRow for Game {
const TABLE: &'static str = "games";
fn row_id(&self) -> i64 {
self.id
}
}
impl TableRow for Romfile {
const TABLE: &'static str = "romfiles";
fn row_id(&self) -> i64 {
self.id
}
}
impl<T: TableRow + 'static> Loader<i64> for IdLoader<T> {
type Value = T;
type Error = Error;
async fn load(&self, ids: &[i64]) -> Result<HashMap<i64, Self::Value>, Self::Error> {
let sql = format!(
"
SELECT {}
FROM {}
WHERE id in ({})
",
T::SELECT,
T::TABLE,
ids.iter().join(",")
);
Ok(sqlx::query_as(AssertSqlSafe(sql.as_str()))
.fetch(&mut *self.pool.acquire().await.unwrap())
.map_ok(|row: T| (row.row_id(), row))
.try_collect()
.await?)
}
}
pub struct QueryRoot;
#[Object]
impl QueryRoot {
async fn version(&self) -> Result<String> {
Ok(env!("CARGO_PKG_VERSION").to_string())
}
async fn dependencies(&self) -> Result<Vec<Dependency>> {
let mut deps = vec![
("7-zip", sevenzip::get_version().await),
("chdman", chdman::get_version().await),
("ctrtool", ctrtool::get_version().await),
("flips", flips::get_version().await),
("xso-rs", xso::get_version().await),
("nsz-rs", nsz::get_version().await),
("xdelta3", xdelta3::get_version().await),
];
deps.push((dolphin::BACKEND_NAME, dolphin::get_version().await));
if wit::BACKEND_NAME != dolphin::BACKEND_NAME {
deps.push((wit::BACKEND_NAME, wit::get_version().await));
}
deps.sort_by_key(|(name, _)| *name);
Ok(deps
.into_iter()
.map(|(name, result)| Dependency {
name: name.to_string(),
version: result.ok(),
})
.collect())
}
async fn downloadable_systems(
&self,
ctx: &Context<'_>,
#[graphql(default = false)] update: bool,
) -> Result<Vec<String>> {
let pool = ctx.data_unchecked::<SqlitePool>();
Ok(find_redump_systems(&mut pool.acquire().await.unwrap(), update).await)
}
async fn system_count(&self, ctx: &Context<'_>) -> Result<i64> {
let pool = ctx.data_unchecked::<SqlitePool>();
Ok(count_systems(&mut pool.acquire().await.unwrap()).await)
}
async fn game_count(&self, ctx: &Context<'_>) -> Result<i64> {
let pool = ctx.data_unchecked::<SqlitePool>();
Ok(count_games(&mut pool.acquire().await.unwrap()).await)
}
async fn rom_count(&self, ctx: &Context<'_>) -> Result<i64> {
let pool = ctx.data_unchecked::<SqlitePool>();
Ok(count_roms(&mut pool.acquire().await.unwrap()).await)
}
async fn settings(&self, ctx: &Context<'_>) -> Result<Vec<Setting>> {
log::debug!("query::get settings()");
let pool = ctx.data_unchecked::<SqlitePool>();
Ok(find_settings(&mut pool.acquire().await.unwrap(), None).await)
}
async fn system_settings(&self, ctx: &Context<'_>, system_id: i64) -> Result<Vec<Setting>> {
log::debug!("query::system_settings({})", system_id);
let pool = ctx.data_unchecked::<SqlitePool>();
let mut conn = pool.acquire().await.unwrap();
let global = find_settings(&mut conn, None).await;
let overrides = find_settings(&mut conn, Some(system_id)).await;
let mut merged: HashMap<String, Setting> =
global.into_iter().map(|s| (s.key.clone(), s)).collect();
for setting in overrides {
merged.insert(setting.key.clone(), setting);
}
let mut result: Vec<Setting> = merged.into_values().collect();
result.sort_by(|a, b| a.key.cmp(&b.key));
Ok(result)
}
async fn systems(&self, ctx: &Context<'_>) -> Result<Vec<System>> {
let pool = ctx.data_unchecked::<SqlitePool>();
Ok(find_systems(&mut pool.acquire().await.unwrap()).await)
}
async fn games(
&self,
ctx: &Context<'_>,
system_id: i64,
offset: Option<i64>,
limit: Option<i64>,
) -> Result<Vec<Game>> {
let pool = ctx.data_unchecked::<SqlitePool>();
let connection = &mut pool.acquire().await.unwrap();
Ok(match limit {
Some(limit) => {
find_games_by_system_id_paged(connection, system_id, offset.unwrap_or(0), limit)
.await
}
None => find_games_by_system_id(connection, system_id).await,
})
}
async fn game_information(&self, game_name: String) -> Result<GameInformation> {
let mut title: String = String::new();
let mut regions: Vec<String> = vec![];
let mut languages: Vec<String> = vec![];
let mut release: Option<String> = None;
let mut flags: Vec<String> = vec![];
if let Ok(name) = NoIntroName::try_parse(&game_name) {
for token in name.iter() {
if let NoIntroToken::Title(parsed_title) = token {
title = parsed_title.to_string();
} else if let NoIntroToken::Region {
region_strings: _,
regions: parsed_regions,
} = token
{
regions.append(
&mut Region::to_normalized_region_string(parsed_regions)
.split('-')
.map(|region| region.to_string())
.collect_vec(),
);
} else if let NoIntroToken::Languages(parsed_languages) = token {
languages.append(
&mut parsed_languages
.iter()
.map(|l| l.code.to_string())
.collect_vec(),
);
} else if let NoIntroToken::Release { status, number: _ } = token {
release = Some(status.to_string());
} else if let NoIntroToken::Flag { flag_type: _, flag } = token {
flags.append(&mut flag.split(", ").map(|flag| flag.to_string()).collect_vec());
}
}
}
Ok(GameInformation {
title,
regions,
languages,
release,
flags,
})
}
async fn roms(&self, ctx: &Context<'_>, game_id: i64) -> Result<Vec<Rom>> {
let pool = ctx.data_unchecked::<SqlitePool>();
Ok(find_roms_by_game_id_parents(&mut pool.acquire().await.unwrap(), game_id).await)
}
async fn total_original_size(&self, ctx: &Context<'_>, system_id: i64) -> Result<i64> {
system_size(ctx, system_id, false, false).await
}
async fn one_region_original_size(&self, ctx: &Context<'_>, system_id: i64) -> Result<i64> {
system_size(ctx, system_id, false, true).await
}
async fn total_actual_size(&self, ctx: &Context<'_>, system_id: i64) -> Result<i64> {
system_size(ctx, system_id, true, false).await
}
async fn one_region_actual_size(&self, ctx: &Context<'_>, system_id: i64) -> Result<i64> {
system_size(ctx, system_id, true, true).await
}
}