mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-28 21:31:03 +00:00
feat: adding import download source
This commit is contained in:
@@ -1,95 +1,35 @@
|
||||
import { formatName, getSteamAppAsset, repackerFormatter } from "@main/helpers";
|
||||
import type { CatalogueCategory, CatalogueEntry, GameShop } from "@types";
|
||||
import { getSteamAppAsset } from "@main/helpers";
|
||||
import type { CatalogueEntry, GameShop } from "@types";
|
||||
|
||||
import { stateManager } from "@main/state-manager";
|
||||
import { searchGames, searchRepacks } from "../helpers/search-games";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { requestSteam250 } from "@main/services";
|
||||
|
||||
const repacks = stateManager.getValue("repacks");
|
||||
|
||||
const getStringForLookup = (index: number): string => {
|
||||
const repack = repacks[index];
|
||||
const formatter =
|
||||
repackerFormatter[repack.repacker as keyof typeof repackerFormatter];
|
||||
|
||||
return formatName(formatter(repack.title));
|
||||
};
|
||||
import { SearchEngine, requestSteam250 } from "@main/services";
|
||||
|
||||
const resultSize = 12;
|
||||
|
||||
const getCatalogue = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
category: CatalogueCategory
|
||||
) => {
|
||||
if (!repacks.length) return [];
|
||||
|
||||
if (category === "trending") {
|
||||
return getTrendingCatalogue(resultSize);
|
||||
}
|
||||
|
||||
return getRecentlyAddedCatalogue(resultSize);
|
||||
};
|
||||
|
||||
const getTrendingCatalogue = async (
|
||||
resultSize: number
|
||||
): Promise<CatalogueEntry[]> => {
|
||||
const results: CatalogueEntry[] = [];
|
||||
const getCatalogue = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
const trendingGames = await requestSteam250("/90day");
|
||||
|
||||
for (
|
||||
let i = 0;
|
||||
i < trendingGames.length && results.length < resultSize;
|
||||
i++
|
||||
) {
|
||||
if (!trendingGames[i]) continue;
|
||||
|
||||
const { title, objectID } = trendingGames[i]!;
|
||||
const repacks = searchRepacks(title);
|
||||
|
||||
if (title && repacks.length) {
|
||||
const catalogueEntry = {
|
||||
objectID,
|
||||
title,
|
||||
shop: "steam" as GameShop,
|
||||
cover: getSteamAppAsset("library", objectID),
|
||||
};
|
||||
|
||||
results.push({ ...catalogueEntry, repacks });
|
||||
}
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
const getRecentlyAddedCatalogue = async (
|
||||
resultSize: number
|
||||
): Promise<CatalogueEntry[]> => {
|
||||
const results: CatalogueEntry[] = [];
|
||||
|
||||
for (let i = 0; results.length < resultSize; i++) {
|
||||
const stringForLookup = getStringForLookup(i);
|
||||
|
||||
if (!stringForLookup) {
|
||||
for (let i = 0; i < resultSize; i++) {
|
||||
if (!trendingGames[i]) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const games = searchGames({ query: stringForLookup });
|
||||
const { title, objectID } = trendingGames[i]!;
|
||||
const repacks = SearchEngine.searchRepacks(title);
|
||||
|
||||
for (const game of games) {
|
||||
const isAlreadyIncluded = results.some(
|
||||
(result) => result.objectID === game?.objectID
|
||||
);
|
||||
const catalogueEntry = {
|
||||
objectID,
|
||||
title,
|
||||
shop: "steam" as GameShop,
|
||||
cover: getSteamAppAsset("library", objectID),
|
||||
};
|
||||
|
||||
if (!game || !game.repacks.length || isAlreadyIncluded) {
|
||||
continue;
|
||||
}
|
||||
|
||||
results.push(game);
|
||||
}
|
||||
results.push({ ...catalogueEntry, repacks });
|
||||
}
|
||||
|
||||
return results.slice(0, resultSize);
|
||||
return results;
|
||||
};
|
||||
|
||||
registerEvent("getCatalogue", getCatalogue);
|
||||
|
||||
@@ -4,9 +4,9 @@ import { getSteamAppDetails } from "@main/services";
|
||||
import type { ShopDetails, GameShop, SteamAppDetails } from "@types";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
import { stateManager } from "@main/state-manager";
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
|
||||
const getLocalizedSteamAppDetails = (
|
||||
const getLocalizedSteamAppDetails = async (
|
||||
objectID: string,
|
||||
language: string
|
||||
): Promise<ShopDetails | null> => {
|
||||
@@ -14,20 +14,22 @@ const getLocalizedSteamAppDetails = (
|
||||
return getSteamAppDetails(objectID, language);
|
||||
}
|
||||
|
||||
return getSteamAppDetails(objectID, language).then((localizedAppDetails) => {
|
||||
const steamGame = stateManager
|
||||
.getValue("steamGames")
|
||||
.find((game) => game.id === Number(objectID));
|
||||
return getSteamAppDetails(objectID, language).then(
|
||||
async (localizedAppDetails) => {
|
||||
const steamGame = await steamGamesWorker.run(Number(objectID), {
|
||||
name: "getById",
|
||||
});
|
||||
|
||||
if (steamGame && localizedAppDetails) {
|
||||
return {
|
||||
...localizedAppDetails,
|
||||
name: steamGame.name,
|
||||
};
|
||||
if (steamGame && localizedAppDetails) {
|
||||
return {
|
||||
...localizedAppDetails,
|
||||
name: steamGame.name,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
);
|
||||
};
|
||||
|
||||
const getGameShopDetails = async (
|
||||
|
||||
@@ -1,39 +1,23 @@
|
||||
import type { CatalogueEntry, GameShop } from "@types";
|
||||
import type { CatalogueEntry } from "@types";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
import { searchRepacks } from "../helpers/search-games";
|
||||
import { stateManager } from "@main/state-manager";
|
||||
import { getSteamAppAsset } from "@main/helpers";
|
||||
|
||||
const steamGames = stateManager.getValue("steamGames");
|
||||
import { convertSteamGameToCatalogueEntry } from "../helpers/search-games";
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
|
||||
const getGames = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
take = 12,
|
||||
cursor = 0
|
||||
): Promise<{ results: CatalogueEntry[]; cursor: number }> => {
|
||||
const results: CatalogueEntry[] = [];
|
||||
const results = await steamGamesWorker.run(
|
||||
{ limit: take, offset: cursor },
|
||||
{ name: "list" }
|
||||
);
|
||||
|
||||
let i = 0 + cursor;
|
||||
|
||||
while (results.length < take) {
|
||||
const game = steamGames[i];
|
||||
const repacks = searchRepacks(game.name);
|
||||
|
||||
if (repacks.length) {
|
||||
results.push({
|
||||
objectID: String(game.id),
|
||||
title: game.name,
|
||||
shop: "steam" as GameShop,
|
||||
cover: getSteamAppAsset("library", String(game.id)),
|
||||
repacks,
|
||||
});
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return { results, cursor: i };
|
||||
return {
|
||||
results: results.map((result) => convertSteamGameToCatalogueEntry(result)),
|
||||
cursor: cursor + results.length,
|
||||
};
|
||||
};
|
||||
|
||||
registerEvent("getGames", getGames);
|
||||
|
||||
@@ -1,23 +1,36 @@
|
||||
import { shuffle } from "lodash-es";
|
||||
import { shuffle, slice } from "lodash-es";
|
||||
|
||||
import { getSteam250List } from "@main/services";
|
||||
import { SearchEngine, getSteam250List } from "@main/services";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
import { searchGames, searchRepacks } from "../helpers/search-games";
|
||||
import { searchSteamGames } from "../helpers/search-games";
|
||||
import type { Steam250Game } from "@types";
|
||||
|
||||
const state = { games: Array<Steam250Game>(), index: 0 };
|
||||
|
||||
const filterGames = async (games: Steam250Game[]) => {
|
||||
const results: Steam250Game[] = [];
|
||||
|
||||
for (const game of games) {
|
||||
const catalogue = await searchSteamGames({ query: game.title });
|
||||
|
||||
if (catalogue.length) {
|
||||
const repacks = SearchEngine.searchRepacks(catalogue[0].title);
|
||||
|
||||
if (repacks.length) {
|
||||
results.push(game);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
const getRandomGame = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
if (state.games.length == 0) {
|
||||
const steam250List = await getSteam250List();
|
||||
|
||||
const filteredSteam250List = steam250List.filter((game) => {
|
||||
const repacks = searchRepacks(game.title);
|
||||
const catalogue = searchGames({ query: game.title });
|
||||
|
||||
return repacks.length && catalogue.length;
|
||||
});
|
||||
const filteredSteam250List = await filterGames(steam250List);
|
||||
|
||||
state.games = shuffle(filteredSteam250List);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { searchRepacks } from "../helpers/search-games";
|
||||
import { SearchEngine } from "@main/services";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const searchGameRepacks = (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
query: string
|
||||
) => {
|
||||
return searchRepacks(query);
|
||||
};
|
||||
) => SearchEngine.searchRepacks(query);
|
||||
|
||||
registerEvent("searchGameRepacks", searchGameRepacks);
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { searchGames } from "../helpers/search-games";
|
||||
import { searchSteamGames } from "../helpers/search-games";
|
||||
import { CatalogueEntry } from "@types";
|
||||
|
||||
const searchGamesEvent = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
query: string
|
||||
): Promise<CatalogueEntry[]> => {
|
||||
return searchGames({ query, take: 12 });
|
||||
};
|
||||
): Promise<CatalogueEntry[]> => searchSteamGames({ query, limit: 12 });
|
||||
|
||||
registerEvent("searchGames", searchGamesEvent);
|
||||
|
||||
57
src/main/events/download-sources/add-download-source.ts
Normal file
57
src/main/events/download-sources/add-download-source.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { chunk } from "lodash-es";
|
||||
import { dataSource } from "@main/data-source";
|
||||
import { DownloadSource, Repack } from "@main/entity";
|
||||
import axios from "axios";
|
||||
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
||||
import { downloadSourceSchema } from "../helpers/validators";
|
||||
import { SearchEngine } from "@main/services";
|
||||
|
||||
const addDownloadSource = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
url: string
|
||||
) => {
|
||||
const response = await axios.get(url);
|
||||
|
||||
const source = downloadSourceSchema.parse(response.data);
|
||||
|
||||
const downloadSource = await dataSource.transaction(
|
||||
async (transactionalEntityManager) => {
|
||||
const downloadSource = await transactionalEntityManager
|
||||
.getRepository(DownloadSource)
|
||||
.save({ url, name: source.name });
|
||||
|
||||
const repacks: QueryDeepPartialEntity<Repack>[] = source.downloads.map(
|
||||
(download) => ({
|
||||
title: download.title,
|
||||
magnet: download.uris[0],
|
||||
fileSize: download.fileSize,
|
||||
repacker: source.name,
|
||||
uploadDate: download.uploadDate,
|
||||
downloadSource: { id: downloadSource.id },
|
||||
})
|
||||
);
|
||||
|
||||
const downloadsChunks = chunk(repacks, 800);
|
||||
|
||||
for (const chunk of downloadsChunks) {
|
||||
await transactionalEntityManager
|
||||
.getRepository(Repack)
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.values(chunk)
|
||||
.updateEntity(false)
|
||||
.orIgnore()
|
||||
.execute();
|
||||
}
|
||||
|
||||
return downloadSource;
|
||||
}
|
||||
);
|
||||
|
||||
await SearchEngine.updateRepacks();
|
||||
|
||||
return downloadSource;
|
||||
};
|
||||
|
||||
registerEvent("addDownloadSource", addDownloadSource);
|
||||
16
src/main/events/download-sources/get-download-sources.ts
Normal file
16
src/main/events/download-sources/get-download-sources.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { downloadSourceRepository } from "@main/repository";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||
return downloadSourceRepository
|
||||
.createQueryBuilder("downloadSource")
|
||||
.leftJoin("downloadSource.repacks", "repacks")
|
||||
.orderBy("downloadSource.createdAt", "DESC")
|
||||
.loadRelationCountAndMap(
|
||||
"downloadSource.repackCount",
|
||||
"downloadSource.repacks"
|
||||
)
|
||||
.getMany();
|
||||
};
|
||||
|
||||
registerEvent("getDownloadSources", getDownloadSources);
|
||||
13
src/main/events/download-sources/remove-download-source.ts
Normal file
13
src/main/events/download-sources/remove-download-source.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { downloadSourceRepository } from "@main/repository";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { SearchEngine } from "@main/services";
|
||||
|
||||
const removeDownloadSource = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
id: number
|
||||
) => {
|
||||
await downloadSourceRepository.delete(id);
|
||||
await SearchEngine.updateRepacks();
|
||||
};
|
||||
|
||||
registerEvent("removeDownloadSource", removeDownloadSource);
|
||||
39
src/main/events/download-sources/validate-download-source.ts
Normal file
39
src/main/events/download-sources/validate-download-source.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { z } from "zod";
|
||||
import { registerEvent } from "../register-event";
|
||||
import axios from "axios";
|
||||
import { downloadSourceRepository } from "@main/repository";
|
||||
|
||||
const downloadSourceSchema = z.object({
|
||||
name: z.string().max(255),
|
||||
downloads: z.array(
|
||||
z.object({
|
||||
title: z.string().max(255),
|
||||
objectId: z.string().max(255).nullable(),
|
||||
shop: z.enum(["steam"]).nullable(),
|
||||
downloaders: z.array(z.enum(["real_debrid", "torrent"])),
|
||||
uris: z.array(z.string()),
|
||||
uploadDate: z.string().max(255),
|
||||
fileSize: z.string().max(255),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
const validateDownloadSource = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
url: string
|
||||
) => {
|
||||
const response = await axios.get(url);
|
||||
|
||||
const source = downloadSourceSchema.parse(response.data);
|
||||
|
||||
const existingSource = await downloadSourceRepository.findOne({
|
||||
where: [{ url }, { name: source.name }],
|
||||
});
|
||||
|
||||
if (existingSource?.url === url)
|
||||
throw new Error("Source with the same url already exists");
|
||||
|
||||
return { name: source.name, downloadCount: source.downloads.length };
|
||||
};
|
||||
|
||||
registerEvent("validateDownloadSource", validateDownloadSource);
|
||||
@@ -1,40 +1,11 @@
|
||||
import flexSearch from "flexsearch";
|
||||
import { orderBy } from "lodash-es";
|
||||
import flexSearch from "flexsearch";
|
||||
|
||||
import type { GameRepack, GameShop, CatalogueEntry } from "@types";
|
||||
import type { GameShop, CatalogueEntry, SteamGame } from "@types";
|
||||
|
||||
import { formatName, getSteamAppAsset, repackerFormatter } from "@main/helpers";
|
||||
import { stateManager } from "@main/state-manager";
|
||||
|
||||
const { Index } = flexSearch;
|
||||
const repacksIndex = new Index();
|
||||
const steamGamesIndex = new Index({ tokenize: "forward" });
|
||||
|
||||
const repacks = stateManager.getValue("repacks");
|
||||
const steamGames = stateManager.getValue("steamGames");
|
||||
|
||||
for (let i = 0; i < repacks.length; i++) {
|
||||
const repack = repacks[i];
|
||||
const formatter =
|
||||
repackerFormatter[repack.repacker as keyof typeof repackerFormatter];
|
||||
|
||||
repacksIndex.add(i, formatName(formatter(repack.title)));
|
||||
}
|
||||
|
||||
for (let i = 0; i < steamGames.length; i++) {
|
||||
const steamGame = steamGames[i];
|
||||
steamGamesIndex.add(i, formatName(steamGame.name));
|
||||
}
|
||||
|
||||
export const searchRepacks = (title: string): GameRepack[] => {
|
||||
return orderBy(
|
||||
repacksIndex
|
||||
.search(formatName(title))
|
||||
.map((index) => repacks.at(index as number)!),
|
||||
["uploadDate"],
|
||||
"desc"
|
||||
);
|
||||
};
|
||||
import { getSteamAppAsset } from "@main/helpers";
|
||||
import { SearchEngine } from "@main/services";
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
|
||||
export interface SearchGamesArgs {
|
||||
query?: string;
|
||||
@@ -42,27 +13,25 @@ export interface SearchGamesArgs {
|
||||
skip?: number;
|
||||
}
|
||||
|
||||
export const searchGames = ({
|
||||
query,
|
||||
take,
|
||||
skip,
|
||||
}: SearchGamesArgs): CatalogueEntry[] => {
|
||||
const results = steamGamesIndex
|
||||
.search(formatName(query || ""), { limit: take, offset: skip })
|
||||
.map((index) => {
|
||||
const result = steamGames.at(index as number)!;
|
||||
export const convertSteamGameToCatalogueEntry = (
|
||||
result: SteamGame
|
||||
): CatalogueEntry => {
|
||||
return {
|
||||
objectID: String(result.id),
|
||||
title: result.name,
|
||||
shop: "steam" as GameShop,
|
||||
cover: getSteamAppAsset("library", String(result.id)),
|
||||
repacks: SearchEngine.searchRepacks(result.name),
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
objectID: String(result.id),
|
||||
title: result.name,
|
||||
shop: "steam" as GameShop,
|
||||
cover: getSteamAppAsset("library", String(result.id)),
|
||||
repacks: searchRepacks(result.name),
|
||||
};
|
||||
});
|
||||
export const searchSteamGames = async (
|
||||
options: flexSearch.SearchOptions
|
||||
): Promise<CatalogueEntry[]> => {
|
||||
const steamGames = await steamGamesWorker.run(options, { name: "search" });
|
||||
|
||||
return orderBy(
|
||||
results,
|
||||
steamGames.map((result) => convertSteamGameToCatalogueEntry(result)),
|
||||
[({ repacks }) => repacks.length, "repacks"],
|
||||
["desc"]
|
||||
);
|
||||
|
||||
16
src/main/events/helpers/validators.ts
Normal file
16
src/main/events/helpers/validators.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const downloadSourceSchema = z.object({
|
||||
name: z.string().max(255),
|
||||
downloads: z.array(
|
||||
z.object({
|
||||
title: z.string().max(255),
|
||||
objectId: z.string().max(255).nullable(),
|
||||
shop: z.enum(["steam"]).nullable(),
|
||||
downloaders: z.array(z.enum(["real_debrid", "torrent"])),
|
||||
uris: z.array(z.string()),
|
||||
uploadDate: z.string().max(255),
|
||||
fileSize: z.string().max(255),
|
||||
})
|
||||
),
|
||||
});
|
||||
@@ -30,6 +30,10 @@ import "./user-preferences/auto-launch";
|
||||
import "./autoupdater/check-for-updates";
|
||||
import "./autoupdater/restart-and-install-update";
|
||||
import "./user-preferences/authenticate-real-debrid";
|
||||
import "./download-sources/get-download-sources";
|
||||
import "./download-sources/validate-download-source";
|
||||
import "./download-sources/add-download-source";
|
||||
import "./download-sources/remove-download-source";
|
||||
|
||||
ipcMain.handle("ping", () => "pong");
|
||||
ipcMain.handle("getVersion", () => app.getVersion());
|
||||
|
||||
@@ -11,9 +11,6 @@ const getGameByObjectID = async (
|
||||
objectID,
|
||||
isDeleted: false,
|
||||
},
|
||||
relations: {
|
||||
repack: true,
|
||||
},
|
||||
});
|
||||
|
||||
registerEvent("getGameByObjectID", getGameByObjectID);
|
||||
|
||||
@@ -1,30 +1,14 @@
|
||||
import { gameRepository } from "@main/repository";
|
||||
|
||||
import { searchRepacks } from "../helpers/search-games";
|
||||
import { registerEvent } from "../register-event";
|
||||
import { sortBy } from "lodash-es";
|
||||
|
||||
const getLibrary = async () =>
|
||||
gameRepository
|
||||
.find({
|
||||
where: {
|
||||
isDeleted: false,
|
||||
},
|
||||
order: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
relations: {
|
||||
repack: true,
|
||||
},
|
||||
})
|
||||
.then((games) =>
|
||||
sortBy(
|
||||
games.map((game) => ({
|
||||
...game,
|
||||
repacks: searchRepacks(game.title),
|
||||
})),
|
||||
(game) => (game.status !== "removed" ? 0 : 1)
|
||||
)
|
||||
);
|
||||
gameRepository.find({
|
||||
where: {
|
||||
isDeleted: false,
|
||||
},
|
||||
order: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
registerEvent("getLibrary", getLibrary);
|
||||
|
||||
@@ -16,7 +16,6 @@ const resumeGameDownload = async (
|
||||
id: gameId,
|
||||
isDeleted: false,
|
||||
},
|
||||
relations: { repack: true },
|
||||
});
|
||||
|
||||
if (!game) return;
|
||||
|
||||
@@ -20,7 +20,6 @@ const startGameDownload = async (
|
||||
objectID,
|
||||
shop,
|
||||
},
|
||||
relations: { repack: true },
|
||||
}),
|
||||
repackRepository.findOne({
|
||||
where: {
|
||||
@@ -49,7 +48,7 @@ const startGameDownload = async (
|
||||
bytesDownloaded: 0,
|
||||
downloadPath,
|
||||
downloader,
|
||||
repack: { id: repackId },
|
||||
uri: repack.magnet,
|
||||
isDeleted: false,
|
||||
}
|
||||
);
|
||||
@@ -71,7 +70,7 @@ const startGameDownload = async (
|
||||
shop,
|
||||
status: "active",
|
||||
downloadPath,
|
||||
repack: { id: repackId },
|
||||
uri: repack.magnet,
|
||||
})
|
||||
.then((result) => {
|
||||
if (iconUrl) {
|
||||
@@ -88,7 +87,6 @@ const startGameDownload = async (
|
||||
where: {
|
||||
objectID,
|
||||
},
|
||||
relations: { repack: true },
|
||||
});
|
||||
|
||||
await DownloadManager.startDownload(updatedGame!);
|
||||
|
||||
Reference in New Issue
Block a user