oxyromon 0.23.0

Rusty ROM OrgaNizer
use super::bchunk;
use super::chdman;
use super::ctrtool;
use super::database::*;
use super::dolphin;
use super::download_dats::find_redump_systems;
use super::flips;
use super::maxcso;
use super::model::*;
use super::nsz;
use super::sevenzip;
use super::wit;
use super::xdelta3;
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 {
    /// Human readable title, or null when it is identical to `name` and the
    /// client can simply fall back to it.
    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;
    fn row_id(&self) -> i64;
}

impl TableRow for System {
    const TABLE: &'static str = "systems";
    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::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),
            ("bchunk", bchunk::get_version().await),
            ("chdman", chdman::get_version().await),
            ("ctrtool", ctrtool::get_version().await),
            ("flips", flips::get_version().await),
            ("maxcso", maxcso::get_version().await),
            ("nsz", nsz::get_version().await),
            ("xdelta3", xdelta3::get_version().await),
        ];
        // RVZ and WBFS share a backend when it is the native one, so list it once
        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())
    }

    /// Redump systems the server can fetch: those not yet in the database, or
    /// with `update`, those already in it whose DATs can be refreshed.
    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)
    }

    /// Games of a system, ordered by name.
    ///
    /// `offset` and `limit` let a client pull a large system in pieces rather
    /// than waiting on one multi megabyte response; omitting `limit` returns
    /// everything.
    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
    }
}