mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-26 20:31:03 +00:00
feat: adding change hero
This commit is contained in:
@@ -30,7 +30,7 @@ const getCatalogue = async (
|
||||
title: steamGame.name,
|
||||
shop: game.shop,
|
||||
cover: steamUrlBuilder.library(game.objectId),
|
||||
objectID: game.objectId,
|
||||
objectId: game.objectId,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
@@ -7,16 +7,16 @@ import { registerEvent } from "../register-event";
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
|
||||
const getLocalizedSteamAppDetails = async (
|
||||
objectID: string,
|
||||
objectId: string,
|
||||
language: string
|
||||
): Promise<ShopDetails | null> => {
|
||||
if (language === "english") {
|
||||
return getSteamAppDetails(objectID, language);
|
||||
return getSteamAppDetails(objectId, language);
|
||||
}
|
||||
|
||||
return getSteamAppDetails(objectID, language).then(
|
||||
return getSteamAppDetails(objectId, language).then(
|
||||
async (localizedAppDetails) => {
|
||||
const steamGame = await steamGamesWorker.run(Number(objectID), {
|
||||
const steamGame = await steamGamesWorker.run(Number(objectId), {
|
||||
name: "getById",
|
||||
});
|
||||
|
||||
@@ -34,21 +34,21 @@ const getLocalizedSteamAppDetails = async (
|
||||
|
||||
const getGameShopDetails = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
objectID: string,
|
||||
objectId: string,
|
||||
shop: GameShop,
|
||||
language: string
|
||||
): Promise<ShopDetails | null> => {
|
||||
if (shop === "steam") {
|
||||
const cachedData = await gameShopCacheRepository.findOne({
|
||||
where: { objectID, language },
|
||||
where: { objectID: objectId, language },
|
||||
});
|
||||
|
||||
const appDetails = getLocalizedSteamAppDetails(objectID, language).then(
|
||||
const appDetails = getLocalizedSteamAppDetails(objectId, language).then(
|
||||
(result) => {
|
||||
if (result) {
|
||||
gameShopCacheRepository.upsert(
|
||||
{
|
||||
objectID,
|
||||
objectID: objectId,
|
||||
shop: "steam",
|
||||
language,
|
||||
serializedData: JSON.stringify(result),
|
||||
@@ -68,7 +68,7 @@ const getGameShopDetails = async (
|
||||
if (cachedGame) {
|
||||
return {
|
||||
...cachedGame,
|
||||
objectID,
|
||||
objectId,
|
||||
} as ShopDetails;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
import type { CatalogueEntry } from "@types";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
import { HydraApi } from "@main/services";
|
||||
import { steamUrlBuilder } from "@shared";
|
||||
|
||||
const getGames = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
take = 12,
|
||||
cursor = 0
|
||||
): Promise<{ results: CatalogueEntry[]; cursor: number }> => {
|
||||
const steamGames = await steamGamesWorker.run(
|
||||
{ limit: take, offset: cursor },
|
||||
{ name: "list" }
|
||||
skip = 0
|
||||
): Promise<CatalogueEntry[]> => {
|
||||
const searchParams = new URLSearchParams({
|
||||
take: take.toString(),
|
||||
skip: skip.toString(),
|
||||
});
|
||||
|
||||
const games = await HydraApi.get<CatalogueEntry[]>(
|
||||
`/games/catalogue?${searchParams.toString()}`,
|
||||
undefined,
|
||||
{ needsAuth: false }
|
||||
);
|
||||
|
||||
return {
|
||||
results: steamGames.map((steamGame) => ({
|
||||
title: steamGame.name,
|
||||
shop: "steam",
|
||||
cover: steamUrlBuilder.library(steamGame.id),
|
||||
objectID: steamGame.id,
|
||||
})),
|
||||
cursor: cursor + steamGames.length,
|
||||
};
|
||||
return games.map((game) => ({
|
||||
...game,
|
||||
cover: steamUrlBuilder.library(game.objectId),
|
||||
}));
|
||||
};
|
||||
|
||||
registerEvent("getGames", getGames);
|
||||
|
||||
@@ -6,14 +6,14 @@ import { gameShopCacheRepository } from "@main/repository";
|
||||
|
||||
const getHowLongToBeat = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
objectID: string,
|
||||
objectId: string,
|
||||
shop: GameShop,
|
||||
title: string
|
||||
): Promise<HowLongToBeatCategory[] | null> => {
|
||||
const searchHowLongToBeatPromise = searchHowLongToBeat(title);
|
||||
|
||||
const gameShopCache = await gameShopCacheRepository.findOne({
|
||||
where: { objectID, shop },
|
||||
where: { objectID: objectId, shop },
|
||||
});
|
||||
|
||||
const howLongToBeatCachedData = gameShopCache?.howLongToBeatSerializedData
|
||||
@@ -23,7 +23,7 @@ const getHowLongToBeat = async (
|
||||
|
||||
return searchHowLongToBeatPromise.then(async (response) => {
|
||||
const game = response.data.find(
|
||||
(game) => game.profile_steam === Number(objectID)
|
||||
(game) => game.profile_steam === Number(objectId)
|
||||
);
|
||||
|
||||
if (!game) return null;
|
||||
@@ -31,7 +31,7 @@ const getHowLongToBeat = async (
|
||||
|
||||
gameShopCacheRepository.upsert(
|
||||
{
|
||||
objectID,
|
||||
objectID: objectId,
|
||||
shop,
|
||||
howLongToBeatSerializedData: JSON.stringify(howLongToBeat),
|
||||
},
|
||||
|
||||
@@ -4,6 +4,9 @@ import { registerEvent } from "../register-event";
|
||||
const deleteGameArtifact = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
gameArtifactId: string
|
||||
) => HydraApi.delete<{ ok: boolean }>(`/games/artifacts/${gameArtifactId}`);
|
||||
) =>
|
||||
HydraApi.delete<{ ok: boolean }>(
|
||||
`/profile/games/artifacts/${gameArtifactId}`
|
||||
);
|
||||
|
||||
registerEvent("deleteGameArtifact", deleteGameArtifact);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { HydraApi, logger, Ludusavi, WindowManager } from "@main/services";
|
||||
import fs from "node:fs";
|
||||
import AdmZip from "adm-zip";
|
||||
import * as tar from "tar";
|
||||
import { registerEvent } from "../register-event";
|
||||
import axios from "axios";
|
||||
import { app } from "electron";
|
||||
@@ -8,16 +8,54 @@ import path from "node:path";
|
||||
import { backupsPath } from "@main/constants";
|
||||
import type { GameShop } from "@types";
|
||||
|
||||
import YAML from "yaml";
|
||||
|
||||
export interface LudusaviBackup {
|
||||
files: {
|
||||
[key: string]: {
|
||||
hash: string;
|
||||
size: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const replaceLudusaviBackupWithCurrentUser = (
|
||||
mappingPath: string,
|
||||
backupHomeDir: string
|
||||
) => {
|
||||
const data = fs.readFileSync(mappingPath, "utf8");
|
||||
const manifest = YAML.parse(data);
|
||||
|
||||
const currentHomeDir = app.getPath("home");
|
||||
|
||||
const backups = manifest.backups.map((backup: LudusaviBackup) => {
|
||||
const files = Object.entries(backup.files).reduce((prev, [key, value]) => {
|
||||
return {
|
||||
...prev,
|
||||
[key.replace(backupHomeDir, currentHomeDir)]: value,
|
||||
};
|
||||
}, {});
|
||||
|
||||
return {
|
||||
...backup,
|
||||
files,
|
||||
};
|
||||
});
|
||||
|
||||
fs.writeFileSync(mappingPath, YAML.stringify({ ...manifest, backups }));
|
||||
};
|
||||
|
||||
const downloadGameArtifact = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
objectId: string,
|
||||
shop: GameShop,
|
||||
gameArtifactId: string
|
||||
) => {
|
||||
const { downloadUrl, objectKey } = await HydraApi.post<{
|
||||
const { downloadUrl, objectKey, homeDir } = await HydraApi.post<{
|
||||
downloadUrl: string;
|
||||
objectKey: string;
|
||||
}>(`/games/artifacts/${gameArtifactId}/download`);
|
||||
homeDir: string;
|
||||
}>(`/profile/games/artifacts/${gameArtifactId}/download`);
|
||||
|
||||
const zipLocation = path.join(app.getPath("userData"), objectKey);
|
||||
const backupPath = path.join(backupsPath, `${shop}-${objectId}`);
|
||||
@@ -42,20 +80,31 @@ const downloadGameArtifact = async (
|
||||
});
|
||||
|
||||
writer.on("close", () => {
|
||||
const zip = new AdmZip(zipLocation);
|
||||
zip.extractAllToAsync(backupPath, true, true, (err) => {
|
||||
if (err) {
|
||||
logger.error("Failed to extract zip", err);
|
||||
throw err;
|
||||
}
|
||||
tar
|
||||
.x({
|
||||
file: zipLocation,
|
||||
cwd: backupPath,
|
||||
})
|
||||
.then(async () => {
|
||||
const [game] = await Ludusavi.findGames(shop, objectId);
|
||||
if (!game) throw new Error("Game not found in Ludusavi manifest");
|
||||
|
||||
Ludusavi.restoreBackup(backupPath).then(() => {
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
`on-backup-download-complete-${objectId}-${shop}`,
|
||||
true
|
||||
const mappingPath = path.join(
|
||||
backupsPath,
|
||||
`${shop}-${objectId}`,
|
||||
game,
|
||||
"mapping.yaml"
|
||||
);
|
||||
|
||||
replaceLudusaviBackupWithCurrentUser(mappingPath, homeDir);
|
||||
|
||||
Ludusavi.restoreBackup(backupPath).then(() => {
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
`on-backup-download-complete-${objectId}-${shop}`,
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ const getGameArtifacts = async (
|
||||
shop,
|
||||
});
|
||||
|
||||
return HydraApi.get<GameArtifact[]>(`/games/artifacts?${params.toString()}`);
|
||||
return HydraApi.get<GameArtifact[]>(
|
||||
`/profile/games/artifacts?${params.toString()}`
|
||||
);
|
||||
};
|
||||
|
||||
registerEvent("getGameArtifacts", getGameArtifacts);
|
||||
|
||||
@@ -2,47 +2,31 @@ import { HydraApi, logger, Ludusavi, WindowManager } from "@main/services";
|
||||
import { registerEvent } from "../register-event";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import archiver from "archiver";
|
||||
import * as tar from "tar";
|
||||
import crypto from "node:crypto";
|
||||
import { GameShop } from "@types";
|
||||
import axios from "axios";
|
||||
import os from "node:os";
|
||||
import { app } from "electron";
|
||||
import { backupsPath } from "@main/constants";
|
||||
import { app } from "electron";
|
||||
|
||||
const compressBackupToArtifact = async (
|
||||
shop: GameShop,
|
||||
objectId: string,
|
||||
cb: (zipLocation: string) => void
|
||||
) => {
|
||||
const bundleBackup = async (shop: GameShop, objectId: string) => {
|
||||
const backupPath = path.join(backupsPath, `${shop}-${objectId}`);
|
||||
|
||||
await Ludusavi.backupGame(shop, objectId, backupPath);
|
||||
|
||||
const archive = archiver("zip", {
|
||||
zlib: { level: 9 },
|
||||
});
|
||||
const tarLocation = path.join(backupsPath, `${crypto.randomUUID()}.zip`);
|
||||
|
||||
const zipLocation = path.join(
|
||||
app.getPath("userData"),
|
||||
`${crypto.randomUUID()}.zip`
|
||||
await tar.create(
|
||||
{
|
||||
gzip: false,
|
||||
file: tarLocation,
|
||||
cwd: backupPath,
|
||||
},
|
||||
["."]
|
||||
);
|
||||
|
||||
const output = fs.createWriteStream(zipLocation);
|
||||
|
||||
output.on("close", () => {
|
||||
cb(zipLocation);
|
||||
});
|
||||
|
||||
output.on("error", (err) => {
|
||||
logger.error("Failed to compress folder", err);
|
||||
throw err;
|
||||
});
|
||||
|
||||
archive.pipe(output);
|
||||
|
||||
archive.directory(backupPath, false);
|
||||
archive.finalize();
|
||||
return tarLocation;
|
||||
};
|
||||
|
||||
const uploadSaveGame = async (
|
||||
@@ -50,49 +34,51 @@ const uploadSaveGame = async (
|
||||
objectId: string,
|
||||
shop: GameShop
|
||||
) => {
|
||||
compressBackupToArtifact(shop, objectId, (zipLocation) => {
|
||||
fs.stat(zipLocation, async (err, stat) => {
|
||||
const bundleLocation = await bundleBackup(shop, objectId);
|
||||
|
||||
fs.stat(bundleLocation, async (err, stat) => {
|
||||
if (err) {
|
||||
logger.error("Failed to get zip file stats", err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
const { uploadUrl } = await HydraApi.post<{
|
||||
id: string;
|
||||
uploadUrl: string;
|
||||
}>("/profile/games/artifacts", {
|
||||
artifactLengthInBytes: stat.size,
|
||||
shop,
|
||||
objectId,
|
||||
hostname: os.hostname(),
|
||||
homeDir: app.getPath("home"),
|
||||
platform: os.platform(),
|
||||
});
|
||||
|
||||
fs.readFile(bundleLocation, async (err, fileBuffer) => {
|
||||
if (err) {
|
||||
logger.error("Failed to get zip file stats", err);
|
||||
logger.error("Failed to read zip file", err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
const { uploadUrl } = await HydraApi.post<{
|
||||
id: string;
|
||||
uploadUrl: string;
|
||||
}>("/games/artifacts", {
|
||||
artifactLengthInBytes: stat.size,
|
||||
shop,
|
||||
objectId,
|
||||
hostname: os.hostname(),
|
||||
await axios.put(uploadUrl, fileBuffer, {
|
||||
headers: {
|
||||
"Content-Type": "application/tar",
|
||||
},
|
||||
onUploadProgress: (progressEvent) => {
|
||||
console.log(progressEvent);
|
||||
},
|
||||
});
|
||||
|
||||
fs.readFile(zipLocation, async (err, fileBuffer) => {
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
`on-upload-complete-${objectId}-${shop}`,
|
||||
true
|
||||
);
|
||||
|
||||
fs.rm(bundleLocation, (err) => {
|
||||
if (err) {
|
||||
logger.error("Failed to read zip file", err);
|
||||
logger.error("Failed to remove tar file", err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
await axios.put(uploadUrl, fileBuffer, {
|
||||
headers: {
|
||||
"Content-Type": "application/zip",
|
||||
},
|
||||
onUploadProgress: (progressEvent) => {
|
||||
console.log(progressEvent);
|
||||
},
|
||||
});
|
||||
|
||||
WindowManager.mainWindow?.webContents.send(
|
||||
`on-upload-complete-${objectId}-${shop}`,
|
||||
true
|
||||
);
|
||||
|
||||
fs.rm(zipLocation, (err) => {
|
||||
if (err) {
|
||||
logger.error("Failed to remove zip file", err);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ export interface SearchGamesArgs {
|
||||
export const convertSteamGameToCatalogueEntry = (
|
||||
game: SteamGame
|
||||
): CatalogueEntry => ({
|
||||
objectID: String(game.id),
|
||||
objectId: String(game.id),
|
||||
title: game.name,
|
||||
shop: "steam" as GameShop,
|
||||
cover: steamUrlBuilder.library(String(game.id)),
|
||||
|
||||
@@ -10,14 +10,14 @@ import { steamUrlBuilder } from "@shared";
|
||||
|
||||
const addGameToLibrary = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
objectID: string,
|
||||
objectId: string,
|
||||
title: string,
|
||||
shop: GameShop
|
||||
) => {
|
||||
return gameRepository
|
||||
.update(
|
||||
{
|
||||
objectID,
|
||||
objectID: objectId,
|
||||
},
|
||||
{
|
||||
shop,
|
||||
@@ -27,23 +27,25 @@ const addGameToLibrary = async (
|
||||
)
|
||||
.then(async ({ affected }) => {
|
||||
if (!affected) {
|
||||
const steamGame = await steamGamesWorker.run(Number(objectID), {
|
||||
const steamGame = await steamGamesWorker.run(Number(objectId), {
|
||||
name: "getById",
|
||||
});
|
||||
|
||||
const iconUrl = steamGame?.clientIcon
|
||||
? steamUrlBuilder.icon(objectID, steamGame.clientIcon)
|
||||
? steamUrlBuilder.icon(objectId, steamGame.clientIcon)
|
||||
: null;
|
||||
|
||||
await gameRepository.insert({
|
||||
title,
|
||||
iconUrl,
|
||||
objectID,
|
||||
objectID: objectId,
|
||||
shop,
|
||||
});
|
||||
}
|
||||
|
||||
const game = await gameRepository.findOne({ where: { objectID } });
|
||||
const game = await gameRepository.findOne({
|
||||
where: { objectID: objectId },
|
||||
});
|
||||
|
||||
createGame(game!).catch(() => {});
|
||||
});
|
||||
|
||||
@@ -2,15 +2,15 @@ import { gameRepository } from "@main/repository";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const getGameByObjectID = async (
|
||||
const getGameByObjectId = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
objectID: string
|
||||
objectId: string
|
||||
) =>
|
||||
gameRepository.findOne({
|
||||
where: {
|
||||
objectID,
|
||||
objectID: objectId,
|
||||
isDeleted: false,
|
||||
},
|
||||
});
|
||||
|
||||
registerEvent("getGameByObjectID", getGameByObjectID);
|
||||
registerEvent("getGameByObjectId", getGameByObjectId);
|
||||
|
||||
@@ -14,7 +14,7 @@ const startGameDownload = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
payload: StartGameDownloadPayload
|
||||
) => {
|
||||
const { objectID, title, shop, downloadPath, downloader, uri } = payload;
|
||||
const { objectId, title, shop, downloadPath, downloader, uri } = payload;
|
||||
|
||||
return dataSource.transaction(async (transactionalEntityManager) => {
|
||||
const gameRepository = transactionalEntityManager.getRepository(Game);
|
||||
@@ -23,7 +23,7 @@ const startGameDownload = async (
|
||||
|
||||
const game = await gameRepository.findOne({
|
||||
where: {
|
||||
objectID,
|
||||
objectID: objectId,
|
||||
shop,
|
||||
},
|
||||
});
|
||||
@@ -51,18 +51,18 @@ const startGameDownload = async (
|
||||
}
|
||||
);
|
||||
} else {
|
||||
const steamGame = await steamGamesWorker.run(Number(objectID), {
|
||||
const steamGame = await steamGamesWorker.run(Number(objectId), {
|
||||
name: "getById",
|
||||
});
|
||||
|
||||
const iconUrl = steamGame?.clientIcon
|
||||
? steamUrlBuilder.icon(objectID, steamGame.clientIcon)
|
||||
? steamUrlBuilder.icon(objectId, steamGame.clientIcon)
|
||||
: null;
|
||||
|
||||
await gameRepository.insert({
|
||||
title,
|
||||
iconUrl,
|
||||
objectID,
|
||||
objectID: objectId,
|
||||
downloader,
|
||||
shop,
|
||||
status: "active",
|
||||
@@ -73,7 +73,7 @@ const startGameDownload = async (
|
||||
|
||||
const updatedGame = await gameRepository.findOne({
|
||||
where: {
|
||||
objectID,
|
||||
objectID: objectId,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user