Refactor script into separate modules
This commit is contained in:
parent
88ba4dd5e5
commit
dd63fdb183
8 changed files with 371 additions and 354 deletions
|
@ -10,6 +10,8 @@ Requires Node.js.
|
||||||
* Games not yet checked on the wiki: `npm run manifest -- --unchecked`
|
* Games not yet checked on the wiki: `npm run manifest -- --unchecked`
|
||||||
* Games that had an unknown OS: `npm run manifest -- --unsuportedOs`
|
* Games that had an unknown OS: `npm run manifest -- --unsuportedOs`
|
||||||
* Games that had an unusable path: `npm run manifest -- --unsupportedPath`
|
* Games that had an unusable path: `npm run manifest -- --unsupportedPath`
|
||||||
|
* Games with paths that are too broad and are tagged as such in the wiki cache: `npm run manifest -- --tooBroad`
|
||||||
|
* Games with paths that are too broad and aren't already tagged as such in the wiki cache: `npm run manifest -- --tooBroadUntagged`
|
||||||
* A specific game: `npm run manifest -- --game "Name of Game"`
|
* A specific game: `npm run manifest -- --game "Name of Game"`
|
||||||
* Validate the manifest against the schema:
|
* Validate the manifest against the schema:
|
||||||
* `npm run schema`
|
* `npm run schema`
|
||||||
|
|
|
@ -5,12 +5,12 @@
|
||||||
"author": "Matthew T. Kennerly <mtkennerly@gmail.com>",
|
"author": "Matthew T. Kennerly <mtkennerly@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"cache": "ts-node ./src/importer.ts --cache",
|
"cache": "ts-node ./src/bin.ts --cache",
|
||||||
"manifest": "ts-node ./src/importer.ts --manifest",
|
"manifest": "ts-node ./src/bin.ts --manifest",
|
||||||
"schema": "npm run schema:normal && npm run schema:strict",
|
"schema": "npm run schema:normal && npm run schema:strict",
|
||||||
"schema:normal": "ajv validate -s ./data/schema.yaml -d ./data/manifest.yaml",
|
"schema:normal": "ajv validate -s ./data/schema.yaml -d ./data/manifest.yaml",
|
||||||
"schema:strict": "ajv validate -s ./data/schema.strict.yaml -d ./data/manifest.yaml",
|
"schema:strict": "ajv validate -s ./data/schema.strict.yaml -d ./data/manifest.yaml",
|
||||||
"stats": "ts-node ./src/importer.ts --stats"
|
"stats": "ts-node ./src/bin.ts --stats"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/js-yaml": "^3.12.4",
|
"@types/js-yaml": "^3.12.4",
|
||||||
|
|
82
src/bin.ts
Normal file
82
src/bin.ts
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import * as minimist from "minimist";
|
||||||
|
|
||||||
|
import { ManifestFile } from "./manifest";
|
||||||
|
import { SteamGameCacheFile, getSteamClient } from "./steam";
|
||||||
|
import { WikiGameCacheFile } from "./wiki";
|
||||||
|
import { saveMissingGames } from "./missing";
|
||||||
|
|
||||||
|
interface Cli {
|
||||||
|
cache?: boolean,
|
||||||
|
manifest?: boolean,
|
||||||
|
stats?: boolean,
|
||||||
|
all?: boolean,
|
||||||
|
existing?: boolean,
|
||||||
|
missing?: boolean,
|
||||||
|
unchecked?: boolean,
|
||||||
|
unsupportedOs?: boolean,
|
||||||
|
unsupportedPath?: boolean,
|
||||||
|
tooBroad?: boolean,
|
||||||
|
tooBroadUntagged?: boolean,
|
||||||
|
game?: string,
|
||||||
|
limit?: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const args = minimist<Cli>(process.argv.slice(2));
|
||||||
|
|
||||||
|
const wikiCache = new WikiGameCacheFile();
|
||||||
|
wikiCache.load();
|
||||||
|
const steamCache = new SteamGameCacheFile(await getSteamClient());
|
||||||
|
steamCache.load();
|
||||||
|
const manifest = new ManifestFile();
|
||||||
|
manifest.load();
|
||||||
|
|
||||||
|
if (args.stats) {
|
||||||
|
console.log(`Total games in manifest: ${Object.keys(manifest.data).length}`);
|
||||||
|
console.log(`Total games in manifest with files or registry: ${Object.values(manifest.data).filter(x => x.files !== undefined || x.registry !== undefined).length}`);
|
||||||
|
console.log(`Total games in manifest without files and registry: ${Object.values(manifest.data).filter(x => x.files === undefined && x.registry === undefined).length}`);
|
||||||
|
console.log(`Total games in wiki cache: ${Object.keys(wikiCache.data).length}`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (args.cache) {
|
||||||
|
await wikiCache.addNewGames(manifest.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.manifest) {
|
||||||
|
await manifest.updateGames(
|
||||||
|
wikiCache.data,
|
||||||
|
{
|
||||||
|
all: args.all ?? false,
|
||||||
|
existing: args.existing ?? false,
|
||||||
|
missing: args.missing ?? false,
|
||||||
|
unchecked: args.unchecked ?? false,
|
||||||
|
unsupportedOs: args.unsupportedOs ?? false,
|
||||||
|
unsupportedPath: args.unsupportedPath ?? false,
|
||||||
|
tooBroad: args.tooBroad ?? false,
|
||||||
|
tooBroadUntagged: args.tooBroadUntagged ?? false,
|
||||||
|
game: args.game,
|
||||||
|
},
|
||||||
|
args.limit ?? 25,
|
||||||
|
steamCache,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
wikiCache.save();
|
||||||
|
steamCache.save();
|
||||||
|
manifest.save();
|
||||||
|
saveMissingGames(wikiCache.data, manifest.data);
|
||||||
|
steamCache.steamClient.logOff();
|
||||||
|
process.exit(0);
|
||||||
|
} catch (e) {
|
||||||
|
wikiCache.save();
|
||||||
|
steamCache.save();
|
||||||
|
manifest.save();
|
||||||
|
saveMissingGames(wikiCache.data, manifest.data);
|
||||||
|
steamCache.steamClient.logOff();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
61
src/index.ts
Normal file
61
src/index.ts
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import * as pathMod from "path";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as yaml from "js-yaml";
|
||||||
|
|
||||||
|
export const REPO = pathMod.dirname(__dirname);
|
||||||
|
export const DELAY_BETWEEN_GAMES_MS = 250;
|
||||||
|
|
||||||
|
export class UnsupportedError extends Error {
|
||||||
|
constructor(message?: string) {
|
||||||
|
super(message);
|
||||||
|
Object.setPrototypeOf(this, new.target.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnsupportedOsError extends UnsupportedError {
|
||||||
|
constructor(message?: string) {
|
||||||
|
super(message);
|
||||||
|
Object.setPrototypeOf(this, new.target.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnsupportedPathError extends UnsupportedError {
|
||||||
|
constructor(message?: string) {
|
||||||
|
super(message);
|
||||||
|
Object.setPrototypeOf(this, new.target.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PathType {
|
||||||
|
FileSystem,
|
||||||
|
Registry,
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class YamlFile<T = object> {
|
||||||
|
data: T;
|
||||||
|
abstract path: string;
|
||||||
|
abstract defaultData: T;
|
||||||
|
|
||||||
|
load(): void {
|
||||||
|
if (fs.existsSync(this.path)) {
|
||||||
|
this.data = yaml.safeLoad(fs.readFileSync(this.path, "utf8"));
|
||||||
|
} else {
|
||||||
|
this.data = this.defaultData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
save(): void {
|
||||||
|
fs.writeFileSync(
|
||||||
|
this.path,
|
||||||
|
yaml.safeDump(
|
||||||
|
this.data,
|
||||||
|
{
|
||||||
|
sortKeys: true,
|
||||||
|
indent: 2,
|
||||||
|
skipInvalid: true,
|
||||||
|
lineWidth: 120,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
121
src/manifest.ts
Normal file
121
src/manifest.ts
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
import { DELAY_BETWEEN_GAMES_MS, REPO, YamlFile } from ".";
|
||||||
|
import { SteamGameCacheFile } from "./steam";
|
||||||
|
import { WikiGameCache, getGame, pathIsTooBroad } from "./wiki";
|
||||||
|
|
||||||
|
export type Os = "dos" | "linux" | "mac" | "windows";
|
||||||
|
|
||||||
|
export type Store = "epic" | "gog" | "microsoft" | "steam" | "uplay";
|
||||||
|
|
||||||
|
export type Tag = "config" | "save";
|
||||||
|
|
||||||
|
export interface Manifest {
|
||||||
|
[game: string]: Game;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Game {
|
||||||
|
files?: {
|
||||||
|
[path: string]: {
|
||||||
|
when?: Array<Constraint>,
|
||||||
|
tags?: Array<Tag>,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
installDir?: {
|
||||||
|
[name: string]: {}
|
||||||
|
};
|
||||||
|
registry?: {
|
||||||
|
[path: string]: {
|
||||||
|
when?: Array<Omit<Constraint, "os">>,
|
||||||
|
tags?: Array<Tag>,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
steam?: {
|
||||||
|
id?: number
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Constraint {
|
||||||
|
os?: Os;
|
||||||
|
store?: Store;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ManifestFile extends YamlFile<Manifest> {
|
||||||
|
path = `${REPO}/data/manifest.yaml`;
|
||||||
|
defaultData = {};
|
||||||
|
|
||||||
|
async updateGames(
|
||||||
|
wikiCache: WikiGameCache,
|
||||||
|
filter: {
|
||||||
|
all: boolean,
|
||||||
|
existing: boolean,
|
||||||
|
missing: boolean,
|
||||||
|
unchecked: boolean,
|
||||||
|
unsupportedOs: boolean,
|
||||||
|
unsupportedPath: boolean,
|
||||||
|
tooBroad: boolean,
|
||||||
|
tooBroadUntagged: boolean,
|
||||||
|
game: string | undefined,
|
||||||
|
},
|
||||||
|
limit: number,
|
||||||
|
steamCache: SteamGameCacheFile,
|
||||||
|
): Promise<void> {
|
||||||
|
let i = 0;
|
||||||
|
for (const [title, info] of Object.entries(wikiCache)) {
|
||||||
|
let check = false;
|
||||||
|
if (filter.all) {
|
||||||
|
check = true;
|
||||||
|
}
|
||||||
|
if (filter.existing && this.data.hasOwnProperty(title)) {
|
||||||
|
check = true;
|
||||||
|
}
|
||||||
|
if (filter.missing && !this.data.hasOwnProperty(title)) {
|
||||||
|
check = true;
|
||||||
|
}
|
||||||
|
if (filter.unchecked && wikiCache[title].revId === null) {
|
||||||
|
check = true;
|
||||||
|
}
|
||||||
|
if (filter.unsupportedOs && info.unsupportedOs) {
|
||||||
|
check = true;
|
||||||
|
}
|
||||||
|
if (filter.unsupportedPath && info.unsupportedPath) {
|
||||||
|
check = true;
|
||||||
|
}
|
||||||
|
if (filter.game === title) {
|
||||||
|
check = true;
|
||||||
|
}
|
||||||
|
if (filter.tooBroad && info.tooBroad) {
|
||||||
|
check = true;
|
||||||
|
}
|
||||||
|
if (filter.tooBroadUntagged && Object.keys(this.data[title]?.files ?? []).some(x => pathIsTooBroad(x))) {
|
||||||
|
check = true;
|
||||||
|
}
|
||||||
|
if (!check) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
if (i > limit) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const game = await getGame(title, wikiCache);
|
||||||
|
if (game.files === undefined && game.registry === undefined && game.steam?.id === undefined) {
|
||||||
|
delete this.data[title];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (game.steam?.id !== undefined) {
|
||||||
|
const installDir = await steamCache.getAppInstallDir(game.steam.id);
|
||||||
|
if (installDir !== undefined) {
|
||||||
|
if (game.installDir === undefined) {
|
||||||
|
game.installDir = {}
|
||||||
|
}
|
||||||
|
game.installDir[installDir] = {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.data[title] = game;
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, DELAY_BETWEEN_GAMES_MS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
15
src/missing.ts
Normal file
15
src/missing.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { REPO } from ".";
|
||||||
|
import { Manifest } from "./manifest";
|
||||||
|
import { WikiGameCache } from "./wiki";
|
||||||
|
|
||||||
|
export function saveMissingGames(cache: WikiGameCache, manifest: Manifest): void {
|
||||||
|
fs.writeFileSync(
|
||||||
|
`${REPO}/data/missing.md`,
|
||||||
|
Object.entries(cache)
|
||||||
|
.sort((x, y) => x[0].localeCompare(y[0]))
|
||||||
|
.filter(([k, _]) => (manifest[k]?.files ?? []).length === 0 && (manifest[k]?.registry ?? []).length === 0)
|
||||||
|
.map(([k, v]) => `* [${k}](https://www.pcgamingwiki.com/wiki/?curid=${v.pageId})`)
|
||||||
|
.join("\n") + "\n",
|
||||||
|
);
|
||||||
|
}
|
64
src/steam.ts
Normal file
64
src/steam.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import { REPO, YamlFile } from ".";
|
||||||
|
import * as SteamUser from "steam-user";
|
||||||
|
|
||||||
|
type SteamGameCache = {
|
||||||
|
[appId: string]: {
|
||||||
|
installDir?: string,
|
||||||
|
unknown?: boolean,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export class SteamGameCacheFile extends YamlFile<SteamGameCache> {
|
||||||
|
path = `${REPO}/data/steam-game-cache.yaml`;
|
||||||
|
defaultData = {};
|
||||||
|
|
||||||
|
constructor(public steamClient: SteamUser) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAppInstallDir(appId: number): Promise<string | undefined> {
|
||||||
|
const key = appId.toString();
|
||||||
|
if (this.data.hasOwnProperty(key)) {
|
||||||
|
return this.data[key].installDir;
|
||||||
|
} else {
|
||||||
|
const info: SteamProductInfoResponse = await this.steamClient.getProductInfo([appId], []);
|
||||||
|
|
||||||
|
if (info.unknownApps.includes(appId)) {
|
||||||
|
this.data[key] = { unknown: true };
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const installDir = info.apps[key].appinfo.config?.installdir;
|
||||||
|
if (installDir !== undefined) {
|
||||||
|
this.data[key] = { installDir };
|
||||||
|
} else {
|
||||||
|
this.data[key] = {};
|
||||||
|
}
|
||||||
|
return installDir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SteamProductInfoResponse {
|
||||||
|
apps: {
|
||||||
|
[appId: string]: {
|
||||||
|
appinfo: {
|
||||||
|
config?: {
|
||||||
|
installdir?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unknownApps: Array<number>,
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSteamClient(): Promise<SteamUser> {
|
||||||
|
const client = new SteamUser();
|
||||||
|
client.logOn();
|
||||||
|
await new Promise(resolve => {
|
||||||
|
client.on("loggedOn", () => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return client;
|
||||||
|
}
|
|
@ -1,64 +1,8 @@
|
||||||
|
import { REPO, PathType, UnsupportedOsError, UnsupportedPathError, YamlFile } from ".";
|
||||||
|
import { Manifest, Constraint, Game, Store, Tag, Os } from "./manifest";
|
||||||
import * as Wikiapi from "wikiapi";
|
import * as Wikiapi from "wikiapi";
|
||||||
import * as fs from "fs";
|
|
||||||
import * as pathMod from "path";
|
|
||||||
import * as minimist from "minimist";
|
|
||||||
import * as yaml from "js-yaml";
|
|
||||||
import * as SteamUser from "steam-user";
|
|
||||||
|
|
||||||
const REPO = pathMod.dirname(__dirname);
|
export type WikiGameCache = {
|
||||||
const DELAY_BETWEEN_GAMES_MS = 250;
|
|
||||||
|
|
||||||
interface Cli {
|
|
||||||
cache?: boolean,
|
|
||||||
manifest?: boolean,
|
|
||||||
stats?: boolean,
|
|
||||||
all?: boolean,
|
|
||||||
existing?: boolean,
|
|
||||||
missing?: boolean,
|
|
||||||
unchecked?: boolean,
|
|
||||||
unsupportedOs?: boolean,
|
|
||||||
unsupportedPath?: boolean,
|
|
||||||
tooBroad?: boolean,
|
|
||||||
tooBroadUntagged?: boolean,
|
|
||||||
game?: string,
|
|
||||||
limit?: number,
|
|
||||||
}
|
|
||||||
|
|
||||||
class UnsupportedError extends Error {
|
|
||||||
constructor(message?: string) {
|
|
||||||
super(message);
|
|
||||||
Object.setPrototypeOf(this, new.target.prototype);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UnsupportedOsError extends UnsupportedError {
|
|
||||||
constructor(message?: string) {
|
|
||||||
super(message);
|
|
||||||
Object.setPrototypeOf(this, new.target.prototype);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UnsupportedPathError extends UnsupportedError {
|
|
||||||
constructor(message?: string) {
|
|
||||||
super(message);
|
|
||||||
Object.setPrototypeOf(this, new.target.prototype);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum PathType {
|
|
||||||
FileSystem,
|
|
||||||
Registry,
|
|
||||||
}
|
|
||||||
|
|
||||||
type Os = "dos" | "linux" | "mac" | "windows";
|
|
||||||
|
|
||||||
type Store = "epic" | "gog" | "microsoft" | "steam" | "uplay";
|
|
||||||
|
|
||||||
type Tag = "config" | "save";
|
|
||||||
|
|
||||||
type GamePages = Array<{ pageid: number, title: string }>;
|
|
||||||
|
|
||||||
type WikiGameCache = {
|
|
||||||
[title: string]: {
|
[title: string]: {
|
||||||
pageId: number,
|
pageId: number,
|
||||||
revId: number | null,
|
revId: number | null,
|
||||||
|
@ -71,12 +15,23 @@ type WikiGameCache = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type SteamGameCache = {
|
export class WikiGameCacheFile extends YamlFile<WikiGameCache> {
|
||||||
[appId: string]: {
|
path = `${REPO}/data/wiki-game-cache.yaml`;
|
||||||
installDir?: string,
|
defaultData = {};
|
||||||
unknown?: boolean,
|
|
||||||
};
|
async addNewGames(manifest: Manifest): Promise<void> {
|
||||||
};
|
const wiki = makeApiClient();
|
||||||
|
const pages: Array<{ pageid: number, title: string }> = JSON.parse(JSON.stringify(await wiki.categorymembers("Games")));
|
||||||
|
for (const page of pages) {
|
||||||
|
if (!this.data.hasOwnProperty(page.title)) {
|
||||||
|
this.data[page.title] = {
|
||||||
|
pageId: page.pageid,
|
||||||
|
revId: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// This defines how {{P|game}} and such are converted.
|
// This defines how {{P|game}} and such are converted.
|
||||||
const PATH_ARGS: { [arg: string]: { mapped: string, when?: Constraint, registry?: boolean, ignored?: boolean } } = {
|
const PATH_ARGS: { [arg: string]: { mapped: string, when?: Constraint, registry?: boolean, ignored?: boolean } } = {
|
||||||
|
@ -172,36 +127,6 @@ const PATH_ARGS: { [arg: string]: { mapped: string, when?: Constraint, registry?
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Manifest {
|
|
||||||
[game: string]: Game;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Game {
|
|
||||||
files?: {
|
|
||||||
[path: string]: {
|
|
||||||
when?: Array<Constraint>,
|
|
||||||
tags?: Array<Tag>,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
installDir?: {
|
|
||||||
[name: string]: {}
|
|
||||||
};
|
|
||||||
registry?: {
|
|
||||||
[path: string]: {
|
|
||||||
when?: Array<Omit<Constraint, "os">>,
|
|
||||||
tags?: Array<Tag>,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
steam?: {
|
|
||||||
id?: number
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Constraint {
|
|
||||||
os?: Os;
|
|
||||||
store?: Store;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makePathArgRegex(arg: string): RegExp {
|
function makePathArgRegex(arg: string): RegExp {
|
||||||
const escaped = `{{P|${arg}}}`
|
const escaped = `{{P|${arg}}}`
|
||||||
.replace("\\", "\\\\")
|
.replace("\\", "\\\\")
|
||||||
|
@ -242,14 +167,13 @@ function parsePath(path: string): [string, PathType] {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function pathIsTooBroad(path: string): boolean {
|
export function pathIsTooBroad(path: string): boolean {
|
||||||
if (Object.values(PATH_ARGS).map(x => x.mapped).includes(path)) {
|
if (Object.values(PATH_ARGS).map(x => x.mapped).includes(path)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: These paths are present whether or not the game is installed.
|
// TODO: These paths are present whether or not the game is installed.
|
||||||
// To include them in the manifest, there should be some way to flag them
|
// If possible, they should be narrowed down on the wiki.
|
||||||
// as likely false positives.
|
|
||||||
if ([
|
if ([
|
||||||
"<home>/Documents",
|
"<home>/Documents",
|
||||||
"<home>/Saved Games",
|
"<home>/Saved Games",
|
||||||
|
@ -343,179 +267,10 @@ function makeApiClient() {
|
||||||
return new Wikiapi("https://www.pcgamingwiki.com/w");
|
return new Wikiapi("https://www.pcgamingwiki.com/w");
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveMissingGames(cache: WikiGameCache, manifest: Manifest): void {
|
|
||||||
fs.writeFileSync(
|
|
||||||
`${REPO}/data/missing.md`,
|
|
||||||
Object.entries(cache)
|
|
||||||
.sort((x, y) => x[0].localeCompare(y[0]))
|
|
||||||
.filter(([k, _]) => (manifest[k]?.files ?? []).length === 0 && (manifest[k]?.registry ?? []).length === 0)
|
|
||||||
.map(([k, v]) => `* [${k}](https://www.pcgamingwiki.com/wiki/?curid=${v.pageId})`)
|
|
||||||
.join("\n") + "\n",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class YamlFile<T = object> {
|
|
||||||
data: T;
|
|
||||||
abstract path: string;
|
|
||||||
abstract defaultData: T;
|
|
||||||
|
|
||||||
load(): void {
|
|
||||||
if (fs.existsSync(this.path)) {
|
|
||||||
this.data = yaml.safeLoad(fs.readFileSync(this.path, "utf8"));
|
|
||||||
} else {
|
|
||||||
this.data = this.defaultData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
save(): void {
|
|
||||||
fs.writeFileSync(
|
|
||||||
this.path,
|
|
||||||
yaml.safeDump(
|
|
||||||
this.data,
|
|
||||||
{
|
|
||||||
sortKeys: true,
|
|
||||||
indent: 2,
|
|
||||||
skipInvalid: true,
|
|
||||||
lineWidth: 120,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WikiGameCacheFile extends YamlFile<WikiGameCache> {
|
|
||||||
path = `${REPO}/data/wiki-game-cache.yaml`;
|
|
||||||
defaultData = {};
|
|
||||||
|
|
||||||
async addNewGames(manifest: Manifest): Promise<void> {
|
|
||||||
const wiki = makeApiClient();
|
|
||||||
const pages: Array<{ pageid: number, title: string }> = JSON.parse(JSON.stringify(await wiki.categorymembers("Games")));
|
|
||||||
for (const page of pages) {
|
|
||||||
if (!this.data.hasOwnProperty(page.title)) {
|
|
||||||
this.data[page.title] = {
|
|
||||||
pageId: page.pageid,
|
|
||||||
revId: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SteamGameCacheFile extends YamlFile<SteamGameCache> {
|
|
||||||
path = `${REPO}/data/steam-game-cache.yaml`;
|
|
||||||
defaultData = {};
|
|
||||||
|
|
||||||
constructor(public steamClient: SteamUser) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAppInstallDir(appId: number): Promise<string | undefined> {
|
|
||||||
const key = appId.toString();
|
|
||||||
if (this.data.hasOwnProperty(key)) {
|
|
||||||
return this.data[key].installDir;
|
|
||||||
} else {
|
|
||||||
const info: SteamProductInfoResponse = await this.steamClient.getProductInfo([appId], []);
|
|
||||||
|
|
||||||
if (info.unknownApps.includes(appId)) {
|
|
||||||
this.data[key] = { unknown: true };
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const installDir = info.apps[key].appinfo.config?.installdir;
|
|
||||||
if (installDir !== undefined) {
|
|
||||||
this.data[key] = { installDir };
|
|
||||||
} else {
|
|
||||||
this.data[key] = {};
|
|
||||||
}
|
|
||||||
return installDir;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ManifestFile extends YamlFile<Manifest> {
|
|
||||||
path = `${REPO}/data/manifest.yaml`;
|
|
||||||
defaultData = {};
|
|
||||||
|
|
||||||
async updateGames(
|
|
||||||
wikiCache: WikiGameCache,
|
|
||||||
filter: {
|
|
||||||
all: boolean,
|
|
||||||
existing: boolean,
|
|
||||||
missing: boolean,
|
|
||||||
unchecked: boolean,
|
|
||||||
unsupportedOs: boolean,
|
|
||||||
unsupportedPath: boolean,
|
|
||||||
tooBroad: boolean,
|
|
||||||
tooBroadUntagged: boolean,
|
|
||||||
game: string | undefined,
|
|
||||||
},
|
|
||||||
limit: number,
|
|
||||||
steamCache: SteamGameCacheFile,
|
|
||||||
): Promise<void> {
|
|
||||||
let i = 0;
|
|
||||||
for (const [title, info] of Object.entries(wikiCache)) {
|
|
||||||
let check = false;
|
|
||||||
if (filter.all) {
|
|
||||||
check = true;
|
|
||||||
}
|
|
||||||
if (filter.existing && this.data.hasOwnProperty(title)) {
|
|
||||||
check = true;
|
|
||||||
}
|
|
||||||
if (filter.missing && !this.data.hasOwnProperty(title)) {
|
|
||||||
check = true;
|
|
||||||
}
|
|
||||||
if (filter.unchecked && wikiCache[title].revId === null) {
|
|
||||||
check = true;
|
|
||||||
}
|
|
||||||
if (filter.unsupportedOs && info.unsupportedOs) {
|
|
||||||
check = true;
|
|
||||||
}
|
|
||||||
if (filter.unsupportedPath && info.unsupportedPath) {
|
|
||||||
check = true;
|
|
||||||
}
|
|
||||||
if (filter.game === title) {
|
|
||||||
check = true;
|
|
||||||
}
|
|
||||||
if (filter.tooBroad && info.tooBroad) {
|
|
||||||
check = true;
|
|
||||||
}
|
|
||||||
if (filter.tooBroadUntagged && Object.keys(this.data[title]?.files ?? []).some(x => pathIsTooBroad(x))) {
|
|
||||||
check = true;
|
|
||||||
}
|
|
||||||
if (!check) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
i++;
|
|
||||||
if (i > limit) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const game = await getGame(title, wikiCache);
|
|
||||||
if (game.files === undefined && game.registry === undefined && game.steam?.id === undefined) {
|
|
||||||
delete this.data[title];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (game.steam?.id !== undefined) {
|
|
||||||
const installDir = await steamCache.getAppInstallDir(game.steam.id);
|
|
||||||
if (installDir !== undefined) {
|
|
||||||
if (game.installDir === undefined) {
|
|
||||||
game.installDir = {}
|
|
||||||
}
|
|
||||||
game.installDir[installDir] = {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.data[title] = game;
|
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, DELAY_BETWEEN_GAMES_MS));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://www.pcgamingwiki.com/wiki/Template:Game_data
|
* https://www.pcgamingwiki.com/wiki/Template:Game_data
|
||||||
*/
|
*/
|
||||||
async function getGame(pageTitle: string, cache: WikiGameCache): Promise<Game> {
|
export async function getGame(pageTitle: string, cache: WikiGameCache): Promise<Game> {
|
||||||
console.log(pageTitle);
|
console.log(pageTitle);
|
||||||
const wiki = makeApiClient();
|
const wiki = makeApiClient();
|
||||||
const page = await wiki.page(pageTitle, { rvprop: "ids|content" });
|
const page = await wiki.page(pageTitle, { rvprop: "ids|content" });
|
||||||
|
@ -651,86 +406,3 @@ async function getGame(pageTitle: string, cache: WikiGameCache): Promise<Game> {
|
||||||
return game;
|
return game;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SteamProductInfoResponse {
|
|
||||||
apps: {
|
|
||||||
[appId: string]: {
|
|
||||||
appinfo: {
|
|
||||||
config?: {
|
|
||||||
installdir?: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
unknownApps: Array<number>,
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getSteamClient(): Promise<SteamUser> {
|
|
||||||
const client = new SteamUser();
|
|
||||||
client.logOn();
|
|
||||||
await new Promise(resolve => {
|
|
||||||
client.on("loggedOn", () => {
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const args = minimist<Cli>(process.argv.slice(2));
|
|
||||||
|
|
||||||
const wikiCache = new WikiGameCacheFile();
|
|
||||||
wikiCache.load();
|
|
||||||
const steamCache = new SteamGameCacheFile(await getSteamClient());
|
|
||||||
steamCache.load();
|
|
||||||
const manifest = new ManifestFile();
|
|
||||||
manifest.load();
|
|
||||||
|
|
||||||
if (args.stats) {
|
|
||||||
console.log(`Total games in manifest: ${Object.keys(manifest.data).length}`);
|
|
||||||
console.log(`Total games in manifest with files or registry: ${Object.values(manifest.data).filter(x => x.files !== undefined || x.registry !== undefined).length}`);
|
|
||||||
console.log(`Total games in manifest without files and registry: ${Object.values(manifest.data).filter(x => x.files === undefined && x.registry === undefined).length}`);
|
|
||||||
console.log(`Total games in wiki cache: ${Object.keys(wikiCache.data).length}`);
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (args.cache) {
|
|
||||||
await wikiCache.addNewGames(manifest.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.manifest) {
|
|
||||||
await manifest.updateGames(
|
|
||||||
wikiCache.data,
|
|
||||||
{
|
|
||||||
all: args.all ?? false,
|
|
||||||
existing: args.existing ?? false,
|
|
||||||
missing: args.missing ?? false,
|
|
||||||
unchecked: args.unchecked ?? false,
|
|
||||||
unsupportedOs: args.unsupportedOs ?? false,
|
|
||||||
unsupportedPath: args.unsupportedPath ?? false,
|
|
||||||
tooBroad: args.tooBroad ?? false,
|
|
||||||
tooBroadUntagged: args.tooBroadUntagged ?? false,
|
|
||||||
game: args.game,
|
|
||||||
},
|
|
||||||
args.limit ?? 25,
|
|
||||||
steamCache,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
wikiCache.save();
|
|
||||||
steamCache.save();
|
|
||||||
manifest.save();
|
|
||||||
saveMissingGames(wikiCache.data, manifest.data);
|
|
||||||
steamCache.steamClient.logOff();
|
|
||||||
process.exit(0);
|
|
||||||
} catch (e) {
|
|
||||||
wikiCache.save();
|
|
||||||
steamCache.save();
|
|
||||||
manifest.save();
|
|
||||||
saveMissingGames(wikiCache.data, manifest.data);
|
|
||||||
steamCache.steamClient.logOff();
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
Reference in a new issue