Files
hydra/src/main/services/download-sources-checker.ts

205 lines
6.0 KiB
TypeScript

import { HydraApi } from "./hydra-api";
import {
gamesSublevel,
getDownloadSourcesCheckBaseline,
updateDownloadSourcesCheckBaseline,
updateDownloadSourcesSinceValue,
downloadSourcesSublevel,
db,
levelKeys,
} from "@main/level";
import { logger } from "./logger";
import { WindowManager } from "./window-manager";
import type { Game, UserPreferences } from "@types";
interface DownloadSourcesChangeResponse {
shop: string;
objectId: string;
newDownloadOptionsCount: number;
downloadSourceIds: string[];
}
export class DownloadSourcesChecker {
private static async clearStaleBadges(
nonCustomGames: Game[]
): Promise<{ gameId: string; count: number }[]> {
const previouslyFlaggedGames = nonCustomGames.filter(
(game: Game) =>
game.newDownloadOptionsCount && game.newDownloadOptionsCount > 0
);
const clearedPayload: { gameId: string; count: number }[] = [];
if (previouslyFlaggedGames.length > 0) {
logger.info(
`Clearing stale newDownloadOptionsCount for ${previouslyFlaggedGames.length} games`
);
for (const game of previouslyFlaggedGames) {
await gamesSublevel.put(`${game.shop}:${game.objectId}`, {
...game,
newDownloadOptionsCount: undefined,
});
clearedPayload.push({
gameId: `${game.shop}:${game.objectId}`,
count: 0,
});
}
}
return clearedPayload;
}
private static async processApiResponse(
response: unknown,
nonCustomGames: Game[]
): Promise<{ gameId: string; count: number }[]> {
if (!response || !Array.isArray(response)) {
return [];
}
const gamesWithNewOptions: { gameId: string; count: number }[] = [];
for (const gameUpdate of response as DownloadSourcesChangeResponse[]) {
if (gameUpdate.newDownloadOptionsCount > 0) {
const game = nonCustomGames.find(
(g) =>
g.shop === gameUpdate.shop && g.objectId === gameUpdate.objectId
);
if (game) {
await gamesSublevel.put(`${game.shop}:${game.objectId}`, {
...game,
newDownloadOptionsCount: gameUpdate.newDownloadOptionsCount,
});
gamesWithNewOptions.push({
gameId: `${game.shop}:${game.objectId}`,
count: gameUpdate.newDownloadOptionsCount,
});
}
}
}
return gamesWithNewOptions;
}
private static sendNewDownloadOptionsEvent(
clearedPayload: { gameId: string; count: number }[],
gamesWithNewOptions: { gameId: string; count: number }[]
): void {
const eventPayload = [...clearedPayload, ...gamesWithNewOptions];
if (eventPayload.length > 0 && WindowManager.mainWindow) {
WindowManager.mainWindow.webContents.send(
"on-new-download-options",
eventPayload
);
}
logger.info(
`Found new download options for ${gamesWithNewOptions.length} games`
);
}
static async checkForChanges(): Promise<void> {
logger.info("DownloadSourcesChecker.checkForChanges() called");
try {
const userPreferences = await db.get<string, UserPreferences | null>(
levelKeys.userPreferences,
{
valueEncoding: "json",
}
);
if (userPreferences?.enableNewDownloadOptionsBadges === false) {
logger.info(
"New download options badges are disabled, skipping download sources check"
);
return;
}
// Get all installed games (excluding custom games)
const installedGames = await gamesSublevel.values().all();
const nonCustomGames = installedGames.filter(
(game: Game) => game.shop !== "custom"
);
logger.info(
`Found ${installedGames.length} total games, ${nonCustomGames.length} non-custom games`
);
if (nonCustomGames.length === 0) {
logger.info(
"No non-custom games found, skipping download sources check"
);
return;
}
const downloadSources = await downloadSourcesSublevel.values().all();
const downloadSourceIds = downloadSources.map((source) => source.id);
logger.info(
`Found ${downloadSourceIds.length} download sources: ${downloadSourceIds.join(", ")}`
);
if (downloadSourceIds.length === 0) {
logger.info(
"No download sources found, skipping download sources check"
);
return;
}
const previousBaseline = await getDownloadSourcesCheckBaseline();
const since =
previousBaseline ||
new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
logger.info(`Using since: ${since} (from last app start)`);
const clearedPayload = await this.clearStaleBadges(nonCustomGames);
const games = nonCustomGames.map((game: Game) => ({
shop: game.shop,
objectId: game.objectId,
}));
logger.info(
`Checking download sources changes for ${games.length} non-custom games since ${since}`
);
logger.info(
`Making API call to HydraApi.checkDownloadSourcesChanges with:`,
{
downloadSourceIds,
gamesCount: games.length,
since,
}
);
const response = await HydraApi.checkDownloadSourcesChanges(
downloadSourceIds,
games,
since
);
logger.info("API call completed, response:", response);
await updateDownloadSourcesSinceValue(since);
logger.info(`Saved 'since' value: ${since} (for modal comparison)`);
const now = new Date().toISOString();
await updateDownloadSourcesCheckBaseline(now);
logger.info(
`Updated baseline to: ${now} (will be 'since' on next app start)`
);
const gamesWithNewOptions = await this.processApiResponse(
response,
nonCustomGames
);
this.sendNewDownloadOptionsEvent(clearedPayload, gamesWithNewOptions);
logger.info("Download sources check completed successfully");
} catch (error) {
logger.error("Failed to check download sources changes:", error);
}
}
}