feat: migrating achievements to level

This commit is contained in:
Chubby Granny Chaser
2025-01-16 02:30:09 +00:00
parent 2c881a6100
commit a23106b0b1
34 changed files with 388 additions and 475 deletions

View File

@@ -1,23 +1,11 @@
import { DataSource } from "typeorm";
import {
DownloadQueue,
Game,
GameShopCache,
UserPreferences,
GameAchievement,
} from "@main/entity";
import { DownloadQueue, Game, UserPreferences } from "@main/entity";
import { databasePath } from "./constants";
export const dataSource = new DataSource({
type: "better-sqlite3",
entities: [
Game,
UserPreferences,
GameShopCache,
DownloadQueue,
GameAchievement,
],
entities: [Game, UserPreferences, DownloadQueue],
synchronize: false,
database: databasePath,
});

View File

@@ -1,19 +0,0 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity("game_achievement")
export class GameAchievement {
@PrimaryGeneratedColumn()
id: number;
@Column("text")
objectId: string;
@Column("text")
shop: string;
@Column("text", { nullable: true })
unlockedAchievements: string | null;
@Column("text", { nullable: true })
achievements: string | null;
}

View File

@@ -1,35 +0,0 @@
import {
Entity,
PrimaryColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from "typeorm";
import type { GameShop } from "@types";
@Entity("game_shop_cache")
export class GameShopCache {
@PrimaryColumn("text", { unique: true })
objectID: string;
@Column("text")
shop: GameShop;
@Column("text", { nullable: true })
serializedData: string;
/**
* @deprecated Use IndexedDB's `howLongToBeatEntries` instead
*/
@Column("text", { nullable: true })
howLongToBeatSerializedData: string;
@Column("text", { nullable: true })
language: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}

View File

@@ -1,5 +1,3 @@
export * from "./game.entity";
export * from "./user-preferences.entity";
export * from "./game-shop-cache.entity";
export * from "./game-achievements.entity";
export * from "./download-queue.entity";

View File

@@ -3,7 +3,7 @@ import jwt from "jsonwebtoken";
import { registerEvent } from "../register-event";
import { db } from "@main/level";
import type { Auth } from "@types";
import { levelKeys } from "@main/level/sublevels/keys";
import { levelKeys } from "@main/level";
import { Crypto } from "@main/services";
const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => {

View File

@@ -4,7 +4,7 @@ import { dataSource } from "@main/data-source";
import { DownloadQueue, Game } from "@main/entity";
import { PythonRPC } from "@main/services/python-rpc";
import { db } from "@main/level";
import { levelKeys } from "@main/level/sublevels/keys";
import { levelKeys } from "@main/level";
const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
const databaseOperations = dataSource

View File

@@ -1,10 +1,10 @@
import { gameShopCacheRepository } from "@main/repository";
import { getSteamAppDetails } from "@main/services";
import { getSteamAppDetails, logger } from "@main/services";
import type { ShopDetails, GameShop, SteamAppDetails } from "@types";
import type { ShopDetails, GameShop } from "@types";
import { registerEvent } from "../register-event";
import { steamGamesWorker } from "@main/workers";
import { gamesShopCacheSublevel, levelKeys } from "@main/level";
const getLocalizedSteamAppDetails = async (
objectId: string,
@@ -39,35 +39,27 @@ const getGameShopDetails = async (
language: string
): Promise<ShopDetails | null> => {
if (shop === "steam") {
const cachedData = await gameShopCacheRepository.findOne({
where: { objectID: objectId, language },
});
const cachedData = await gamesShopCacheSublevel.get(
levelKeys.gameShopCacheItem(shop, objectId, language)
);
const appDetails = getLocalizedSteamAppDetails(objectId, language).then(
(result) => {
if (result) {
gameShopCacheRepository.upsert(
{
objectID: objectId,
shop: "steam",
language,
serializedData: JSON.stringify(result),
},
["objectID"]
);
gamesShopCacheSublevel
.put(levelKeys.gameShopCacheItem(shop, objectId, language), result)
.catch((err) => {
logger.error("Could not cache game details", err);
});
}
return result;
}
);
const cachedGame = cachedData?.serializedData
? (JSON.parse(cachedData?.serializedData) as SteamAppDetails)
: null;
if (cachedGame) {
if (cachedData) {
return {
...cachedGame,
...cachedData,
objectId,
} as ShopDetails;
}

View File

@@ -1,9 +1,10 @@
import { gameAchievementRepository, gameRepository } from "@main/repository";
import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event";
import { findAchievementFiles } from "@main/services/achievements/find-achivement-files";
import fs from "fs";
import { achievementsLogger, HydraApi, WindowManager } from "@main/services";
import { getUnlockedAchievements } from "../user/get-unlocked-achievements";
import { gameAchievementsSublevel, levelKeys } from "@main/level";
const resetGameAchievements = async (
_event: Electron.IpcMainInvokeEvent,
@@ -23,12 +24,21 @@ const resetGameAchievements = async (
}
}
await gameAchievementRepository.update(
{ objectId: game.objectID },
{
unlockedAchievements: null,
}
);
const levelKey = levelKeys.game(game.shop, game.objectID);
await gameAchievementsSublevel
.get(levelKey)
.then(async (gameAchievements) => {
if (gameAchievements) {
await gameAchievementsSublevel.put(
levelKeys.game(game.shop, game.objectID),
{
...gameAchievements,
unlockedAchievements: [],
}
);
}
});
await HydraApi.delete(`/profile/games/achievements/${game.remoteId}`).then(
() =>

View File

@@ -3,7 +3,7 @@ import { registerEvent } from "../register-event";
import { Crypto, HydraApi } from "@main/services";
import { db } from "@main/level";
import type { Auth } from "@types";
import { levelKeys } from "@main/level/sublevels/keys";
import { levelKeys } from "@main/level";
const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => {
const auth = await db.get<string, Auth>(levelKeys.auth, {

View File

@@ -1,19 +1,17 @@
import type { GameShop, UnlockedAchievement, UserAchievement } from "@types";
import type { GameShop, UserAchievement } from "@types";
import { registerEvent } from "../register-event";
import {
gameAchievementRepository,
userPreferencesRepository,
} from "@main/repository";
import { userPreferencesRepository } from "@main/repository";
import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data";
import { gameAchievementsSublevel, levelKeys } from "@main/level";
export const getUnlockedAchievements = async (
objectId: string,
shop: GameShop,
useCachedData: boolean
): Promise<UserAchievement[]> => {
const cachedAchievements = await gameAchievementRepository.findOne({
where: { objectId, shop },
});
const cachedAchievements = await gameAchievementsSublevel.get(
levelKeys.game(shop, objectId)
);
const userPreferences = await userPreferencesRepository.findOne({
where: { id: 1 },
@@ -25,12 +23,10 @@ export const getUnlockedAchievements = async (
const achievementsData = await getGameAchievementData(
objectId,
shop,
useCachedData ? cachedAchievements : null
useCachedData
);
const unlockedAchievements = JSON.parse(
cachedAchievements?.unlockedAchievements || "[]"
) as UnlockedAchievement[];
const unlockedAchievements = cachedAchievements?.unlockedAchievements ?? [];
return achievementsData
.map((achievementData) => {

View File

@@ -2,7 +2,7 @@ import { db } from "@main/level";
import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services";
import type { User, UserFriends } from "@types";
import { levelKeys } from "@main/level/sublevels/keys";
import { levelKeys } from "@main/level/sublevels";
export const getUserFriends = async (
userId: string,

View File

@@ -0,0 +1,11 @@
import type { GameAchievement } from "@types";
import { db } from "../level";
import { levelKeys } from "./keys";
export const gameAchievementsSublevel = db.sublevel<string, GameAchievement>(
levelKeys.gameAchievements,
{
valueEncoding: "json",
}
);

View File

@@ -0,0 +1,11 @@
import type { ShopDetails } from "@types";
import { db } from "../level";
import { levelKeys } from "./keys";
export const gamesShopCacheSublevel = db.sublevel<string, ShopDetails>(
levelKeys.gameShopCache,
{
valueEncoding: "json",
}
);

View File

@@ -1,4 +1,5 @@
import { Game } from "@types";
import type { Game } from "@types";
import { db } from "../level";
import { levelKeys } from "./keys";

View File

@@ -1 +1,5 @@
export * from "./games";
export * from "./game-shop-cache";
export * from "./game-achievements";
export * from "./keys";

View File

@@ -5,4 +5,8 @@ export const levelKeys = {
game: (shop: GameShop, objectId: string) => `${shop}:${objectId}`,
user: "user",
auth: "auth",
gameShopCache: "gameShopCache",
gameShopCacheItem: (shop: GameShop, objectId: string, language: string) =>
`${shop}:${objectId}:${language}`,
gameAchievements: "gameAchievements",
};

View File

@@ -1,20 +1,9 @@
import { dataSource } from "./data-source";
import {
DownloadQueue,
Game,
GameShopCache,
UserPreferences,
GameAchievement,
} from "@main/entity";
import { DownloadQueue, Game, UserPreferences } from "@main/entity";
export const gameRepository = dataSource.getRepository(Game);
export const userPreferencesRepository =
dataSource.getRepository(UserPreferences);
export const gameShopCacheRepository = dataSource.getRepository(GameShopCache);
export const downloadQueueRepository = dataSource.getRepository(DownloadQueue);
export const gameAchievementRepository =
dataSource.getRepository(GameAchievement);

View File

@@ -1,40 +1,36 @@
import {
gameAchievementRepository,
userPreferencesRepository,
} from "@main/repository";
import { userPreferencesRepository } from "@main/repository";
import { HydraApi } from "../hydra-api";
import type { AchievementData, GameShop } from "@types";
import type { GameShop, SteamAchievement } from "@types";
import { UserNotLoggedInError } from "@shared";
import { logger } from "../logger";
import { GameAchievement } from "@main/entity";
import { gameAchievementsSublevel, levelKeys } from "@main/level";
export const getGameAchievementData = async (
objectId: string,
shop: GameShop,
cachedAchievements: GameAchievement | null
useCachedData: boolean
) => {
if (cachedAchievements && cachedAchievements.achievements) {
return JSON.parse(cachedAchievements.achievements) as AchievementData[];
}
const cachedAchievements = await gameAchievementsSublevel.get(
levelKeys.game(shop, objectId)
);
if (cachedAchievements && useCachedData)
return cachedAchievements.achievements;
const userPreferences = await userPreferencesRepository.findOne({
where: { id: 1 },
});
return HydraApi.get<AchievementData[]>("/games/achievements", {
return HydraApi.get<SteamAchievement[]>("/games/achievements", {
shop,
objectId,
language: userPreferences?.language || "en",
})
.then((achievements) => {
gameAchievementRepository.upsert(
{
objectId,
shop,
achievements: JSON.stringify(achievements),
},
["objectId", "shop"]
);
.then(async (achievements) => {
await gameAchievementsSublevel.put(levelKeys.game(shop, objectId), {
unlockedAchievements: cachedAchievements?.unlockedAchievements ?? [],
achievements,
});
return achievements;
})
@@ -42,15 +38,9 @@ export const getGameAchievementData = async (
if (err instanceof UserNotLoggedInError) {
throw err;
}
logger.error("Failed to get game achievements", err);
return gameAchievementRepository
.findOne({
where: { objectId, shop },
})
.then((gameAchievements) => {
return JSON.parse(
gameAchievements?.achievements || "[]"
) as AchievementData[];
});
return [];
});
};

View File

@@ -1,8 +1,5 @@
import {
gameAchievementRepository,
userPreferencesRepository,
} from "@main/repository";
import type { AchievementData, GameShop, UnlockedAchievement } from "@types";
import { userPreferencesRepository } from "@main/repository";
import type { GameShop, UnlockedAchievement } from "@types";
import { WindowManager } from "../window-manager";
import { HydraApi } from "../hydra-api";
import { getUnlockedAchievements } from "@main/events/user/get-unlocked-achievements";
@@ -10,33 +7,36 @@ import { Game } from "@main/entity";
import { publishNewAchievementNotification } from "../notifications";
import { SubscriptionRequiredError } from "@shared";
import { achievementsLogger } from "../logger";
import { gameAchievementsSublevel, levelKeys } from "@main/level";
const saveAchievementsOnLocal = async (
objectId: string,
shop: GameShop,
achievements: UnlockedAchievement[],
unlockedAchievements: UnlockedAchievement[],
sendUpdateEvent: boolean
) => {
return gameAchievementRepository
.upsert(
{
objectId,
shop,
unlockedAchievements: JSON.stringify(achievements),
},
["objectId", "shop"]
)
.then(() => {
if (!sendUpdateEvent) return;
const levelKey = levelKeys.game(shop, objectId);
return getUnlockedAchievements(objectId, shop, true)
.then((achievements) => {
WindowManager.mainWindow?.webContents.send(
`on-update-achievements-${objectId}-${shop}`,
achievements
);
})
.catch(() => {});
return gameAchievementsSublevel
.get(levelKey)
.then(async (gameAchievement) => {
if (gameAchievement) {
await gameAchievementsSublevel.put(levelKey, {
...gameAchievement,
unlockedAchievements: unlockedAchievements,
});
if (!sendUpdateEvent) return;
return getUnlockedAchievements(objectId, shop, true)
.then((achievements) => {
WindowManager.mainWindow?.webContents.send(
`on-update-achievements-${objectId}-${shop}`,
achievements
);
})
.catch(() => {});
}
});
};
@@ -46,22 +46,12 @@ export const mergeAchievements = async (
publishNotification: boolean
) => {
const [localGameAchievement, userPreferences] = await Promise.all([
gameAchievementRepository.findOne({
where: {
objectId: game.objectID,
shop: game.shop,
},
}),
gameAchievementsSublevel.get(levelKeys.game(game.shop, game.objectID)),
userPreferencesRepository.findOne({ where: { id: 1 } }),
]);
const achievementsData = JSON.parse(
localGameAchievement?.achievements || "[]"
) as AchievementData[];
const unlockedAchievements = JSON.parse(
localGameAchievement?.unlockedAchievements || "[]"
).filter((achievement) => achievement.name) as UnlockedAchievement[];
const achievementsData = localGameAchievement?.achievements ?? [];
const unlockedAchievements = localGameAchievement?.unlockedAchievements ?? [];
const newAchievementsMap = new Map(
achievements.reverse().map((achievement) => {

View File

@@ -10,7 +10,7 @@ import { appVersion } from "@main/constants";
import { getUserData } from "./user/get-user-data";
import { isFuture, isToday } from "date-fns";
import { db } from "@main/level";
import { levelKeys } from "@main/level/sublevels/keys";
import { levelKeys } from "@main/level/sublevels";
import type { Auth, User } from "@types";
import { Crypto } from "./crypto";

View File

@@ -3,7 +3,7 @@ import { HydraApi } from "../hydra-api";
import { UserNotLoggedInError } from "@shared";
import { logger } from "../logger";
import { db } from "@main/level";
import { levelKeys } from "@main/level/sublevels/keys";
import { levelKeys } from "@main/level/sublevels";
export const getUserData = async () => {
return HydraApi.get<UserDetails>(`/profile/me`)