feat: adding import download source

This commit is contained in:
Chubby Granny Chaser
2024-06-03 02:12:05 +01:00
parent ddd9ea69df
commit 48e07370e4
70 changed files with 925 additions and 1261 deletions

View File

@@ -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);

View File

@@ -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 (

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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);

View 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);

View 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);

View 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);

View 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);

View File

@@ -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"]
);

View 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),
})
),
});

View File

@@ -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());

View File

@@ -11,9 +11,6 @@ const getGameByObjectID = async (
objectID,
isDeleted: false,
},
relations: {
repack: true,
},
});
registerEvent("getGameByObjectID", getGameByObjectID);

View File

@@ -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);

View File

@@ -16,7 +16,6 @@ const resumeGameDownload = async (
id: gameId,
isDeleted: false,
},
relations: { repack: true },
});
if (!game) return;

View File

@@ -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!);