oxyromon 0.24.0

Rusty ROM OrgaNizer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
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 {
    /// 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;
    // The `SELECT` list. Plain tables use `*`, but a table may carry computed
    // columns (e.g. `System`'s completion counts) that are not real columns and
    // so are absent from `*` — the loader would then fail to map the row.
    const SELECT: &'static str = "*";
    fn row_id(&self) -> i64;
}

impl TableRow for System {
    const TABLE: &'static str = "systems";
    // Must match the count subqueries the System-mapping queries in database.rs
    // select, so a row loaded this way maps to the full `System`.
    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),
        ];
        // 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
    }
}