game-scanner 1.1.0

Game Scanner for any launcher and OS
use std::path::{Path, PathBuf};

use rusqlite::{params_from_iter, Connection, OpenFlags, Row};

use crate::{
    error::{Error, ErrorKind, Result},
    prelude::{Game, GameType},
    utils::sqlite::repeat_vars,
};

pub fn read_all(file: &Path, launcher_executable: &Path) -> Result<Vec<Game>> {
    let conn =
        Connection::open_with_flags(&file, OpenFlags::SQLITE_OPEN_READ_ONLY).map_err(|error| {
            Error::new(
                ErrorKind::InvalidManifest,
                format!("Invalid GOG manifest: {}", error.to_string()),
            )
        })?;

    let mut manifest_ids = Vec::new();

    {
        let mut stmt = conn
            .prepare("SELECT releaseKey FROM LibraryReleases WHERE releaseKey LIKE 'gog_%'")
            .map_err(|error| {
                Error::new(
                    ErrorKind::InvalidManifest,
                    format!(
                        "Error to read the GOG manifest (LibraryReleases): {}",
                        error.to_string()
                    ),
                )
            })?;

        let rows = stmt
            .query_map([], |row| parse_library_releases(row))
            .map_err(|error| match error {
                rusqlite::Error::QueryReturnedNoRows => Error::new(
                    ErrorKind::LibraryNotFound,
                    format!("GOG library could be empty"),
                ),
                _ => Error::new(
                    ErrorKind::InvalidManifest,
                    format!(
                        "Error to read the GOG manifest (LibraryReleases): {}",
                        error.to_string()
                    ),
                ),
            })?;

        for id in rows {
            manifest_ids.push(id?.replace("gog_", ""));
        }
    }

    let mut stmt = conn
        .prepare(
            format!(
                "SELECT * FROM LimitedDetails WHERE productId IN ({})",
                repeat_vars(manifest_ids.len())
            )
            .as_str(),
        )
        .map_err(|error| {
            Error::new(
                ErrorKind::InvalidManifest,
                format!(
                    "Error to read the GOG manifest (LimitedDetails): {}",
                    error.to_string()
                ),
            )
        })?;

    let game_columns = stmt
        .column_names()
        .into_iter()
        .map(String::from)
        .collect::<Vec<String>>();

    let manifest_games = stmt
        .query_map(params_from_iter(manifest_ids.iter()), |row| {
            parse_limited_details(&game_columns, row, launcher_executable)
        })
        .map_err(|error| {
            Error::new(
                ErrorKind::InvalidManifest,
                format!(
                    "Error to read the GOG manifest (LimitedDetails): {}",
                    error.to_string()
                ),
            )
        })?;

    let mut games = Vec::<Game>::new();

    for game in manifest_games {
        let mut game = game?;

        let path = conn
            .prepare("SELECT installationPath FROM InstalledBaseProducts WHERE productId = :id")
            .and_then(|mut statement| {
                statement.query_row(&[(":id", &game.id)], parse_installed_base_product)
            })
            .map_err(|error| match error {
                rusqlite::Error::QueryReturnedNoRows => Error::new(
                    ErrorKind::LibraryNotFound,
                    format!("GOG library could be empty"),
                ),
                _ => Error::new(
                    ErrorKind::InvalidManifest,
                    format!(
                        "Error to read the GOG manifest (Manifests): {}",
                        error.to_string()
                    ),
                ),
            });

        match path {
            Ok(path) => {
                game.path = Some(path.clone());
                game.commands.launch = Some(vec![
                    launcher_executable.display().to_string(),
                    String::from("/command=runGame"),
                    format!("/gameId={}", &game.id),
                    format!("/path={}", &path.display().to_string()),
                ]);
                game.state.installed = true;
            }
            Err(error) => match error.kind() {
                ErrorKind::LibraryNotFound => {
                    game.state.installed = false;
                }
                _ => {
                    return Err(error);
                }
            },
        }

        games.push(game);
    }

    return Ok(games);
}

pub fn read(id: &str, file: &Path, launcher_executable: &Path) -> Result<Game> {
    let conn =
        Connection::open_with_flags(&file, OpenFlags::SQLITE_OPEN_READ_ONLY).map_err(|error| {
            Error::new(
                ErrorKind::InvalidManifest,
                format!("Invalid GOG manifest: {}", error.to_string()),
            )
        })?;

    let mut stmt = conn
        .prepare("SELECT * FROM LimitedDetails WHERE productId = :id")
        .map_err(|error| {
            Error::new(
                ErrorKind::InvalidManifest,
                format!(
                    "Error to read the GOG manifest (LimitedDetails): {}",
                    error.to_string()
                ),
            )
        })?;

    let game_columns = stmt
        .column_names()
        .into_iter()
        .map(String::from)
        .collect::<Vec<String>>();

    let mut game = stmt
        .query_row(&[(":id", &id)], |row| {
            parse_limited_details(&game_columns, row, launcher_executable)
        })
        .map_err(|error| {
            Error::new(
                ErrorKind::InvalidManifest,
                format!(
                    "Error to read the GOG manifest (LimitedDetails): {}",
                    error.to_string()
                ),
            )
        })?;

    let path = conn
        .prepare("SELECT installationPath FROM InstalledBaseProducts WHERE productId = :id")
        .map_err(|error| {
            Error::new(
                ErrorKind::InvalidManifest,
                format!(
                    "Error to read the GOG manifest (Manifests): {}",
                    error.to_string()
                ),
            )
        })
        .and_then(|mut statement| {
            statement
                .query_row(&[(":id", &game.id)], parse_installed_base_product)
                .map_err(|error| match error {
                    rusqlite::Error::QueryReturnedNoRows => Error::new(
                        ErrorKind::LibraryNotFound,
                        format!("GOG library could be empty"),
                    ),
                    _ => Error::new(
                        ErrorKind::InvalidManifest,
                        format!(
                            "Error to read the GOG manifest (Manifests): {}",
                            error.to_string()
                        ),
                    ),
                })
        });

    match path {
        Ok(path) => {
            game.path = Some(path.clone());
            game.commands.launch = Some(vec![
                launcher_executable.display().to_string(),
                String::from("/command=runGame"),
                format!("/gameId={}", &game.id),
                format!("/path={}", &path.display().to_string()),
            ]);

            if cfg!(target_os = "windows") {
                game.commands.uninstall =
                    Some(vec![path.join("unins000.exe").display().to_string()]);
            }

            game.state.installed = true;
        }
        Err(error) => match error.kind() {
            ErrorKind::LibraryNotFound => {
                game.state.installed = false;
            }
            _ => {
                return Err(error);
            }
        },
    }

    Ok(game)
}

fn parse_library_releases(row: &Row) -> rusqlite::Result<String> {
    row.get::<_, String>(0)
}

fn parse_limited_details(
    columns: &Vec<String>,
    row: &Row,
    launcher_executable: &Path,
) -> rusqlite::Result<Game> {
    let mut game = Game::default();
    game._type = GameType::GOG.to_string();

    for col in 0..columns.len() {
        let name = columns.get(col).unwrap();

        match name.as_str() {
            "productId" => game.id = row.get::<_, i64>(col)?.to_string(),
            "title" => game.name = row.get(col)?,
            _ => {}
        }
    }

    game.commands.install = Some(vec![
        launcher_executable.display().to_string(),
        String::from("/command=installGame"),
        format!("/gameId={}", &game.id),
    ]);

    Ok(game)
}

fn parse_installed_base_product(row: &Row) -> rusqlite::Result<PathBuf> {
    row.get::<_, String>(0).map(PathBuf::from)
}