mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-02 16:23:48 +03:00
feat: adding import download source
This commit is contained in:
parent
ddd9ea69df
commit
48e07370e4
@ -8,6 +8,7 @@ import {
|
|||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
|
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
|
||||||
import svgr from "vite-plugin-svgr";
|
import svgr from "vite-plugin-svgr";
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
loadEnv(mode);
|
loadEnv(mode);
|
||||||
|
|
||||||
|
@ -59,6 +59,7 @@
|
|||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lottie-react": "^2.4.0",
|
"lottie-react": "^2.4.0",
|
||||||
"parse-torrent": "^11.0.16",
|
"parse-torrent": "^11.0.16",
|
||||||
|
"piscina": "^4.5.1",
|
||||||
"ps-list": "^8.1.1",
|
"ps-list": "^8.1.1",
|
||||||
"react-i18next": "^14.1.0",
|
"react-i18next": "^14.1.0",
|
||||||
"react-loading-skeleton": "^3.4.0",
|
"react-loading-skeleton": "^3.4.0",
|
||||||
@ -66,7 +67,8 @@
|
|||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.22.3",
|
||||||
"typeorm": "^0.3.20",
|
"typeorm": "^0.3.20",
|
||||||
"user-agents": "^1.1.193",
|
"user-agents": "^1.1.193",
|
||||||
"yaml": "^2.4.1"
|
"yaml": "^2.4.1",
|
||||||
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^19.3.0",
|
"@commitlint/cli": "^19.3.0",
|
||||||
@ -82,9 +84,10 @@
|
|||||||
"@types/parse-torrent": "^5.8.7",
|
"@types/parse-torrent": "^5.8.7",
|
||||||
"@types/react": "^18.2.48",
|
"@types/react": "^18.2.48",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^18.2.18",
|
||||||
|
"@types/user-agents": "^1.0.4",
|
||||||
"@vanilla-extract/vite-plugin": "^4.0.7",
|
"@vanilla-extract/vite-plugin": "^4.0.7",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"electron": "^28.2.0",
|
"electron": "^30.0.9",
|
||||||
"electron-builder": "^24.9.1",
|
"electron-builder": "^24.9.1",
|
||||||
"electron-vite": "^2.0.0",
|
"electron-vite": "^2.0.0",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"home": {
|
"home": {
|
||||||
"featured": "Featured",
|
"featured": "Featured",
|
||||||
"recently_added": "Recently added",
|
|
||||||
"trending": "Trending",
|
"trending": "Trending",
|
||||||
"surprise_me": "Surprise me",
|
"surprise_me": "Surprise me",
|
||||||
"no_results": "No results found"
|
"no_results": "No results found"
|
||||||
@ -149,6 +148,7 @@
|
|||||||
"launch_with_system": "Launch Hydra on system start-up",
|
"launch_with_system": "Launch Hydra on system start-up",
|
||||||
"general": "General",
|
"general": "General",
|
||||||
"behavior": "Behavior",
|
"behavior": "Behavior",
|
||||||
|
"download_sources": "Download sources",
|
||||||
"real_debrid_api_token": "API Token",
|
"real_debrid_api_token": "API Token",
|
||||||
"enable_real_debrid": "Enable Real-Debrid",
|
"enable_real_debrid": "Enable Real-Debrid",
|
||||||
"real_debrid_description": "Real-Debrid is an unrestricted downloader that allows you to download files instantly and at the best of your Internet speed.",
|
"real_debrid_description": "Real-Debrid is an unrestricted downloader that allows you to download files instantly and at the best of your Internet speed.",
|
||||||
@ -157,7 +157,16 @@
|
|||||||
"real_debrid_free_account_error": "The account \"{{username}}\" is a free account. Please subscribe to Real-Debrid",
|
"real_debrid_free_account_error": "The account \"{{username}}\" is a free account. Please subscribe to Real-Debrid",
|
||||||
"real_debrid_linked_message": "Account \"{{username}}\" linked",
|
"real_debrid_linked_message": "Account \"{{username}}\" linked",
|
||||||
"save_changes": "Save changes",
|
"save_changes": "Save changes",
|
||||||
"changes_saved": "Changes successfully saved"
|
"changes_saved": "Changes successfully saved",
|
||||||
|
"download_sources_description": "Hydra will fetch the download links from these sources. The source URL must be a direct link to a .json file containing the download links.",
|
||||||
|
"validate_download_source": "Validate",
|
||||||
|
"remove_download_source": "Remove",
|
||||||
|
"add_download_source": "Add source",
|
||||||
|
"download_options_zero": "No download option",
|
||||||
|
"download_options_one": "{{countFormatted}} download option",
|
||||||
|
"download_options_other": "{{countFormatted}} download options",
|
||||||
|
"download_source_url": "Download source URL",
|
||||||
|
"add_download_source_description": "Insert the URL containing the JSON file"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download complete",
|
"download_complete": "Download complete",
|
||||||
@ -180,5 +189,8 @@
|
|||||||
},
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"close": "Close button"
|
"close": "Close button"
|
||||||
|
},
|
||||||
|
"forms": {
|
||||||
|
"toggle_password_visibility": "Toggle password visibility"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -145,6 +145,7 @@
|
|||||||
"launch_with_system": "Iniciar o Hydra junto com o sistema",
|
"launch_with_system": "Iniciar o Hydra junto com o sistema",
|
||||||
"general": "Geral",
|
"general": "Geral",
|
||||||
"behavior": "Comportamento",
|
"behavior": "Comportamento",
|
||||||
|
"download_sources": "Bibliotecas de download",
|
||||||
"real_debrid_api_token": "Token de API",
|
"real_debrid_api_token": "Token de API",
|
||||||
"enable_real_debrid": "Habilitar Real-Debrid",
|
"enable_real_debrid": "Habilitar Real-Debrid",
|
||||||
"real_debrid_api_token_hint": "Você pode obter seu token de API <0>aqui</0>",
|
"real_debrid_api_token_hint": "Você pode obter seu token de API <0>aqui</0>",
|
||||||
@ -153,7 +154,16 @@
|
|||||||
"real_debrid_free_account_error": "A conta \"{{username}}\" é uma conta gratuita. Por favor, assine a Real-Debrid",
|
"real_debrid_free_account_error": "A conta \"{{username}}\" é uma conta gratuita. Por favor, assine a Real-Debrid",
|
||||||
"real_debrid_linked_message": "Conta \"{{username}}\" vinculada",
|
"real_debrid_linked_message": "Conta \"{{username}}\" vinculada",
|
||||||
"save_changes": "Salvar mudanças",
|
"save_changes": "Salvar mudanças",
|
||||||
"changes_saved": "Ajustes salvos com sucesso"
|
"changes_saved": "Ajustes salvos com sucesso",
|
||||||
|
"download_sources_description": "Hydra vai buscar links de download em todas as bibliotecas habilitadas. A URL da biblioteca deve ser um link direto para um arquivo .json contendo uma lista de links.",
|
||||||
|
"validate_download_source": "Validar",
|
||||||
|
"remove_download_source": "Remover",
|
||||||
|
"add_download_source": "Adicionar biblioteca",
|
||||||
|
"download_options_zero": "Sem opções de download",
|
||||||
|
"download_options_one": "{{countFormatted}} opcão de download",
|
||||||
|
"download_options_other": "{{countFormatted}} opções de download",
|
||||||
|
"download_source_url": "URL da biblioteca",
|
||||||
|
"add_download_source_description": "Insira a URL contendo o arquivo JSON"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download concluído",
|
"download_complete": "Download concluído",
|
||||||
@ -180,5 +190,8 @@
|
|||||||
},
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"close": "Botão de fechar"
|
"close": "Botão de fechar"
|
||||||
|
},
|
||||||
|
"forms": {
|
||||||
|
"toggle_password_visibility": "Alternar visibilidade da senha"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,6 @@
|
|||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
export const repackersOn1337x = [
|
|
||||||
"DODI",
|
|
||||||
"FitGirl",
|
|
||||||
"0xEMPRESS",
|
|
||||||
"KaOsKrew",
|
|
||||||
"TinyRepacks",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const repackers = [
|
|
||||||
...repackersOn1337x,
|
|
||||||
"Xatab",
|
|
||||||
"TinyRepacks",
|
|
||||||
"CPG",
|
|
||||||
"GOG",
|
|
||||||
"onlinefix",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const defaultDownloadsPath = app.getPath("downloads");
|
export const defaultDownloadsPath = app.getPath("downloads");
|
||||||
|
|
||||||
export const databasePath = path.join(
|
export const databasePath = path.join(
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
import { DataSource } from "typeorm";
|
import { DataSource } from "typeorm";
|
||||||
import { Game, GameShopCache, Repack, UserPreferences } from "@main/entity";
|
import {
|
||||||
|
DownloadSource,
|
||||||
|
Game,
|
||||||
|
GameShopCache,
|
||||||
|
Repack,
|
||||||
|
UserPreferences,
|
||||||
|
} from "@main/entity";
|
||||||
import type { BetterSqlite3ConnectionOptions } from "typeorm/driver/better-sqlite3/BetterSqlite3ConnectionOptions";
|
import type { BetterSqlite3ConnectionOptions } from "typeorm/driver/better-sqlite3/BetterSqlite3ConnectionOptions";
|
||||||
|
|
||||||
import { databasePath } from "./constants";
|
import { databasePath } from "./constants";
|
||||||
@ -10,7 +16,7 @@ export const createDataSource = (
|
|||||||
) =>
|
) =>
|
||||||
new DataSource({
|
new DataSource({
|
||||||
type: "better-sqlite3",
|
type: "better-sqlite3",
|
||||||
entities: [Game, Repack, UserPreferences, GameShopCache],
|
entities: [Game, Repack, UserPreferences, GameShopCache, DownloadSource],
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
database: databasePath,
|
database: databasePath,
|
||||||
...options,
|
...options,
|
||||||
|
30
src/main/entity/download-source.entity.ts
Normal file
30
src/main/entity/download-source.entity.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
OneToMany,
|
||||||
|
} from "typeorm";
|
||||||
|
import { Repack } from "./repack.entity";
|
||||||
|
|
||||||
|
@Entity("download_source")
|
||||||
|
export class DownloadSource {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column("text", { nullable: true, unique: true })
|
||||||
|
url: string;
|
||||||
|
|
||||||
|
@Column("text", { unique: true })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@OneToMany(() => Repack, (repack) => repack.downloadSource, { cascade: true })
|
||||||
|
repacks: Repack[];
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
@ -63,6 +63,9 @@ export class Game {
|
|||||||
@Column("float", { default: 0 })
|
@Column("float", { default: 0 })
|
||||||
fileSize: number;
|
fileSize: number;
|
||||||
|
|
||||||
|
@Column("text", { nullable: true })
|
||||||
|
uri: string | null;
|
||||||
|
|
||||||
@OneToOne(() => Repack, { nullable: true })
|
@OneToOne(() => Repack, { nullable: true })
|
||||||
@JoinColumn()
|
@JoinColumn()
|
||||||
repack: Repack;
|
repack: Repack;
|
||||||
|
@ -2,3 +2,4 @@ export * from "./game.entity";
|
|||||||
export * from "./repack.entity";
|
export * from "./repack.entity";
|
||||||
export * from "./user-preferences.entity";
|
export * from "./user-preferences.entity";
|
||||||
export * from "./game-shop-cache.entity";
|
export * from "./game-shop-cache.entity";
|
||||||
|
export * from "./download-source.entity";
|
||||||
|
@ -4,7 +4,9 @@ import {
|
|||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
|
ManyToOne,
|
||||||
} from "typeorm";
|
} from "typeorm";
|
||||||
|
import { DownloadSource } from "./download-source.entity";
|
||||||
|
|
||||||
@Entity("repack")
|
@Entity("repack")
|
||||||
export class Repack {
|
export class Repack {
|
||||||
@ -17,9 +19,6 @@ export class Repack {
|
|||||||
@Column("text", { unique: true })
|
@Column("text", { unique: true })
|
||||||
magnet: string;
|
magnet: string;
|
||||||
|
|
||||||
@Column("int")
|
|
||||||
page: number;
|
|
||||||
|
|
||||||
@Column("text")
|
@Column("text")
|
||||||
repacker: string;
|
repacker: string;
|
||||||
|
|
||||||
@ -29,6 +28,9 @@ export class Repack {
|
|||||||
@Column("datetime")
|
@Column("datetime")
|
||||||
uploadDate: Date | string;
|
uploadDate: Date | string;
|
||||||
|
|
||||||
|
@ManyToOne(() => DownloadSource, { nullable: true, onDelete: "CASCADE" })
|
||||||
|
downloadSource: DownloadSource;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
|
@ -1,95 +1,35 @@
|
|||||||
import { formatName, getSteamAppAsset, repackerFormatter } from "@main/helpers";
|
import { getSteamAppAsset } from "@main/helpers";
|
||||||
import type { CatalogueCategory, CatalogueEntry, GameShop } from "@types";
|
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 { registerEvent } from "../register-event";
|
||||||
import { requestSteam250 } from "@main/services";
|
import { SearchEngine, 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));
|
|
||||||
};
|
|
||||||
|
|
||||||
const resultSize = 12;
|
const resultSize = 12;
|
||||||
|
|
||||||
const getCatalogue = async (
|
const getCatalogue = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
_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 trendingGames = await requestSteam250("/90day");
|
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[] = [];
|
const results: CatalogueEntry[] = [];
|
||||||
|
|
||||||
for (let i = 0; results.length < resultSize; i++) {
|
for (let i = 0; i < resultSize; i++) {
|
||||||
const stringForLookup = getStringForLookup(i);
|
if (!trendingGames[i]) {
|
||||||
|
|
||||||
if (!stringForLookup) {
|
|
||||||
i++;
|
i++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const games = searchGames({ query: stringForLookup });
|
const { title, objectID } = trendingGames[i]!;
|
||||||
|
const repacks = SearchEngine.searchRepacks(title);
|
||||||
|
|
||||||
for (const game of games) {
|
const catalogueEntry = {
|
||||||
const isAlreadyIncluded = results.some(
|
objectID,
|
||||||
(result) => result.objectID === game?.objectID
|
title,
|
||||||
);
|
shop: "steam" as GameShop,
|
||||||
|
cover: getSteamAppAsset("library", objectID),
|
||||||
|
};
|
||||||
|
|
||||||
if (!game || !game.repacks.length || isAlreadyIncluded) {
|
results.push({ ...catalogueEntry, repacks });
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
results.push(game);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return results.slice(0, resultSize);
|
return results;
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("getCatalogue", getCatalogue);
|
registerEvent("getCatalogue", getCatalogue);
|
||||||
|
@ -4,9 +4,9 @@ import { getSteamAppDetails } from "@main/services";
|
|||||||
import type { ShopDetails, GameShop, SteamAppDetails } from "@types";
|
import type { ShopDetails, GameShop, SteamAppDetails } from "@types";
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { stateManager } from "@main/state-manager";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
|
|
||||||
const getLocalizedSteamAppDetails = (
|
const getLocalizedSteamAppDetails = async (
|
||||||
objectID: string,
|
objectID: string,
|
||||||
language: string
|
language: string
|
||||||
): Promise<ShopDetails | null> => {
|
): Promise<ShopDetails | null> => {
|
||||||
@ -14,20 +14,22 @@ const getLocalizedSteamAppDetails = (
|
|||||||
return getSteamAppDetails(objectID, language);
|
return getSteamAppDetails(objectID, language);
|
||||||
}
|
}
|
||||||
|
|
||||||
return getSteamAppDetails(objectID, language).then((localizedAppDetails) => {
|
return getSteamAppDetails(objectID, language).then(
|
||||||
const steamGame = stateManager
|
async (localizedAppDetails) => {
|
||||||
.getValue("steamGames")
|
const steamGame = await steamGamesWorker.run(Number(objectID), {
|
||||||
.find((game) => game.id === Number(objectID));
|
name: "getById",
|
||||||
|
});
|
||||||
|
|
||||||
if (steamGame && localizedAppDetails) {
|
if (steamGame && localizedAppDetails) {
|
||||||
return {
|
return {
|
||||||
...localizedAppDetails,
|
...localizedAppDetails,
|
||||||
name: steamGame.name,
|
name: steamGame.name,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
);
|
||||||
return null;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getGameShopDetails = async (
|
const getGameShopDetails = async (
|
||||||
|
@ -1,39 +1,23 @@
|
|||||||
import type { CatalogueEntry, GameShop } from "@types";
|
import type { CatalogueEntry } from "@types";
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { searchRepacks } from "../helpers/search-games";
|
import { convertSteamGameToCatalogueEntry } from "../helpers/search-games";
|
||||||
import { stateManager } from "@main/state-manager";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
import { getSteamAppAsset } from "@main/helpers";
|
|
||||||
|
|
||||||
const steamGames = stateManager.getValue("steamGames");
|
|
||||||
|
|
||||||
const getGames = async (
|
const getGames = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
take = 12,
|
take = 12,
|
||||||
cursor = 0
|
cursor = 0
|
||||||
): Promise<{ results: CatalogueEntry[]; cursor: number }> => {
|
): Promise<{ results: CatalogueEntry[]; cursor: number }> => {
|
||||||
const results: CatalogueEntry[] = [];
|
const results = await steamGamesWorker.run(
|
||||||
|
{ limit: take, offset: cursor },
|
||||||
|
{ name: "list" }
|
||||||
|
);
|
||||||
|
|
||||||
let i = 0 + cursor;
|
return {
|
||||||
|
results: results.map((result) => convertSteamGameToCatalogueEntry(result)),
|
||||||
while (results.length < take) {
|
cursor: cursor + results.length,
|
||||||
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 };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("getGames", getGames);
|
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 { registerEvent } from "../register-event";
|
||||||
import { searchGames, searchRepacks } from "../helpers/search-games";
|
import { searchSteamGames } from "../helpers/search-games";
|
||||||
import type { Steam250Game } from "@types";
|
import type { Steam250Game } from "@types";
|
||||||
|
|
||||||
const state = { games: Array<Steam250Game>(), index: 0 };
|
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) => {
|
const getRandomGame = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
if (state.games.length == 0) {
|
if (state.games.length == 0) {
|
||||||
const steam250List = await getSteam250List();
|
const steam250List = await getSteam250List();
|
||||||
|
|
||||||
const filteredSteam250List = steam250List.filter((game) => {
|
const filteredSteam250List = await filterGames(steam250List);
|
||||||
const repacks = searchRepacks(game.title);
|
|
||||||
const catalogue = searchGames({ query: game.title });
|
|
||||||
|
|
||||||
return repacks.length && catalogue.length;
|
|
||||||
});
|
|
||||||
|
|
||||||
state.games = shuffle(filteredSteam250List);
|
state.games = shuffle(filteredSteam250List);
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import { searchRepacks } from "../helpers/search-games";
|
import { SearchEngine } from "@main/services";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
const searchGameRepacks = (
|
const searchGameRepacks = (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
query: string
|
query: string
|
||||||
) => {
|
) => SearchEngine.searchRepacks(query);
|
||||||
return searchRepacks(query);
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("searchGameRepacks", searchGameRepacks);
|
registerEvent("searchGameRepacks", searchGameRepacks);
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { searchGames } from "../helpers/search-games";
|
import { searchSteamGames } from "../helpers/search-games";
|
||||||
import { CatalogueEntry } from "@types";
|
import { CatalogueEntry } from "@types";
|
||||||
|
|
||||||
const searchGamesEvent = async (
|
const searchGamesEvent = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
query: string
|
query: string
|
||||||
): Promise<CatalogueEntry[]> => {
|
): Promise<CatalogueEntry[]> => searchSteamGames({ query, limit: 12 });
|
||||||
return searchGames({ query, take: 12 });
|
|
||||||
};
|
|
||||||
|
|
||||||
registerEvent("searchGames", searchGamesEvent);
|
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 { 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 { getSteamAppAsset } from "@main/helpers";
|
||||||
import { stateManager } from "@main/state-manager";
|
import { SearchEngine } from "@main/services";
|
||||||
|
import { steamGamesWorker } from "@main/workers";
|
||||||
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"
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface SearchGamesArgs {
|
export interface SearchGamesArgs {
|
||||||
query?: string;
|
query?: string;
|
||||||
@ -42,27 +13,25 @@ export interface SearchGamesArgs {
|
|||||||
skip?: number;
|
skip?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const searchGames = ({
|
export const convertSteamGameToCatalogueEntry = (
|
||||||
query,
|
result: SteamGame
|
||||||
take,
|
): CatalogueEntry => {
|
||||||
skip,
|
return {
|
||||||
}: SearchGamesArgs): CatalogueEntry[] => {
|
objectID: String(result.id),
|
||||||
const results = steamGamesIndex
|
title: result.name,
|
||||||
.search(formatName(query || ""), { limit: take, offset: skip })
|
shop: "steam" as GameShop,
|
||||||
.map((index) => {
|
cover: getSteamAppAsset("library", String(result.id)),
|
||||||
const result = steamGames.at(index as number)!;
|
repacks: SearchEngine.searchRepacks(result.name),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
export const searchSteamGames = async (
|
||||||
objectID: String(result.id),
|
options: flexSearch.SearchOptions
|
||||||
title: result.name,
|
): Promise<CatalogueEntry[]> => {
|
||||||
shop: "steam" as GameShop,
|
const steamGames = await steamGamesWorker.run(options, { name: "search" });
|
||||||
cover: getSteamAppAsset("library", String(result.id)),
|
|
||||||
repacks: searchRepacks(result.name),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return orderBy(
|
return orderBy(
|
||||||
results,
|
steamGames.map((result) => convertSteamGameToCatalogueEntry(result)),
|
||||||
[({ repacks }) => repacks.length, "repacks"],
|
[({ repacks }) => repacks.length, "repacks"],
|
||||||
["desc"]
|
["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/check-for-updates";
|
||||||
import "./autoupdater/restart-and-install-update";
|
import "./autoupdater/restart-and-install-update";
|
||||||
import "./user-preferences/authenticate-real-debrid";
|
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("ping", () => "pong");
|
||||||
ipcMain.handle("getVersion", () => app.getVersion());
|
ipcMain.handle("getVersion", () => app.getVersion());
|
||||||
|
@ -11,9 +11,6 @@ const getGameByObjectID = async (
|
|||||||
objectID,
|
objectID,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
},
|
},
|
||||||
relations: {
|
|
||||||
repack: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
registerEvent("getGameByObjectID", getGameByObjectID);
|
registerEvent("getGameByObjectID", getGameByObjectID);
|
||||||
|
@ -1,30 +1,14 @@
|
|||||||
import { gameRepository } from "@main/repository";
|
import { gameRepository } from "@main/repository";
|
||||||
|
|
||||||
import { searchRepacks } from "../helpers/search-games";
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { sortBy } from "lodash-es";
|
|
||||||
|
|
||||||
const getLibrary = async () =>
|
const getLibrary = async () =>
|
||||||
gameRepository
|
gameRepository.find({
|
||||||
.find({
|
where: {
|
||||||
where: {
|
isDeleted: false,
|
||||||
isDeleted: false,
|
},
|
||||||
},
|
order: {
|
||||||
order: {
|
createdAt: "desc",
|
||||||
createdAt: "desc",
|
},
|
||||||
},
|
});
|
||||||
relations: {
|
|
||||||
repack: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((games) =>
|
|
||||||
sortBy(
|
|
||||||
games.map((game) => ({
|
|
||||||
...game,
|
|
||||||
repacks: searchRepacks(game.title),
|
|
||||||
})),
|
|
||||||
(game) => (game.status !== "removed" ? 0 : 1)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
registerEvent("getLibrary", getLibrary);
|
registerEvent("getLibrary", getLibrary);
|
||||||
|
@ -16,7 +16,6 @@ const resumeGameDownload = async (
|
|||||||
id: gameId,
|
id: gameId,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
},
|
},
|
||||||
relations: { repack: true },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!game) return;
|
if (!game) return;
|
||||||
|
@ -20,7 +20,6 @@ const startGameDownload = async (
|
|||||||
objectID,
|
objectID,
|
||||||
shop,
|
shop,
|
||||||
},
|
},
|
||||||
relations: { repack: true },
|
|
||||||
}),
|
}),
|
||||||
repackRepository.findOne({
|
repackRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
@ -49,7 +48,7 @@ const startGameDownload = async (
|
|||||||
bytesDownloaded: 0,
|
bytesDownloaded: 0,
|
||||||
downloadPath,
|
downloadPath,
|
||||||
downloader,
|
downloader,
|
||||||
repack: { id: repackId },
|
uri: repack.magnet,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -71,7 +70,7 @@ const startGameDownload = async (
|
|||||||
shop,
|
shop,
|
||||||
status: "active",
|
status: "active",
|
||||||
downloadPath,
|
downloadPath,
|
||||||
repack: { id: repackId },
|
uri: repack.magnet,
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (iconUrl) {
|
if (iconUrl) {
|
||||||
@ -88,7 +87,6 @@ const startGameDownload = async (
|
|||||||
where: {
|
where: {
|
||||||
objectID,
|
objectID,
|
||||||
},
|
},
|
||||||
relations: { repack: true },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await DownloadManager.startDownload(updatedGame!);
|
await DownloadManager.startDownload(updatedGame!);
|
||||||
|
@ -1,98 +0,0 @@
|
|||||||
import assert from "node:assert/strict";
|
|
||||||
import { describe, test } from "node:test";
|
|
||||||
import {
|
|
||||||
dodiFormatter,
|
|
||||||
empressFormatter,
|
|
||||||
fitGirlFormatter,
|
|
||||||
kaosKrewFormatter,
|
|
||||||
} from "./formatters";
|
|
||||||
|
|
||||||
describe("testing formatters", () => {
|
|
||||||
describe("testing fitgirl formatter", () => {
|
|
||||||
const fitGirlGames = [
|
|
||||||
"REVEIL (v1.0.3f4 + 0.5 DLC, MULTi14) [FitGirl Repack]",
|
|
||||||
"Dune: Spice Wars - The Ixian Edition (v2.0.0.31558 + DLC, MULTi9) [FitGirl Repack]",
|
|
||||||
"HUMANKIND: Premium Edition (v1.0.22.3819 + 17 DLCs/Bonus Content, MULTi12) [FitGirl Repack, Selective Download - from 7.3 GB]",
|
|
||||||
"Call to Arms: Gates of Hell - Ostfront: WW2 Bundle (v1.034 Hotfix 3 + 3 DLCs, MULTi9) [FitGirl Repack, Selective Download - from 21.8 GB]",
|
|
||||||
"SUPER BOMBERMAN R 2 (v1.2.0, MULTi12) [FitGirl Repack]",
|
|
||||||
"God of Rock (v3110, MULTi11) [FitGirl Repack]",
|
|
||||||
];
|
|
||||||
|
|
||||||
test("should format games correctly", () => {
|
|
||||||
assert.equal(fitGirlGames.map(fitGirlFormatter), [
|
|
||||||
"REVEIL",
|
|
||||||
"Dune: Spice Wars - The Ixian Edition",
|
|
||||||
"HUMANKIND: Premium Edition",
|
|
||||||
"Call to Arms: Gates of Hell - Ostfront: WW2 Bundle",
|
|
||||||
"SUPER BOMBERMAN R 2",
|
|
||||||
"God of Rock",
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("testing kaoskrew formatter", () => {
|
|
||||||
const kaosKrewGames = [
|
|
||||||
"Song.Of.Horror.Complete.Edition.v1.25.MULTi4.REPACK-KaOs",
|
|
||||||
"Remoteness.REPACK-KaOs",
|
|
||||||
"Persona.5.Royal.v1.0.0.MULTi5.NSW.For.PC.REPACK-KaOs",
|
|
||||||
"The.Wreck.MULTi5.REPACK-KaOs",
|
|
||||||
"Nemezis.Mysterious.Journey.III.v1.04.Deluxe.Edition.REPACK-KaOs",
|
|
||||||
"The.World.Of.Others.v1.05.REPACK-KaOs",
|
|
||||||
];
|
|
||||||
|
|
||||||
test("should format games correctly", () => {
|
|
||||||
assert.equal(kaosKrewGames.map(kaosKrewFormatter), [
|
|
||||||
"Song Of Horror Complete Edition",
|
|
||||||
"Remoteness",
|
|
||||||
"Persona 5 Royal NSW For PC",
|
|
||||||
"The Wreck",
|
|
||||||
"Nemezis Mysterious Journey III Deluxe Edition",
|
|
||||||
"The World Of Others",
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("testing empress formatter", () => {
|
|
||||||
const empressGames = [
|
|
||||||
"Resident.Evil.4-EMPRESS",
|
|
||||||
"Marvels.Guardians.of.the.Galaxy.Crackfix-EMPRESS",
|
|
||||||
"Life.is.Strange.2.Complete.Edition-EMPRESS",
|
|
||||||
"Forza.Horizon.4.PROPER-EMPRESS",
|
|
||||||
"Just.Cause.4.Complete.Edition.READNFO-EMPRESS",
|
|
||||||
"Immortals.Fenyx.Rising.Crackfix.V2-EMPRESS",
|
|
||||||
];
|
|
||||||
|
|
||||||
test("should format games correctly", () => {
|
|
||||||
assert.equal(empressGames.map(empressFormatter), [
|
|
||||||
"Resident Evil 4",
|
|
||||||
"Marvels Guardians of the Galaxy",
|
|
||||||
"Life is Strange 2 Complete Edition",
|
|
||||||
"Forza Horizon 4 PROPER",
|
|
||||||
"Just Cause 4 Complete Edition",
|
|
||||||
"Immortals Fenyx Rising",
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("testing kodi formatter", () => {
|
|
||||||
const dodiGames = [
|
|
||||||
"Tomb Raider I-III Remastered Starring Lara Croft (MULTi20) (From 2.5 GB) [DODI Repack]",
|
|
||||||
"Trail Out: Complete Edition (v2.9st + All DLCs + MULTi11) [DODI Repack]",
|
|
||||||
"Call to Arms - Gates of Hell: Ostfront (v1.034.0 + All DLCs + MULTi9) (From 22.4 GB) [DODI Repack]",
|
|
||||||
"Metal Gear Solid 2: Sons of Liberty - HD Master Collection Edition (Digital book + MULTi6) [DODI Repack]",
|
|
||||||
"DREDGE: Digital Deluxe Edition (v1.2.0.1922 + All DLCs + Bonus Content + MULTi11) (From 413 MB) [DODI Repack]",
|
|
||||||
"Outliver: Tribulation [DODI Repack]",
|
|
||||||
];
|
|
||||||
|
|
||||||
test("should format games correctly", () => {
|
|
||||||
assert.equal(dodiGames.map(dodiFormatter), [
|
|
||||||
"Tomb Raider I-III Remastered Starring Lara Croft",
|
|
||||||
"Trail Out: Complete Edition",
|
|
||||||
"Call to Arms - Gates of Hell: Ostfront",
|
|
||||||
"Metal Gear Solid 2: Sons of Liberty - HD Master Collection Edition",
|
|
||||||
"DREDGE: Digital Deluxe Edition",
|
|
||||||
"Outliver: Tribulation",
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,56 +0,0 @@
|
|||||||
/* String formatting */
|
|
||||||
|
|
||||||
export const removeReleaseYearFromName = (name: string) =>
|
|
||||||
name.replace(/\([0-9]{4}\)/g, "");
|
|
||||||
|
|
||||||
export const removeSymbolsFromName = (name: string) =>
|
|
||||||
name.replace(/[^A-Za-z 0-9]/g, "");
|
|
||||||
|
|
||||||
export const removeSpecialEditionFromName = (name: string) =>
|
|
||||||
name.replace(
|
|
||||||
/(The |Digital )?(GOTY|Deluxe|Standard|Ultimate|Definitive|Enhanced|Collector's|Premium|Digital|Limited|Game of the Year|Reloaded|[0-9]{4}) Edition/g,
|
|
||||||
""
|
|
||||||
);
|
|
||||||
|
|
||||||
export const removeDuplicateSpaces = (name: string) =>
|
|
||||||
name.replace(/\s{2,}/g, " ");
|
|
||||||
|
|
||||||
export const removeTrash = (title: string) =>
|
|
||||||
title.replace(/\(.*\)|\[.*]/g, "").replace(/:/g, "");
|
|
||||||
|
|
||||||
/* Formatters per repacker */
|
|
||||||
|
|
||||||
export const fitGirlFormatter = (title: string) =>
|
|
||||||
title.replace(/\(.*\)/g, "").trim();
|
|
||||||
|
|
||||||
export const kaosKrewFormatter = (title: string) =>
|
|
||||||
title
|
|
||||||
.replace(/(v\.?[0-9])+([0-9]|\.)+/, "")
|
|
||||||
.replace(
|
|
||||||
/(\.Build\.[0-9]*)?(\.MULTi[0-9]{1,2})?(\.REPACK-KaOs|\.UPDATE-KaOs)?/g,
|
|
||||||
""
|
|
||||||
)
|
|
||||||
.replace(/\./g, " ")
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
export const empressFormatter = (title: string) =>
|
|
||||||
title
|
|
||||||
.replace(/-EMPRESS/, "")
|
|
||||||
.replace(/\./g, " ")
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
export const dodiFormatter = (title: string) =>
|
|
||||||
title.replace(/\(.*?\)/g, "").trim();
|
|
||||||
|
|
||||||
export const xatabFormatter = (title: string) =>
|
|
||||||
title
|
|
||||||
.replace(/RePack от xatab|RePack от Decepticon|R.G. GOGFAN/, "")
|
|
||||||
.replace(/[\u0400-\u04FF]/g, "")
|
|
||||||
.replace(/(v\.?([0-9]| )+)+([0-9]|\.|-|_|\/|[a-zA-Z]| )+/, "");
|
|
||||||
|
|
||||||
export const tinyRepacksFormatter = (title: string) => title;
|
|
||||||
export const onlinefixFormatter = (title: string) =>
|
|
||||||
title.replace("по сети", "").trim();
|
|
||||||
|
|
||||||
export const gogFormatter = (title: string) =>
|
|
||||||
title.replace(/(v\.[0-9]+|v[0-9]+\.|v[0-9]{4})+.+/, "");
|
|
@ -1,48 +1,5 @@
|
|||||||
import {
|
import axios from "axios";
|
||||||
removeReleaseYearFromName,
|
import UserAgent from "user-agents";
|
||||||
removeSymbolsFromName,
|
|
||||||
removeSpecialEditionFromName,
|
|
||||||
empressFormatter,
|
|
||||||
kaosKrewFormatter,
|
|
||||||
fitGirlFormatter,
|
|
||||||
removeDuplicateSpaces,
|
|
||||||
dodiFormatter,
|
|
||||||
removeTrash,
|
|
||||||
xatabFormatter,
|
|
||||||
tinyRepacksFormatter,
|
|
||||||
gogFormatter,
|
|
||||||
onlinefixFormatter,
|
|
||||||
} from "./formatters";
|
|
||||||
import { repackers } from "../constants";
|
|
||||||
|
|
||||||
export const pipe =
|
|
||||||
<T>(...fns: ((arg: T) => any)[]) =>
|
|
||||||
(arg: T) =>
|
|
||||||
fns.reduce((prev, fn) => fn(prev), arg);
|
|
||||||
|
|
||||||
export const formatName = pipe<string>(
|
|
||||||
removeTrash,
|
|
||||||
removeReleaseYearFromName,
|
|
||||||
removeSymbolsFromName,
|
|
||||||
removeSpecialEditionFromName,
|
|
||||||
removeDuplicateSpaces,
|
|
||||||
(str) => str.trim()
|
|
||||||
);
|
|
||||||
|
|
||||||
export const repackerFormatter: Record<
|
|
||||||
(typeof repackers)[number],
|
|
||||||
(title: string) => string
|
|
||||||
> = {
|
|
||||||
DODI: dodiFormatter,
|
|
||||||
"0xEMPRESS": empressFormatter,
|
|
||||||
KaOsKrew: kaosKrewFormatter,
|
|
||||||
FitGirl: fitGirlFormatter,
|
|
||||||
Xatab: xatabFormatter,
|
|
||||||
CPG: (title: string) => title,
|
|
||||||
TinyRepacks: tinyRepacksFormatter,
|
|
||||||
GOG: gogFormatter,
|
|
||||||
onlinefix: onlinefixFormatter,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getSteamAppAsset = (
|
export const getSteamAppAsset = (
|
||||||
category: "library" | "hero" | "logo" | "icon",
|
category: "library" | "hero" | "logo" | "icon",
|
||||||
@ -88,5 +45,16 @@ export const steamUrlBuilder = {
|
|||||||
export const sleep = (ms: number) =>
|
export const sleep = (ms: number) =>
|
||||||
new Promise((resolve) => setTimeout(resolve, ms));
|
new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
export * from "./formatters";
|
export const requestWebPage = async (url: string) => {
|
||||||
|
const userAgent = new UserAgent();
|
||||||
|
|
||||||
|
return axios
|
||||||
|
.get(url, {
|
||||||
|
headers: {
|
||||||
|
"User-Agent": userAgent.toString(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((response) => response.data);
|
||||||
|
};
|
||||||
|
|
||||||
export * from "./ps";
|
export * from "./ps";
|
||||||
|
@ -3,15 +3,11 @@ import updater from "electron-updater";
|
|||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
||||||
import {
|
import { DownloadManager, logger, WindowManager } from "@main/services";
|
||||||
DownloadManager,
|
|
||||||
logger,
|
|
||||||
resolveDatabaseUpdates,
|
|
||||||
WindowManager,
|
|
||||||
} from "@main/services";
|
|
||||||
import { dataSource } from "@main/data-source";
|
import { dataSource } from "@main/data-source";
|
||||||
import * as resources from "@locales";
|
import * as resources from "@locales";
|
||||||
import { userPreferencesRepository } from "@main/repository";
|
import { userPreferencesRepository } from "@main/repository";
|
||||||
|
|
||||||
const { autoUpdater } = updater;
|
const { autoUpdater } = updater;
|
||||||
|
|
||||||
autoUpdater.setFeedURL({
|
autoUpdater.setFeedURL({
|
||||||
@ -51,27 +47,24 @@ if (process.defaultApp) {
|
|||||||
// This method will be called when Electron has finished
|
// This method will be called when Electron has finished
|
||||||
// initialization and is ready to create browser windows.
|
// initialization and is ready to create browser windows.
|
||||||
// Some APIs can only be used after this event occurs.
|
// Some APIs can only be used after this event occurs.
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(async () => {
|
||||||
electronApp.setAppUserModelId("site.hydralauncher.hydra");
|
electronApp.setAppUserModelId("site.hydralauncher.hydra");
|
||||||
|
|
||||||
protocol.handle("hydra", (request) =>
|
protocol.handle("hydra", (request) =>
|
||||||
net.fetch("file://" + request.url.slice("hydra://".length))
|
net.fetch("file://" + request.url.slice("hydra://".length))
|
||||||
);
|
);
|
||||||
|
|
||||||
dataSource.initialize().then(async () => {
|
await dataSource.initialize();
|
||||||
await dataSource.runMigrations();
|
await dataSource.runMigrations();
|
||||||
|
|
||||||
await resolveDatabaseUpdates();
|
await import("./main");
|
||||||
|
|
||||||
await import("./main");
|
const userPreferences = await userPreferencesRepository.findOne({
|
||||||
|
where: { id: 1 },
|
||||||
const userPreferences = await userPreferencesRepository.findOne({
|
|
||||||
where: { id: 1 },
|
|
||||||
});
|
|
||||||
|
|
||||||
WindowManager.createMainWindow();
|
|
||||||
WindowManager.createSystemTray(userPreferences?.language || "en");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
WindowManager.createMainWindow();
|
||||||
|
WindowManager.createSystemTray(userPreferences?.language || "en");
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("browser-window-created", (_, window) => {
|
app.on("browser-window-created", (_, window) => {
|
||||||
|
@ -1,88 +1,12 @@
|
|||||||
import { stateManager } from "./state-manager";
|
import { DownloadManager, SearchEngine, startMainLoop } from "./services";
|
||||||
import { repackersOn1337x, seedsPath } from "./constants";
|
import { gameRepository, userPreferencesRepository } from "./repository";
|
||||||
import {
|
import { UserPreferences } from "./entity";
|
||||||
getNewGOGGames,
|
|
||||||
getNewRepacksFromUser,
|
|
||||||
getNewRepacksFromXatab,
|
|
||||||
getNewRepacksFromOnlineFix,
|
|
||||||
DownloadManager,
|
|
||||||
startMainLoop,
|
|
||||||
} from "./services";
|
|
||||||
import {
|
|
||||||
gameRepository,
|
|
||||||
repackRepository,
|
|
||||||
userPreferencesRepository,
|
|
||||||
} from "./repository";
|
|
||||||
import { Repack, UserPreferences } from "./entity";
|
|
||||||
import { Notification } from "electron";
|
|
||||||
import { t } from "i18next";
|
|
||||||
import fs from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import { RealDebridClient } from "./services/real-debrid";
|
import { RealDebridClient } from "./services/real-debrid";
|
||||||
import { orderBy } from "lodash-es";
|
|
||||||
import { SteamGame } from "@types";
|
|
||||||
import { Not } from "typeorm";
|
import { Not } from "typeorm";
|
||||||
|
|
||||||
startMainLoop();
|
startMainLoop();
|
||||||
|
|
||||||
const track1337xUsers = async (existingRepacks: Repack[]) => {
|
|
||||||
for (const repacker of repackersOn1337x) {
|
|
||||||
await getNewRepacksFromUser(
|
|
||||||
repacker,
|
|
||||||
existingRepacks.filter((repack) => repack.repacker === repacker)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkForNewRepacks = async (userPreferences: UserPreferences | null) => {
|
|
||||||
const existingRepacks = stateManager.getValue("repacks");
|
|
||||||
|
|
||||||
Promise.allSettled([
|
|
||||||
track1337xUsers(existingRepacks),
|
|
||||||
getNewRepacksFromXatab(
|
|
||||||
existingRepacks.filter((repack) => repack.repacker === "Xatab")
|
|
||||||
),
|
|
||||||
getNewGOGGames(
|
|
||||||
existingRepacks.filter((repack) => repack.repacker === "GOG")
|
|
||||||
),
|
|
||||||
getNewRepacksFromOnlineFix(
|
|
||||||
existingRepacks.filter((repack) => repack.repacker === "onlinefix")
|
|
||||||
),
|
|
||||||
]).then(() => {
|
|
||||||
repackRepository.count().then((count) => {
|
|
||||||
const total = count - stateManager.getValue("repacks").length;
|
|
||||||
|
|
||||||
if (total > 0 && userPreferences?.repackUpdatesNotificationsEnabled) {
|
|
||||||
new Notification({
|
|
||||||
title: t("repack_list_updated", {
|
|
||||||
ns: "notifications",
|
|
||||||
lng: userPreferences?.language || "en",
|
|
||||||
}),
|
|
||||||
body: t("repack_count", {
|
|
||||||
ns: "notifications",
|
|
||||||
lng: userPreferences?.language || "en",
|
|
||||||
count: total,
|
|
||||||
}),
|
|
||||||
}).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadState = async (userPreferences: UserPreferences | null) => {
|
const loadState = async (userPreferences: UserPreferences | null) => {
|
||||||
const repacks = repackRepository.find({
|
|
||||||
order: {
|
|
||||||
createdAt: "desc",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const steamGames = JSON.parse(
|
|
||||||
fs.readFileSync(path.join(seedsPath, "steam-games.json"), "utf-8")
|
|
||||||
) as SteamGame[];
|
|
||||||
|
|
||||||
stateManager.setValue("repacks", await repacks);
|
|
||||||
stateManager.setValue("steamGames", orderBy(steamGames, ["name"], "asc"));
|
|
||||||
|
|
||||||
import("./events");
|
import("./events");
|
||||||
|
|
||||||
if (userPreferences?.realDebridApiToken)
|
if (userPreferences?.realDebridApiToken)
|
||||||
@ -94,10 +18,10 @@ const loadState = async (userPreferences: UserPreferences | null) => {
|
|||||||
progress: Not(1),
|
progress: Not(1),
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
},
|
},
|
||||||
relations: { repack: true },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (game) DownloadManager.startDownload(game);
|
if (game) DownloadManager.startDownload(game);
|
||||||
|
await SearchEngine.updateRepacks();
|
||||||
};
|
};
|
||||||
|
|
||||||
userPreferencesRepository
|
userPreferencesRepository
|
||||||
@ -105,5 +29,5 @@ userPreferencesRepository
|
|||||||
where: { id: 1 },
|
where: { id: 1 },
|
||||||
})
|
})
|
||||||
.then((userPreferences) => {
|
.then((userPreferences) => {
|
||||||
loadState(userPreferences).then(() => checkForNewRepacks(userPreferences));
|
loadState(userPreferences);
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
import { dataSource } from "./data-source";
|
import { dataSource } from "./data-source";
|
||||||
import { Game, GameShopCache, Repack, UserPreferences } from "@main/entity";
|
import {
|
||||||
|
DownloadSource,
|
||||||
|
Game,
|
||||||
|
GameShopCache,
|
||||||
|
Repack,
|
||||||
|
UserPreferences,
|
||||||
|
} from "@main/entity";
|
||||||
|
|
||||||
export const gameRepository = dataSource.getRepository(Game);
|
export const gameRepository = dataSource.getRepository(Game);
|
||||||
|
|
||||||
@ -9,3 +15,6 @@ export const userPreferencesRepository =
|
|||||||
dataSource.getRepository(UserPreferences);
|
dataSource.getRepository(UserPreferences);
|
||||||
|
|
||||||
export const gameShopCacheRepository = dataSource.getRepository(GameShopCache);
|
export const gameShopCacheRepository = dataSource.getRepository(GameShopCache);
|
||||||
|
|
||||||
|
export const downloadSourceRepository =
|
||||||
|
dataSource.getRepository(DownloadSource);
|
||||||
|
@ -192,7 +192,6 @@ export class DownloadManager {
|
|||||||
|
|
||||||
const game = await gameRepository.findOne({
|
const game = await gameRepository.findOne({
|
||||||
where: { id: this.game.id, isDeleted: false },
|
where: { id: this.game.id, isDeleted: false },
|
||||||
relations: { repack: true },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (progress === 1 && this.game && !isDownloadingMetadata) {
|
if (progress === 1 && this.game && !isDownloadingMetadata) {
|
||||||
@ -291,10 +290,10 @@ export class DownloadManager {
|
|||||||
|
|
||||||
if (game.downloader === Downloader.RealDebrid) {
|
if (game.downloader === Downloader.RealDebrid) {
|
||||||
this.realDebridTorrentId = await RealDebridClient.getTorrentId(
|
this.realDebridTorrentId = await RealDebridClient.getTorrentId(
|
||||||
game!.repack.magnet
|
game!.uri!
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.gid = await this.aria2.call("addUri", [game.repack.magnet], options);
|
this.gid = await this.aria2.call("addUri", [game.uri!], options);
|
||||||
this.downloads.set(game.id, this.gid);
|
this.downloads.set(game.id, this.gid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { formatName } from "@main/helpers";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { JSDOM } from "jsdom";
|
import { JSDOM } from "jsdom";
|
||||||
import { requestWebPage } from "./repack-tracker/helpers";
|
import { requestWebPage } from "@main/helpers";
|
||||||
import { HowLongToBeatCategory } from "@types";
|
import { HowLongToBeatCategory } from "@types";
|
||||||
|
import { formatName } from "@shared";
|
||||||
|
|
||||||
export interface HowLongToBeatResult {
|
export interface HowLongToBeatResult {
|
||||||
game_id: number;
|
game_id: number;
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
export * from "./logger";
|
export * from "./logger";
|
||||||
export * from "./repack-tracker";
|
|
||||||
export * from "./steam";
|
export * from "./steam";
|
||||||
export * from "./steam-250";
|
export * from "./steam-250";
|
||||||
export * from "./steam-grid";
|
export * from "./steam-grid";
|
||||||
export * from "./update-resolver";
|
|
||||||
export * from "./window-manager";
|
export * from "./window-manager";
|
||||||
export * from "./download-manager";
|
export * from "./download-manager";
|
||||||
export * from "./how-long-to-beat";
|
export * from "./how-long-to-beat";
|
||||||
export * from "./process-watcher";
|
export * from "./process-watcher";
|
||||||
export * from "./main-loop";
|
export * from "./main-loop";
|
||||||
|
export * from "./search-engine";
|
||||||
|
@ -1,146 +0,0 @@
|
|||||||
import { JSDOM } from "jsdom";
|
|
||||||
|
|
||||||
import { Repack } from "@main/entity";
|
|
||||||
import { requestWebPage, savePage } from "./helpers";
|
|
||||||
|
|
||||||
const months = [
|
|
||||||
"Jan",
|
|
||||||
"Feb",
|
|
||||||
"Mar",
|
|
||||||
"Apr",
|
|
||||||
"May",
|
|
||||||
"Jun",
|
|
||||||
"Jul",
|
|
||||||
"Aug",
|
|
||||||
"Sep",
|
|
||||||
"Oct",
|
|
||||||
"Nov",
|
|
||||||
"Dec",
|
|
||||||
];
|
|
||||||
|
|
||||||
export const request1337x = async (path: string) =>
|
|
||||||
requestWebPage(`https://1337xx.to${path}`);
|
|
||||||
|
|
||||||
const formatUploadDate = (str: string) => {
|
|
||||||
const date = new Date();
|
|
||||||
|
|
||||||
const [month, day, year] = str.split(" ");
|
|
||||||
|
|
||||||
date.setMonth(months.indexOf(month.replace(".", "")));
|
|
||||||
date.setDate(Number(day.substring(0, 2)));
|
|
||||||
date.setFullYear(Number("20" + year.replace("'", "")));
|
|
||||||
date.setHours(0, 0, 0, 0);
|
|
||||||
|
|
||||||
return date;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* TODO: $a will often be null */
|
|
||||||
const getTorrentDetails = async (path: string) => {
|
|
||||||
const response = await request1337x(path);
|
|
||||||
|
|
||||||
const { window } = new JSDOM(response);
|
|
||||||
const { document } = window;
|
|
||||||
|
|
||||||
const $a = window.document.querySelector(
|
|
||||||
".torrentdown1"
|
|
||||||
) as HTMLAnchorElement;
|
|
||||||
|
|
||||||
const $ul = Array.from(
|
|
||||||
document.querySelectorAll(".torrent-detail-page .list")
|
|
||||||
);
|
|
||||||
const [$firstColumn, $secondColumn] = $ul;
|
|
||||||
|
|
||||||
if (!$firstColumn || !$secondColumn) {
|
|
||||||
return { magnet: $a?.href };
|
|
||||||
}
|
|
||||||
|
|
||||||
const [_$category, _$type, _$language, $totalSize] = $firstColumn.children;
|
|
||||||
const [_$downloads, _$lastChecked, $dateUploaded] = $secondColumn.children;
|
|
||||||
|
|
||||||
return {
|
|
||||||
magnet: $a?.href,
|
|
||||||
fileSize: $totalSize.querySelector("span")!.textContent,
|
|
||||||
uploadDate: formatUploadDate(
|
|
||||||
$dateUploaded.querySelector("span")!.textContent!
|
|
||||||
),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTorrentListLastPage = async (user: string) => {
|
|
||||||
const response = await request1337x(`/user/${user}/1`);
|
|
||||||
|
|
||||||
const { window } = new JSDOM(response);
|
|
||||||
|
|
||||||
const $ul = window.document.querySelector(".pagination > ul");
|
|
||||||
|
|
||||||
if ($ul) {
|
|
||||||
const $li = Array.from($ul.querySelectorAll("li")).at(-1);
|
|
||||||
const text = $li?.textContent;
|
|
||||||
|
|
||||||
if (text === ">>") {
|
|
||||||
const $previousLi = Array.from($ul.querySelectorAll("li")).at(-2);
|
|
||||||
return Number($previousLi?.textContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Number(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const extractTorrentsFromDocument = async (
|
|
||||||
page: number,
|
|
||||||
user: string,
|
|
||||||
document: Document
|
|
||||||
) => {
|
|
||||||
const $trs = Array.from(document.querySelectorAll("tbody tr"));
|
|
||||||
|
|
||||||
return Promise.all(
|
|
||||||
$trs.map(async ($tr) => {
|
|
||||||
const $td = $tr.querySelector("td");
|
|
||||||
|
|
||||||
const [, $name] = Array.from($td!.querySelectorAll("a"));
|
|
||||||
const url = $name.href;
|
|
||||||
const title = $name.textContent ?? "";
|
|
||||||
|
|
||||||
const details = await getTorrentDetails(url);
|
|
||||||
|
|
||||||
return {
|
|
||||||
title,
|
|
||||||
magnet: details.magnet,
|
|
||||||
fileSize: details.fileSize ?? "N/A",
|
|
||||||
uploadDate: details.uploadDate ?? new Date(),
|
|
||||||
repacker: user,
|
|
||||||
page,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getNewRepacksFromUser = async (
|
|
||||||
user: string,
|
|
||||||
existingRepacks: Repack[],
|
|
||||||
page = 1
|
|
||||||
) => {
|
|
||||||
const response = await request1337x(`/user/${user}/${page}`);
|
|
||||||
const { window } = new JSDOM(response);
|
|
||||||
|
|
||||||
const repacks = await extractTorrentsFromDocument(
|
|
||||||
page,
|
|
||||||
user,
|
|
||||||
window.document
|
|
||||||
);
|
|
||||||
|
|
||||||
const newRepacks = repacks.filter(
|
|
||||||
(repack) =>
|
|
||||||
!existingRepacks.some(
|
|
||||||
(existingRepack) => existingRepack.title === repack.title
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!newRepacks.length) return;
|
|
||||||
|
|
||||||
await savePage(newRepacks);
|
|
||||||
|
|
||||||
return getNewRepacksFromUser(user, existingRepacks, page + 1);
|
|
||||||
};
|
|
@ -1,96 +0,0 @@
|
|||||||
import { JSDOM, VirtualConsole } from "jsdom";
|
|
||||||
import { requestWebPage, savePage } from "./helpers";
|
|
||||||
import { Repack } from "@main/entity";
|
|
||||||
|
|
||||||
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
|
||||||
|
|
||||||
const virtualConsole = new VirtualConsole();
|
|
||||||
|
|
||||||
const getUploadDate = (document: Document) => {
|
|
||||||
const $modifiedTime = document.querySelector(
|
|
||||||
'[property="article:modified_time"]'
|
|
||||||
) as HTMLMetaElement;
|
|
||||||
if ($modifiedTime) return $modifiedTime.content;
|
|
||||||
|
|
||||||
const $publishedTime = document.querySelector(
|
|
||||||
'[property="article:published_time"]'
|
|
||||||
) as HTMLMetaElement;
|
|
||||||
return $publishedTime.content;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDownloadLink = (document: Document) => {
|
|
||||||
const $latestDownloadButton = document.querySelector(
|
|
||||||
".download-btn:not(.lightweight-accordion *)"
|
|
||||||
) as HTMLAnchorElement;
|
|
||||||
if ($latestDownloadButton) return $latestDownloadButton.href;
|
|
||||||
|
|
||||||
const $downloadButton = document.querySelector(
|
|
||||||
".download-btn"
|
|
||||||
) as HTMLAnchorElement;
|
|
||||||
if (!$downloadButton) return null;
|
|
||||||
|
|
||||||
return $downloadButton.href;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMagnet = (downloadLink: string) => {
|
|
||||||
if (downloadLink.startsWith("http")) {
|
|
||||||
const { searchParams } = new URL(downloadLink);
|
|
||||||
return Buffer.from(searchParams.get("url")!, "base64").toString("utf-8");
|
|
||||||
}
|
|
||||||
|
|
||||||
return downloadLink;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getGOGGame = async (url: string) => {
|
|
||||||
const data = await requestWebPage(url);
|
|
||||||
const { window } = new JSDOM(data, { virtualConsole });
|
|
||||||
|
|
||||||
const downloadLink = getDownloadLink(window.document);
|
|
||||||
if (!downloadLink) return null;
|
|
||||||
|
|
||||||
const $em = window.document.querySelector("p em");
|
|
||||||
if (!$em) return null;
|
|
||||||
const fileSize = $em.textContent!.split("Size: ").at(1);
|
|
||||||
|
|
||||||
return {
|
|
||||||
fileSize: fileSize ?? "N/A",
|
|
||||||
uploadDate: new Date(getUploadDate(window.document)),
|
|
||||||
repacker: "GOG",
|
|
||||||
magnet: getMagnet(downloadLink),
|
|
||||||
page: 1,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getNewGOGGames = async (existingRepacks: Repack[] = []) => {
|
|
||||||
const data = await requestWebPage(
|
|
||||||
"https://freegogpcgames.com/a-z-games-list/"
|
|
||||||
);
|
|
||||||
|
|
||||||
const { window } = new JSDOM(data, { virtualConsole });
|
|
||||||
|
|
||||||
const $uls = Array.from(window.document.querySelectorAll(".az-columns"));
|
|
||||||
|
|
||||||
for (const $ul of $uls) {
|
|
||||||
const repacks: QueryDeepPartialEntity<Repack>[] = [];
|
|
||||||
const $lis = Array.from($ul.querySelectorAll("li"));
|
|
||||||
|
|
||||||
for (const $li of $lis) {
|
|
||||||
const $a = $li.querySelector("a")!;
|
|
||||||
const href = $a.href;
|
|
||||||
|
|
||||||
const title = $a.textContent!.trim();
|
|
||||||
|
|
||||||
const gameExists = existingRepacks.some(
|
|
||||||
(existingRepack) => existingRepack.title === title
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!gameExists) {
|
|
||||||
const game = await getGOGGame(href);
|
|
||||||
|
|
||||||
if (game) repacks.push({ ...game, title });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (repacks.length) await savePage(repacks);
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,40 +0,0 @@
|
|||||||
import axios from "axios";
|
|
||||||
import UserAgent from "user-agents";
|
|
||||||
|
|
||||||
import type { Repack } from "@main/entity";
|
|
||||||
import { repackRepository } from "@main/repository";
|
|
||||||
|
|
||||||
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
|
||||||
|
|
||||||
export const savePage = async (repacks: QueryDeepPartialEntity<Repack>[]) =>
|
|
||||||
Promise.all(
|
|
||||||
repacks.map((repack) => repackRepository.insert(repack).catch(() => {}))
|
|
||||||
);
|
|
||||||
|
|
||||||
export const requestWebPage = async (url: string) => {
|
|
||||||
const userAgent = new UserAgent();
|
|
||||||
|
|
||||||
return axios
|
|
||||||
.get(url, {
|
|
||||||
headers: {
|
|
||||||
"User-Agent": userAgent.toString(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((response) => response.data);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const decodeNonUtf8Response = async (res: Response) => {
|
|
||||||
const contentType = res.headers.get("content-type");
|
|
||||||
if (!contentType) return res.text();
|
|
||||||
|
|
||||||
const charset = contentType.substring(contentType.indexOf("charset=") + 8);
|
|
||||||
|
|
||||||
const text = await res.arrayBuffer().then((ab) => {
|
|
||||||
const dataView = new DataView(ab);
|
|
||||||
const decoder = new TextDecoder(charset);
|
|
||||||
|
|
||||||
return decoder.decode(dataView);
|
|
||||||
});
|
|
||||||
|
|
||||||
return text;
|
|
||||||
};
|
|
@ -1,4 +0,0 @@
|
|||||||
export * from "./1337x";
|
|
||||||
export * from "./xatab";
|
|
||||||
export * from "./gog";
|
|
||||||
export * from "./online-fix";
|
|
@ -1,157 +0,0 @@
|
|||||||
import { Repack } from "@main/entity";
|
|
||||||
import { decodeNonUtf8Response, savePage } from "./helpers";
|
|
||||||
import { logger } from "../logger";
|
|
||||||
import { JSDOM } from "jsdom";
|
|
||||||
|
|
||||||
import createWorker from "@main/workers/torrent-parser.worker?nodeWorker";
|
|
||||||
import { toMagnetURI } from "parse-torrent";
|
|
||||||
|
|
||||||
const worker = createWorker({});
|
|
||||||
|
|
||||||
import makeFetchCookie from "fetch-cookie";
|
|
||||||
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
|
||||||
import { formatBytes } from "@shared";
|
|
||||||
|
|
||||||
const ONLINE_FIX_URL = "https://online-fix.me/";
|
|
||||||
|
|
||||||
let totalPages = 1;
|
|
||||||
|
|
||||||
export const getNewRepacksFromOnlineFix = async (
|
|
||||||
existingRepacks: Repack[] = [],
|
|
||||||
page = 1,
|
|
||||||
cookieJar = new makeFetchCookie.toughCookie.CookieJar()
|
|
||||||
): Promise<void> => {
|
|
||||||
const hasCredentials =
|
|
||||||
import.meta.env.MAIN_VITE_ONLINEFIX_USERNAME &&
|
|
||||||
import.meta.env.MAIN_VITE_ONLINEFIX_PASSWORD;
|
|
||||||
if (!hasCredentials) return;
|
|
||||||
|
|
||||||
const http = makeFetchCookie(fetch, cookieJar);
|
|
||||||
|
|
||||||
if (page === 1) {
|
|
||||||
await http(ONLINE_FIX_URL);
|
|
||||||
|
|
||||||
const preLogin =
|
|
||||||
((await http("https://online-fix.me/engine/ajax/authtoken.php", {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"X-Requested-With": "XMLHttpRequest",
|
|
||||||
Referer: ONLINE_FIX_URL,
|
|
||||||
},
|
|
||||||
}).then((res) => res.json())) as {
|
|
||||||
field: string;
|
|
||||||
value: string;
|
|
||||||
}) || undefined;
|
|
||||||
|
|
||||||
if (!preLogin.field || !preLogin.value) return;
|
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
login_name: import.meta.env.MAIN_VITE_ONLINEFIX_USERNAME,
|
|
||||||
login_password: import.meta.env.MAIN_VITE_ONLINEFIX_PASSWORD,
|
|
||||||
login: "submit",
|
|
||||||
[preLogin.field]: preLogin.value,
|
|
||||||
});
|
|
||||||
|
|
||||||
await http(ONLINE_FIX_URL, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Referer: ONLINE_FIX_URL,
|
|
||||||
Origin: ONLINE_FIX_URL,
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
|
||||||
},
|
|
||||||
body: params.toString(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const pageParams = page > 1 ? `${`/page/${page}`}` : "";
|
|
||||||
|
|
||||||
const home = await http(`https://online-fix.me${pageParams}`).then((res) =>
|
|
||||||
decodeNonUtf8Response(res)
|
|
||||||
);
|
|
||||||
const document = new JSDOM(home).window.document;
|
|
||||||
|
|
||||||
const repacks: QueryDeepPartialEntity<Repack>[] = [];
|
|
||||||
const articles = Array.from(document.querySelectorAll(".news"));
|
|
||||||
|
|
||||||
if (page == 1) {
|
|
||||||
totalPages = Number(
|
|
||||||
document.querySelector("nav > a:nth-child(13)")?.textContent
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Promise.all(
|
|
||||||
articles.map(async (article) => {
|
|
||||||
const gameLink = article.querySelector("a")?.getAttribute("href");
|
|
||||||
if (!gameLink) return;
|
|
||||||
|
|
||||||
const gamePage = await http(gameLink).then((res) =>
|
|
||||||
decodeNonUtf8Response(res)
|
|
||||||
);
|
|
||||||
const gameDocument = new JSDOM(gamePage).window.document;
|
|
||||||
|
|
||||||
const torrentButtons = Array.from(
|
|
||||||
gameDocument.querySelectorAll("a")
|
|
||||||
).filter((a) => a.textContent?.includes("Torrent"));
|
|
||||||
|
|
||||||
const torrentPrePage = torrentButtons[0]?.getAttribute("href");
|
|
||||||
if (!torrentPrePage) return;
|
|
||||||
|
|
||||||
const torrentPage = await http(torrentPrePage, {
|
|
||||||
headers: {
|
|
||||||
Referer: gameLink,
|
|
||||||
},
|
|
||||||
}).then((res) => res.text());
|
|
||||||
|
|
||||||
const torrentDocument = new JSDOM(torrentPage).window.document;
|
|
||||||
|
|
||||||
const torrentLink = torrentDocument
|
|
||||||
.querySelector("a:nth-child(2)")
|
|
||||||
?.getAttribute("href");
|
|
||||||
|
|
||||||
const torrentFile = Buffer.from(
|
|
||||||
await http(`${torrentPrePage}${torrentLink}`).then((res) =>
|
|
||||||
res.arrayBuffer()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
worker.once("message", (torrent) => {
|
|
||||||
if (!torrent) return;
|
|
||||||
|
|
||||||
const { name, created } = torrent;
|
|
||||||
|
|
||||||
repacks.push({
|
|
||||||
fileSize: formatBytes(torrent.length ?? 0),
|
|
||||||
magnet: toMagnetURI(torrent),
|
|
||||||
page: 1,
|
|
||||||
repacker: "onlinefix",
|
|
||||||
title: name,
|
|
||||||
uploadDate: created,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
worker.postMessage(torrentFile);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (err: unknown) {
|
|
||||||
logger.error((err as Error).message, {
|
|
||||||
method: "getNewRepacksFromOnlineFix",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const newRepacks = repacks.filter(
|
|
||||||
(repack) =>
|
|
||||||
repack.uploadDate &&
|
|
||||||
!existingRepacks.some(
|
|
||||||
(existingRepack) => existingRepack.title === repack.title
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!newRepacks.length) return;
|
|
||||||
|
|
||||||
await savePage(newRepacks);
|
|
||||||
|
|
||||||
if (page === totalPages) return;
|
|
||||||
|
|
||||||
return getNewRepacksFromOnlineFix(existingRepacks, page + 1, cookieJar);
|
|
||||||
};
|
|
@ -1,120 +0,0 @@
|
|||||||
import { JSDOM } from "jsdom";
|
|
||||||
|
|
||||||
import { Repack } from "@main/entity";
|
|
||||||
import { logger } from "../logger";
|
|
||||||
import { requestWebPage, savePage } from "./helpers";
|
|
||||||
|
|
||||||
import createWorker from "@main/workers/torrent-parser.worker?nodeWorker";
|
|
||||||
import { toMagnetURI } from "parse-torrent";
|
|
||||||
import type { Instance } from "parse-torrent";
|
|
||||||
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
|
||||||
import { formatBytes } from "@shared";
|
|
||||||
import { getFileBuffer } from "@main/helpers";
|
|
||||||
|
|
||||||
const worker = createWorker({});
|
|
||||||
worker.setMaxListeners(11);
|
|
||||||
|
|
||||||
let totalPages = 1;
|
|
||||||
|
|
||||||
const formatXatabDate = (str: string) => {
|
|
||||||
const date = new Date();
|
|
||||||
|
|
||||||
const [day, month, year] = str.split(".");
|
|
||||||
|
|
||||||
date.setDate(Number(day));
|
|
||||||
date.setMonth(Number(month) - 1);
|
|
||||||
date.setFullYear(Number(year));
|
|
||||||
date.setHours(0, 0, 0, 0);
|
|
||||||
|
|
||||||
return date;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getXatabRepack = (
|
|
||||||
url: string
|
|
||||||
): Promise<{ fileSize: string; magnet: string; uploadDate: Date } | null> => {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
(async () => {
|
|
||||||
const data = await requestWebPage(url);
|
|
||||||
const { window } = new JSDOM(data);
|
|
||||||
const { document } = window;
|
|
||||||
|
|
||||||
const $uploadDate = document.querySelector(".entry__date");
|
|
||||||
|
|
||||||
const $downloadButton = document.querySelector(
|
|
||||||
".download-torrent"
|
|
||||||
) as HTMLAnchorElement;
|
|
||||||
|
|
||||||
if (!$downloadButton) return resolve(null);
|
|
||||||
|
|
||||||
worker.once("message", (torrent: Instance | null) => {
|
|
||||||
if (!torrent) return resolve(null);
|
|
||||||
|
|
||||||
resolve({
|
|
||||||
fileSize: formatBytes(torrent.length ?? 0),
|
|
||||||
magnet: toMagnetURI(torrent),
|
|
||||||
uploadDate: formatXatabDate($uploadDate!.textContent!),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const buffer = await getFileBuffer($downloadButton.href);
|
|
||||||
worker.postMessage(buffer);
|
|
||||||
})();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getNewRepacksFromXatab = async (
|
|
||||||
existingRepacks: Repack[] = [],
|
|
||||||
page = 1
|
|
||||||
): Promise<void> => {
|
|
||||||
const data = await requestWebPage(`https://byxatab.com/page/${page}`);
|
|
||||||
|
|
||||||
const { window } = new JSDOM(data);
|
|
||||||
|
|
||||||
const repacks: QueryDeepPartialEntity<Repack>[] = [];
|
|
||||||
|
|
||||||
if (page === 1) {
|
|
||||||
totalPages = Number(
|
|
||||||
window.document.querySelector(
|
|
||||||
"#bottom-nav > div.pagination > a:nth-child(12)"
|
|
||||||
)?.textContent
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const repacksFromPage = Array.from(
|
|
||||||
window.document.querySelectorAll(".entry__title a")
|
|
||||||
).map(($a) => {
|
|
||||||
return getXatabRepack(($a as HTMLAnchorElement).href)
|
|
||||||
.then((repack) => {
|
|
||||||
if (repack) {
|
|
||||||
repacks.push({
|
|
||||||
title: $a.textContent!,
|
|
||||||
repacker: "Xatab",
|
|
||||||
...repack,
|
|
||||||
page,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err: unknown) => {
|
|
||||||
logger.error((err as Error).message, {
|
|
||||||
method: "getNewRepacksFromXatab",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(repacksFromPage);
|
|
||||||
|
|
||||||
const newRepacks = repacks.filter(
|
|
||||||
(repack) =>
|
|
||||||
!existingRepacks.some(
|
|
||||||
(existingRepack) => existingRepack.title === repack.title
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!newRepacks.length) return;
|
|
||||||
|
|
||||||
await savePage(newRepacks);
|
|
||||||
|
|
||||||
if (page === totalPages) return;
|
|
||||||
|
|
||||||
return getNewRepacksFromXatab(existingRepacks, page + 1);
|
|
||||||
};
|
|
36
src/main/services/search-engine.ts
Normal file
36
src/main/services/search-engine.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import flexSearch from "flexsearch";
|
||||||
|
|
||||||
|
import { repackRepository } from "@main/repository";
|
||||||
|
import { formatName } from "@shared";
|
||||||
|
import type { GameRepack } from "@types";
|
||||||
|
|
||||||
|
export class SearchEngine {
|
||||||
|
public static repacks: GameRepack[] = [];
|
||||||
|
|
||||||
|
private static repacksIndex = new flexSearch.Index();
|
||||||
|
|
||||||
|
public static searchRepacks(query: string): GameRepack[] {
|
||||||
|
return this.repacksIndex
|
||||||
|
.search(formatName(query))
|
||||||
|
.map((index) => this.repacks[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async updateRepacks() {
|
||||||
|
this.repacks = [];
|
||||||
|
|
||||||
|
const repacks = await repackRepository.find({
|
||||||
|
order: {
|
||||||
|
createdAt: "desc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = 0; i < repacks.length; i++) {
|
||||||
|
const repack = repacks[i];
|
||||||
|
|
||||||
|
const formattedTitle = formatName(repack.title);
|
||||||
|
|
||||||
|
this.repacks = [...this.repacks, { ...repack, title: formattedTitle }];
|
||||||
|
this.repacksIndex.add(i, formattedTitle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,33 +0,0 @@
|
|||||||
import path from "node:path";
|
|
||||||
import { app } from "electron";
|
|
||||||
|
|
||||||
import { chunk } from "lodash-es";
|
|
||||||
|
|
||||||
import { createDataSource } from "@main/data-source";
|
|
||||||
import { Repack } from "@main/entity";
|
|
||||||
import { repackRepository } from "@main/repository";
|
|
||||||
|
|
||||||
export const resolveDatabaseUpdates = async () => {
|
|
||||||
const updateDataSource = createDataSource({
|
|
||||||
database: app.isPackaged
|
|
||||||
? path.join(process.resourcesPath, "hydra.db")
|
|
||||||
: path.join(__dirname, "..", "..", "hydra.db"),
|
|
||||||
});
|
|
||||||
|
|
||||||
return updateDataSource.initialize().then(async () => {
|
|
||||||
const updateRepackRepository = updateDataSource.getRepository(Repack);
|
|
||||||
|
|
||||||
const updateRepacks = await updateRepackRepository.find();
|
|
||||||
|
|
||||||
const updateRepacksChunks = chunk(updateRepacks, 800);
|
|
||||||
|
|
||||||
for (const chunk of updateRepacksChunks) {
|
|
||||||
await repackRepository
|
|
||||||
.createQueryBuilder()
|
|
||||||
.insert()
|
|
||||||
.values(chunk)
|
|
||||||
.orIgnore()
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
1
src/main/vite-env.d.ts
vendored
1
src/main/vite-env.d.ts
vendored
@ -1,4 +1,5 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
/// <reference types="vite-plugin-comlink/client" />
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly MAIN_VITE_STEAMGRIDDB_API_KEY: string;
|
readonly MAIN_VITE_STEAMGRIDDB_API_KEY: string;
|
||||||
|
13
src/main/workers/index.ts
Normal file
13
src/main/workers/index.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import steamGamesWorkerPath from "./steam-games.worker?modulePath";
|
||||||
|
|
||||||
|
import Piscina from "piscina";
|
||||||
|
|
||||||
|
import { seedsPath } from "@main/constants";
|
||||||
|
|
||||||
|
export const steamGamesWorker = new Piscina({
|
||||||
|
filename: steamGamesWorkerPath,
|
||||||
|
workerData: {
|
||||||
|
steamGamesPath: path.join(seedsPath, "steam-games.json"),
|
||||||
|
},
|
||||||
|
});
|
38
src/main/workers/steam-games.worker.ts
Normal file
38
src/main/workers/steam-games.worker.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { SteamGame } from "@types";
|
||||||
|
import { orderBy, slice } from "lodash-es";
|
||||||
|
import flexSearch from "flexsearch";
|
||||||
|
import fs from "node:fs";
|
||||||
|
|
||||||
|
import { formatName } from "@shared";
|
||||||
|
import { workerData } from "node:worker_threads";
|
||||||
|
|
||||||
|
const steamGamesIndex = new flexSearch.Index({
|
||||||
|
tokenize: "forward",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { steamGamesPath } = workerData;
|
||||||
|
|
||||||
|
const data = fs.readFileSync(steamGamesPath, "utf-8");
|
||||||
|
|
||||||
|
const steamGames = JSON.parse(data) as SteamGame[];
|
||||||
|
|
||||||
|
for (let i = 0; i < steamGames.length; i++) {
|
||||||
|
const steamGame = steamGames[i];
|
||||||
|
|
||||||
|
const formattedName = formatName(steamGame.name);
|
||||||
|
|
||||||
|
steamGamesIndex.add(i, formattedName);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const search = (options: flexSearch.SearchOptions) => {
|
||||||
|
const results = steamGamesIndex.search(options.query ?? "", options);
|
||||||
|
const games = results.map((index) => steamGames[index]);
|
||||||
|
|
||||||
|
return orderBy(games, ["name"], ["asc"]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getById = (id: number) =>
|
||||||
|
steamGames.find((game) => game.id === id);
|
||||||
|
|
||||||
|
export const list = ({ limit, offset }: { limit: number; offset: number }) =>
|
||||||
|
slice(steamGames, offset, offset + limit);
|
@ -1,14 +0,0 @@
|
|||||||
import { parentPort } from "worker_threads";
|
|
||||||
import parseTorrent from "parse-torrent";
|
|
||||||
|
|
||||||
const port = parentPort;
|
|
||||||
if (!port) throw new Error("IllegalState");
|
|
||||||
|
|
||||||
port.on("message", async (buffer: Buffer) => {
|
|
||||||
try {
|
|
||||||
const torrent = await parseTorrent(buffer);
|
|
||||||
port.postMessage(torrent);
|
|
||||||
} catch (err) {
|
|
||||||
port.postMessage(null);
|
|
||||||
}
|
|
||||||
});
|
|
@ -32,8 +32,7 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
|
|
||||||
/* Catalogue */
|
/* Catalogue */
|
||||||
searchGames: (query: string) => ipcRenderer.invoke("searchGames", query),
|
searchGames: (query: string) => ipcRenderer.invoke("searchGames", query),
|
||||||
getCatalogue: (category: CatalogueCategory) =>
|
getCatalogue: () => ipcRenderer.invoke("getCatalogue"),
|
||||||
ipcRenderer.invoke("getCatalogue", category),
|
|
||||||
getGameShopDetails: (objectID: string, shop: GameShop, language: string) =>
|
getGameShopDetails: (objectID: string, shop: GameShop, language: string) =>
|
||||||
ipcRenderer.invoke("getGameShopDetails", objectID, shop, language),
|
ipcRenderer.invoke("getGameShopDetails", objectID, shop, language),
|
||||||
getRandomGame: () => ipcRenderer.invoke("getRandomGame"),
|
getRandomGame: () => ipcRenderer.invoke("getRandomGame"),
|
||||||
@ -52,6 +51,15 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
authenticateRealDebrid: (apiToken: string) =>
|
authenticateRealDebrid: (apiToken: string) =>
|
||||||
ipcRenderer.invoke("authenticateRealDebrid", apiToken),
|
ipcRenderer.invoke("authenticateRealDebrid", apiToken),
|
||||||
|
|
||||||
|
/* Download sources */
|
||||||
|
getDownloadSources: () => ipcRenderer.invoke("getDownloadSources"),
|
||||||
|
validateDownloadSource: (url: string) =>
|
||||||
|
ipcRenderer.invoke("validateDownloadSource", url),
|
||||||
|
addDownloadSource: (url: string) =>
|
||||||
|
ipcRenderer.invoke("addDownloadSource", url),
|
||||||
|
removeDownloadSource: (id: number) =>
|
||||||
|
ipcRenderer.invoke("removeDownloadSource", id),
|
||||||
|
|
||||||
/* Library */
|
/* Library */
|
||||||
addGameToLibrary: (
|
addGameToLibrary: (
|
||||||
objectID: string,
|
objectID: string,
|
||||||
|
@ -23,6 +23,7 @@ export const heroMedia = style({
|
|||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
transition: "all ease 0.2s",
|
transition: "all ease 0.2s",
|
||||||
|
imageRendering: "revert",
|
||||||
selectors: {
|
selectors: {
|
||||||
[`${hero}:hover &`]: {
|
[`${hero}:hover &`]: {
|
||||||
transform: "scale(1.02)",
|
transform: "scale(1.02)",
|
||||||
|
@ -21,7 +21,7 @@ export const modal = recipe({
|
|||||||
animationName: fadeIn,
|
animationName: fadeIn,
|
||||||
animationDuration: "0.3s",
|
animationDuration: "0.3s",
|
||||||
backgroundColor: vars.color.background,
|
backgroundColor: vars.color.background,
|
||||||
borderRadius: "5px",
|
borderRadius: "4px",
|
||||||
maxWidth: "600px",
|
maxWidth: "600px",
|
||||||
color: vars.color.body,
|
color: vars.color.body,
|
||||||
maxHeight: "100%",
|
maxHeight: "100%",
|
||||||
|
@ -42,18 +42,33 @@ export const textField = recipe({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const textFieldInput = style({
|
export const textFieldInput = recipe({
|
||||||
backgroundColor: "transparent",
|
base: {
|
||||||
border: "none",
|
backgroundColor: "transparent",
|
||||||
width: "100%",
|
border: "none",
|
||||||
height: "100%",
|
width: "100%",
|
||||||
outline: "none",
|
height: "100%",
|
||||||
color: "#DADBE1",
|
outline: "none",
|
||||||
cursor: "default",
|
color: "#DADBE1",
|
||||||
fontFamily: "inherit",
|
cursor: "default",
|
||||||
textOverflow: "ellipsis",
|
fontFamily: "inherit",
|
||||||
padding: `${SPACING_UNIT}px`,
|
textOverflow: "ellipsis",
|
||||||
":focus": {
|
padding: `${SPACING_UNIT}px`,
|
||||||
cursor: "text",
|
":focus": {
|
||||||
|
cursor: "text",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
readOnly: {
|
||||||
|
true: {
|
||||||
|
textOverflow: "inherit",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const togglePasswordButton = style({
|
||||||
|
cursor: "pointer",
|
||||||
|
color: vars.color.muted,
|
||||||
|
padding: `${SPACING_UNIT}px`,
|
||||||
|
});
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { useId, useState } from "react";
|
import { useId, useMemo, useState } from "react";
|
||||||
import type { RecipeVariants } from "@vanilla-extract/recipes";
|
import type { RecipeVariants } from "@vanilla-extract/recipes";
|
||||||
import * as styles from "./text-field.css";
|
import * as styles from "./text-field.css";
|
||||||
|
import { EyeClosedIcon, EyeIcon } from "@primer/octicons-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export interface TextFieldProps
|
export interface TextFieldProps
|
||||||
extends React.DetailedHTMLProps<
|
extends React.DetailedHTMLProps<
|
||||||
@ -28,9 +30,20 @@ export function TextField({
|
|||||||
containerProps,
|
containerProps,
|
||||||
...props
|
...props
|
||||||
}: TextFieldProps) {
|
}: TextFieldProps) {
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
|
||||||
const id = useId();
|
const id = useId();
|
||||||
|
|
||||||
|
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
|
||||||
|
const { t } = useTranslation("forms");
|
||||||
|
|
||||||
|
const showPasswordToggleButton = props.type === "password";
|
||||||
|
|
||||||
|
const inputType = useMemo(() => {
|
||||||
|
if (props.type === "password" && isPasswordVisible) return "text";
|
||||||
|
return props.type ?? "text";
|
||||||
|
}, [props.type, isPasswordVisible]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.textFieldContainer} {...containerProps}>
|
<div className={styles.textFieldContainer} {...containerProps}>
|
||||||
{label && <label htmlFor={id}>{label}</label>}
|
{label && <label htmlFor={id}>{label}</label>}
|
||||||
@ -41,12 +54,27 @@ export function TextField({
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
id={id}
|
id={id}
|
||||||
type="text"
|
className={styles.textFieldInput({ readOnly: props.readOnly })}
|
||||||
className={styles.textFieldInput}
|
|
||||||
onFocus={() => setIsFocused(true)}
|
onFocus={() => setIsFocused(true)}
|
||||||
onBlur={() => setIsFocused(false)}
|
onBlur={() => setIsFocused(false)}
|
||||||
{...props}
|
{...props}
|
||||||
|
type={inputType}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{showPasswordToggleButton && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.togglePasswordButton}
|
||||||
|
onClick={() => setIsPasswordVisible(!isPasswordVisible)}
|
||||||
|
aria-label={t("toggle_password_visibility")}
|
||||||
|
>
|
||||||
|
{isPasswordVisible ? (
|
||||||
|
<EyeClosedIcon size={16} />
|
||||||
|
) : (
|
||||||
|
<EyeIcon size={16} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hint && <small>{hint}</small>}
|
{hint && <small>{hint}</small>}
|
||||||
|
13
src/renderer/src/declaration.d.ts
vendored
13
src/renderer/src/declaration.d.ts
vendored
@ -12,6 +12,7 @@ import type {
|
|||||||
UserPreferences,
|
UserPreferences,
|
||||||
StartGameDownloadPayload,
|
StartGameDownloadPayload,
|
||||||
RealDebridUser,
|
RealDebridUser,
|
||||||
|
DownloadSource,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { DiskSpace } from "check-disk-space";
|
import type { DiskSpace } from "check-disk-space";
|
||||||
|
|
||||||
@ -33,7 +34,7 @@ declare global {
|
|||||||
|
|
||||||
/* Catalogue */
|
/* Catalogue */
|
||||||
searchGames: (query: string) => Promise<CatalogueEntry[]>;
|
searchGames: (query: string) => Promise<CatalogueEntry[]>;
|
||||||
getCatalogue: (category: CatalogueCategory) => Promise<CatalogueEntry[]>;
|
getCatalogue: () => Promise<CatalogueEntry[]>;
|
||||||
getGameShopDetails: (
|
getGameShopDetails: (
|
||||||
objectID: string,
|
objectID: string,
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
@ -58,7 +59,7 @@ declare global {
|
|||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
executablePath: string | null
|
executablePath: string | null
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
getLibrary: () => Promise<Game[]>;
|
getLibrary: () => Promise<Omit<Game, "repacks">[]>;
|
||||||
openGameInstaller: (gameId: number) => Promise<boolean>;
|
openGameInstaller: (gameId: number) => Promise<boolean>;
|
||||||
openGame: (gameId: number, executablePath: string) => Promise<void>;
|
openGame: (gameId: number, executablePath: string) => Promise<void>;
|
||||||
closeGame: (gameId: number) => Promise<boolean>;
|
closeGame: (gameId: number) => Promise<boolean>;
|
||||||
@ -77,6 +78,14 @@ declare global {
|
|||||||
autoLaunch: (enabled: boolean) => Promise<void>;
|
autoLaunch: (enabled: boolean) => Promise<void>;
|
||||||
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
|
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
|
||||||
|
|
||||||
|
/* Download sources */
|
||||||
|
getDownloadSources: () => Promise<DownloadSource[]>;
|
||||||
|
validateDownloadSource: (
|
||||||
|
url: string
|
||||||
|
) => Promise<{ name: string; downloadCount: number }>;
|
||||||
|
addDownloadSource: (url: string) => Promise<DownloadSource>;
|
||||||
|
removeDownloadSource: (id: number) => Promise<void>;
|
||||||
|
|
||||||
/* Hardware */
|
/* Hardware */
|
||||||
getDiskFreeSpace: (path: string) => Promise<DiskSpace>;
|
getDiskFreeSpace: (path: string) => Promise<DiskSpace>;
|
||||||
|
|
||||||
|
@ -40,3 +40,7 @@ export const buildGameDetailsPath = (
|
|||||||
const searchParams = new URLSearchParams({ title: game.title, ...params });
|
const searchParams = new URLSearchParams({ title: game.title, ...params });
|
||||||
return `/game/${game.shop}/${game.objectID}?${searchParams.toString()}`;
|
return `/game/${game.shop}/${game.objectID}?${searchParams.toString()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const numberFormatter = new Intl.NumberFormat("en-US", {
|
||||||
|
maximumSignificantDigits: 3,
|
||||||
|
});
|
||||||
|
@ -99,12 +99,7 @@ export function Downloads() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (game.progress === 1) {
|
if (game.progress === 1) {
|
||||||
return (
|
return <p>{t("completed")}</p>;
|
||||||
<>
|
|
||||||
<p>{game.repack?.title}</p>
|
|
||||||
<p>{t("completed")}</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (game.status === "paused") {
|
if (game.status === "paused") {
|
||||||
|
@ -21,6 +21,7 @@ export interface GameDetailsContext {
|
|||||||
game: Game | null;
|
game: Game | null;
|
||||||
shopDetails: ShopDetails | null;
|
shopDetails: ShopDetails | null;
|
||||||
repacks: GameRepack[];
|
repacks: GameRepack[];
|
||||||
|
shop: GameShop;
|
||||||
gameTitle: string;
|
gameTitle: string;
|
||||||
isGameRunning: boolean;
|
isGameRunning: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
@ -35,6 +36,7 @@ export const gameDetailsContext = createContext<GameDetailsContext>({
|
|||||||
game: null,
|
game: null,
|
||||||
shopDetails: null,
|
shopDetails: null,
|
||||||
repacks: [],
|
repacks: [],
|
||||||
|
shop: "steam",
|
||||||
gameTitle: "",
|
gameTitle: "",
|
||||||
isGameRunning: false,
|
isGameRunning: false,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@ -92,7 +94,7 @@ export function GameDetailsContextProvider({
|
|||||||
}, [updateGame, isGameDownloading, lastPacket?.game.status]);
|
}, [updateGame, isGameDownloading, lastPacket?.game.status]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.all([
|
Promise.allSettled([
|
||||||
window.electron.getGameShopDetails(
|
window.electron.getGameShopDetails(
|
||||||
objectID!,
|
objectID!,
|
||||||
shop as GameShop,
|
shop as GameShop,
|
||||||
@ -100,9 +102,12 @@ export function GameDetailsContextProvider({
|
|||||||
),
|
),
|
||||||
window.electron.searchGameRepacks(gameTitle),
|
window.electron.searchGameRepacks(gameTitle),
|
||||||
])
|
])
|
||||||
.then(([appDetails, repacks]) => {
|
.then(([appDetailsResult, repacksResult]) => {
|
||||||
if (appDetails) setGameDetails(appDetails);
|
if (appDetailsResult.status === "fulfilled")
|
||||||
setRepacks(repacks);
|
setGameDetails(appDetailsResult.value);
|
||||||
|
|
||||||
|
if (repacksResult.status === "fulfilled")
|
||||||
|
setRepacks(repacksResult.value);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@ -174,6 +179,7 @@ export function GameDetailsContextProvider({
|
|||||||
value={{
|
value={{
|
||||||
game,
|
game,
|
||||||
shopDetails,
|
shopDetails,
|
||||||
|
shop: shop as GameShop,
|
||||||
repacks,
|
repacks,
|
||||||
gameTitle,
|
gameTitle,
|
||||||
isGameRunning,
|
isGameRunning,
|
||||||
|
@ -46,7 +46,9 @@ export function DODIInstallationGuide({
|
|||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p style={{ fontFamily: "Fira Sans", marginBottom: 8 }}>
|
<p
|
||||||
|
style={{ fontFamily: "Fira Sans", marginBottom: `${SPACING_UNIT}px` }}
|
||||||
|
>
|
||||||
<Trans i18nKey="dodi_installation_instruction" ns="game_details">
|
<Trans i18nKey="dodi_installation_instruction" ns="game_details">
|
||||||
<ArrowUpIcon size={16} />
|
<ArrowUpIcon size={16} />
|
||||||
</Trans>
|
</Trans>
|
||||||
|
@ -6,6 +6,7 @@ export const contentSidebar = style({
|
|||||||
borderLeft: `solid 1px ${vars.color.border};`,
|
borderLeft: `solid 1px ${vars.color.border};`,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
|
position: "relative",
|
||||||
"@media": {
|
"@media": {
|
||||||
"(min-width: 768px)": {
|
"(min-width: 768px)": {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
@ -86,6 +87,14 @@ export const howLongToBeatCategorySkeleton = style({
|
|||||||
height: "76px",
|
height: "76px",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const technicalDetailsContainer = style({
|
||||||
|
padding: `0 ${SPACING_UNIT * 2}px`,
|
||||||
|
color: vars.color.body,
|
||||||
|
userSelect: "text",
|
||||||
|
position: "absolute",
|
||||||
|
bottom: `${SPACING_UNIT}px`,
|
||||||
|
});
|
||||||
|
|
||||||
globalStyle(`${requirementsDetails} a`, {
|
globalStyle(`${requirementsDetails} a`, {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
color: vars.color.body,
|
color: vars.color.body,
|
||||||
|
@ -16,7 +16,8 @@ export function Sidebar() {
|
|||||||
const [activeRequirement, setActiveRequirement] =
|
const [activeRequirement, setActiveRequirement] =
|
||||||
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
|
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
|
||||||
|
|
||||||
const { gameTitle, shopDetails, objectID } = useContext(gameDetailsContext);
|
const { gameTitle, shopDetails, shop, objectID } =
|
||||||
|
useContext(gameDetailsContext);
|
||||||
|
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
@ -74,6 +75,15 @@ export function Sidebar() {
|
|||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className={styles.technicalDetailsContainer}>
|
||||||
|
<p>
|
||||||
|
<small>shop: "{shop}"</small>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<small>objectID: "{objectID}"</small>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
import { style } from "@vanilla-extract/css";
|
import { style } from "@vanilla-extract/css";
|
||||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||||
|
|
||||||
export const homeCategories = style({
|
|
||||||
display: "flex",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const homeHeader = style({
|
export const homeHeader = style({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
gap: `${SPACING_UNIT}px`,
|
gap: `${SPACING_UNIT}px`,
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const content = style({
|
export const content = style({
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
|
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
|
||||||
|
|
||||||
import { Button, GameCard, Hero } from "@renderer/components";
|
import { Button, GameCard, Hero } from "@renderer/components";
|
||||||
import {
|
import type { Steam250Game, CatalogueEntry } from "@types";
|
||||||
Steam250Game,
|
|
||||||
type CatalogueCategory,
|
|
||||||
type CatalogueEntry,
|
|
||||||
} from "@types";
|
|
||||||
|
|
||||||
import starsAnimation from "@renderer/assets/lottie/stars.json";
|
import starsAnimation from "@renderer/assets/lottie/stars.json";
|
||||||
|
|
||||||
@ -18,8 +14,6 @@ import { vars } from "../../theme.css";
|
|||||||
import Lottie from "lottie-react";
|
import Lottie from "lottie-react";
|
||||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
|
|
||||||
const categories: CatalogueCategory[] = ["trending", "recently_added"];
|
|
||||||
|
|
||||||
export function Home() {
|
export function Home() {
|
||||||
const { t } = useTranslation("home");
|
const { t } = useTranslation("home");
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -27,22 +21,15 @@ export function Home() {
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
||||||
|
|
||||||
const [searchParams] = useSearchParams();
|
const [catalogue, setCatalogue] = useState<CatalogueEntry[]>([]);
|
||||||
|
|
||||||
const [catalogue, setCatalogue] = useState<
|
const getCatalogue = useCallback(() => {
|
||||||
Record<CatalogueCategory, CatalogueEntry[]>
|
|
||||||
>({
|
|
||||||
trending: [],
|
|
||||||
recently_added: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const getCatalogue = useCallback((category: CatalogueCategory) => {
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
window.electron
|
window.electron
|
||||||
.getCatalogue(category)
|
.getCatalogue()
|
||||||
.then((catalogue) => {
|
.then((catalogue) => {
|
||||||
setCatalogue((prev) => ({ ...prev, [category]: catalogue }));
|
setCatalogue(catalogue);
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@ -50,15 +37,6 @@ export function Home() {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const currentCategory = searchParams.get("category") || categories[0];
|
|
||||||
|
|
||||||
const handleSelectCategory = (category: CatalogueCategory) => {
|
|
||||||
if (category !== currentCategory) {
|
|
||||||
getCatalogue(category);
|
|
||||||
navigate(`/?category=${category}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRandomGame = useCallback(() => {
|
const getRandomGame = useCallback(() => {
|
||||||
window.electron.getRandomGame().then((game) => {
|
window.electron.getRandomGame().then((game) => {
|
||||||
if (game) setRandomGame(game);
|
if (game) setRandomGame(game);
|
||||||
@ -80,9 +58,10 @@ export function Home() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
getCatalogue(currentCategory as CatalogueCategory);
|
getCatalogue();
|
||||||
|
|
||||||
getRandomGame();
|
getRandomGame();
|
||||||
}, [getCatalogue, currentCategory, getRandomGame]);
|
}, [getCatalogue, getRandomGame]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||||
@ -92,17 +71,7 @@ export function Home() {
|
|||||||
<Hero />
|
<Hero />
|
||||||
|
|
||||||
<section className={styles.homeHeader}>
|
<section className={styles.homeHeader}>
|
||||||
<div className={styles.homeCategories}>
|
<h2>{t("trending")}</h2>
|
||||||
{categories.map((category) => (
|
|
||||||
<Button
|
|
||||||
key={category}
|
|
||||||
theme={currentCategory === category ? "primary" : "outline"}
|
|
||||||
onClick={() => handleSelectCategory(category)}
|
|
||||||
>
|
|
||||||
{t(category)}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={handleRandomizerClick}
|
onClick={handleRandomizerClick}
|
||||||
@ -120,14 +89,12 @@ export function Home() {
|
|||||||
</Button>
|
</Button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<h2>{t(currentCategory)}</h2>
|
|
||||||
|
|
||||||
<section className={styles.cards}>
|
<section className={styles.cards}>
|
||||||
{isLoading
|
{isLoading
|
||||||
? Array.from({ length: 12 }).map((_, index) => (
|
? Array.from({ length: 12 }).map((_, index) => (
|
||||||
<Skeleton key={index} className={styles.cardSkeleton} />
|
<Skeleton key={index} className={styles.cardSkeleton} />
|
||||||
))
|
))
|
||||||
: catalogue[currentCategory as CatalogueCategory].map((result) => (
|
: catalogue.map((result) => (
|
||||||
<GameCard
|
<GameCard
|
||||||
key={result.objectID}
|
key={result.objectID}
|
||||||
game={result}
|
game={result}
|
||||||
|
112
src/renderer/src/pages/settings/add-download-source-modal.tsx
Normal file
112
src/renderer/src/pages/settings/add-download-source-modal.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import { Button, Modal, TextField } from "@renderer/components";
|
||||||
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import * as styles from "./settings-download-sources.css";
|
||||||
|
import { numberFormatter } from "@renderer/helpers";
|
||||||
|
|
||||||
|
interface AddDownloadSourceModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onAddDownloadSource: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AddDownloadSourceModal({
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
onAddDownloadSource,
|
||||||
|
}: AddDownloadSourceModalProps) {
|
||||||
|
const [value, setValue] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const [validationResult, setValidationResult] = useState<{
|
||||||
|
name: string;
|
||||||
|
downloadCount: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
|
|
||||||
|
const handleValidateDownloadSource = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.electron.validateDownloadSource(value);
|
||||||
|
setValidationResult(result);
|
||||||
|
console.log(result);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddDownloadSource = async () => {
|
||||||
|
await window.electron.addDownloadSource(value);
|
||||||
|
onClose();
|
||||||
|
onAddDownloadSource();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
title={t("add_download_source")}
|
||||||
|
description={t("add_download_source_description")}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
minWidth: "500px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={styles.downloadSourceField}>
|
||||||
|
<TextField
|
||||||
|
label={t("download_source_url")}
|
||||||
|
placeholder="Insert a valid JSON url"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
style={{ alignSelf: "flex-end" }}
|
||||||
|
onClick={handleValidateDownloadSource}
|
||||||
|
disabled={isLoading || !value}
|
||||||
|
>
|
||||||
|
{t("validate_download_source")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{validationResult && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: `${SPACING_UNIT * 3}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT / 2}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h4>{validationResult?.name}</h4>
|
||||||
|
<small>
|
||||||
|
Found {numberFormatter.format(validationResult?.downloadCount)}{" "}
|
||||||
|
download options
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button type="button" onClick={handleAddDownloadSource}>
|
||||||
|
Import
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
import { style } from "@vanilla-extract/css";
|
||||||
|
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||||
|
|
||||||
|
export const downloadSourceField = style({
|
||||||
|
display: "flex",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const downloadSourceItem = style({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
backgroundColor: vars.color.darkBackground,
|
||||||
|
borderRadius: "8px",
|
||||||
|
padding: `${SPACING_UNIT * 2}px`,
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
border: `solid 1px ${vars.color.border}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const downloadSourceItemHeader = style({
|
||||||
|
marginBottom: `${SPACING_UNIT}px`,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT / 2}px`,
|
||||||
|
});
|
102
src/renderer/src/pages/settings/settings-download-sources.tsx
Normal file
102
src/renderer/src/pages/settings/settings-download-sources.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { TextField, Button } from "@renderer/components";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import * as styles from "./settings-download-sources.css";
|
||||||
|
import type { DownloadSource } from "@types";
|
||||||
|
import { NoEntryIcon, PlusCircleIcon } from "@primer/octicons-react";
|
||||||
|
import { AddDownloadSourceModal } from "./add-download-source-modal";
|
||||||
|
import { useToast } from "@renderer/hooks";
|
||||||
|
import { numberFormatter } from "@renderer/helpers";
|
||||||
|
|
||||||
|
export function SettingsDownloadSources() {
|
||||||
|
const [showAddDownloadSourceModal, setShowAddDownloadSourceModal] =
|
||||||
|
useState(false);
|
||||||
|
const [downloadSources, setDownloadSources] = useState<DownloadSource[]>([]);
|
||||||
|
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
|
|
||||||
|
const { showSuccessToast } = useToast();
|
||||||
|
|
||||||
|
const getDownloadSources = async () => {
|
||||||
|
return window.electron.getDownloadSources().then((sources) => {
|
||||||
|
setDownloadSources(sources);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getDownloadSources();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleRemoveSource = async (id: number) => {
|
||||||
|
await window.electron.removeDownloadSource(id);
|
||||||
|
showSuccessToast("Removed download source");
|
||||||
|
|
||||||
|
getDownloadSources();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddDownloadSource = async () => {
|
||||||
|
await getDownloadSources();
|
||||||
|
showSuccessToast("Download source successfully added");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AddDownloadSourceModal
|
||||||
|
visible={showAddDownloadSourceModal}
|
||||||
|
onClose={() => setShowAddDownloadSourceModal(false)}
|
||||||
|
onAddDownloadSource={handleAddDownloadSource}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p style={{ fontFamily: '"Fira Sans"' }}>
|
||||||
|
{t("download_sources_description")}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{downloadSources.map((downloadSource) => (
|
||||||
|
<div key={downloadSource.id} className={styles.downloadSourceItem}>
|
||||||
|
<div className={styles.downloadSourceItemHeader}>
|
||||||
|
<h3>{downloadSource.name}</h3>
|
||||||
|
<small>
|
||||||
|
{t("download_options", {
|
||||||
|
count: downloadSource.repackCount,
|
||||||
|
countFormatted: numberFormatter.format(
|
||||||
|
downloadSource.repackCount
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.downloadSourceField}>
|
||||||
|
<TextField
|
||||||
|
label={t("download_source_url")}
|
||||||
|
value={downloadSource.url}
|
||||||
|
readOnly
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
theme="outline"
|
||||||
|
style={{ alignSelf: "flex-end" }}
|
||||||
|
onClick={() => handleRemoveSource(downloadSource.id)}
|
||||||
|
>
|
||||||
|
<NoEntryIcon />
|
||||||
|
{t("remove_download_source")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
theme="outline"
|
||||||
|
style={{ alignSelf: "flex-start" }}
|
||||||
|
onClick={() => setShowAddDownloadSourceModal(true)}
|
||||||
|
>
|
||||||
|
<PlusCircleIcon />
|
||||||
|
{t("add_download_source")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -9,13 +9,19 @@ import { SettingsGeneral } from "./settings-general";
|
|||||||
import { SettingsBehavior } from "./settings-behavior";
|
import { SettingsBehavior } from "./settings-behavior";
|
||||||
import { useAppDispatch } from "@renderer/hooks";
|
import { useAppDispatch } from "@renderer/hooks";
|
||||||
import { setUserPreferences } from "@renderer/features";
|
import { setUserPreferences } from "@renderer/features";
|
||||||
|
import { SettingsDownloadSources } from "./settings-download-sources";
|
||||||
|
|
||||||
export function Settings() {
|
export function Settings() {
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const categories = [t("general"), t("behavior"), "Real-Debrid"];
|
const categories = [
|
||||||
|
t("general"),
|
||||||
|
t("behavior"),
|
||||||
|
t("download_sources"),
|
||||||
|
"Real-Debrid",
|
||||||
|
];
|
||||||
|
|
||||||
const [currentCategoryIndex, setCurrentCategoryIndex] = useState(0);
|
const [currentCategoryIndex, setCurrentCategoryIndex] = useState(0);
|
||||||
|
|
||||||
@ -41,6 +47,10 @@ export function Settings() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentCategoryIndex === 2) {
|
||||||
|
return <SettingsDownloadSources />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsRealDebrid updateUserPreferences={handleUpdateUserPreferences} />
|
<SettingsRealDebrid updateUserPreferences={handleUpdateUserPreferences} />
|
||||||
);
|
);
|
||||||
|
@ -6,10 +6,10 @@ interface BinaryNotFoundModalProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BinaryNotFoundModal = ({
|
export function BinaryNotFoundModal({
|
||||||
visible,
|
visible,
|
||||||
onClose,
|
onClose,
|
||||||
}: BinaryNotFoundModalProps) => {
|
}: BinaryNotFoundModalProps) {
|
||||||
const { t } = useTranslation("binary_not_found_modal");
|
const { t } = useTranslation("binary_not_found_modal");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -22,4 +22,4 @@ export const BinaryNotFoundModal = ({
|
|||||||
{t("instructions")}
|
{t("instructions")}
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
@ -18,3 +18,31 @@ export const formatBytes = (bytes: number): string => {
|
|||||||
|
|
||||||
return `${Math.trunc(formatedByte * 10) / 10} ${FORMAT[base]}`;
|
return `${Math.trunc(formatedByte * 10) / 10} ${FORMAT[base]}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const pipe =
|
||||||
|
<T>(...fns: ((arg: T) => any)[]) =>
|
||||||
|
(arg: T) =>
|
||||||
|
fns.reduce((prev, fn) => fn(prev), arg);
|
||||||
|
|
||||||
|
export const removeReleaseYearFromName = (name: string) =>
|
||||||
|
name.replace(/\([0-9]{4}\)/g, "");
|
||||||
|
|
||||||
|
export const removeSymbolsFromName = (name: string) =>
|
||||||
|
name.replace(/[^A-Za-z 0-9]/g, "");
|
||||||
|
|
||||||
|
export const removeSpecialEditionFromName = (name: string) =>
|
||||||
|
name.replace(
|
||||||
|
/(The |Digital )?(GOTY|Deluxe|Standard|Ultimate|Definitive|Enhanced|Collector's|Premium|Digital|Limited|Game of the Year|Reloaded|[0-9]{4}) Edition/g,
|
||||||
|
""
|
||||||
|
);
|
||||||
|
|
||||||
|
export const removeDuplicateSpaces = (name: string) =>
|
||||||
|
name.replace(/\s{2,}/g, " ");
|
||||||
|
|
||||||
|
export const formatName = pipe<string>(
|
||||||
|
removeReleaseYearFromName,
|
||||||
|
removeSymbolsFromName,
|
||||||
|
removeSpecialEditionFromName,
|
||||||
|
removeDuplicateSpaces,
|
||||||
|
(str) => str.trim()
|
||||||
|
);
|
||||||
|
@ -3,7 +3,6 @@ import type { Downloader } from "@shared";
|
|||||||
import { ProgressInfo, UpdateInfo } from "electron-updater";
|
import { ProgressInfo, UpdateInfo } from "electron-updater";
|
||||||
|
|
||||||
export type GameShop = "steam" | "epic";
|
export type GameShop = "steam" | "epic";
|
||||||
export type CatalogueCategory = "recently_added" | "trending";
|
|
||||||
|
|
||||||
export interface SteamGenre {
|
export interface SteamGenre {
|
||||||
id: string;
|
id: string;
|
||||||
@ -61,7 +60,6 @@ export interface GameRepack {
|
|||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
magnet: string;
|
magnet: string;
|
||||||
page: number;
|
|
||||||
repacker: string;
|
repacker: string;
|
||||||
fileSize: string | null;
|
fileSize: string | null;
|
||||||
uploadDate: Date | string | null;
|
uploadDate: Date | string | null;
|
||||||
@ -97,7 +95,6 @@ export interface Game extends Omit<CatalogueEntry, "cover"> {
|
|||||||
folderName: string;
|
folderName: string;
|
||||||
downloadPath: string | null;
|
downloadPath: string | null;
|
||||||
repacks: GameRepack[];
|
repacks: GameRepack[];
|
||||||
repack: GameRepack | null;
|
|
||||||
progress: number;
|
progress: number;
|
||||||
bytesDownloaded: number;
|
bytesDownloaded: number;
|
||||||
playTimeInMilliseconds: number;
|
playTimeInMilliseconds: number;
|
||||||
@ -233,3 +230,12 @@ export interface RealDebridUser {
|
|||||||
export type AppUpdaterEvents =
|
export type AppUpdaterEvents =
|
||||||
| { type: "update-available"; info: Partial<UpdateInfo> }
|
| { type: "update-available"; info: Partial<UpdateInfo> }
|
||||||
| { type: "update-downloaded" };
|
| { type: "update-downloaded" };
|
||||||
|
|
||||||
|
export interface DownloadSource {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
repackCount: number;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
53
yarn.lock
53
yarn.lock
@ -1309,10 +1309,10 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types "~5.26.4"
|
undici-types "~5.26.4"
|
||||||
|
|
||||||
"@types/node@^18.11.18":
|
"@types/node@^20.9.0":
|
||||||
version "18.19.33"
|
version "20.13.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.33.tgz#98cd286a1b8a5e11aa06623210240bcc28e95c48"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.13.0.tgz#011a76bc1e71ae9a026dddcfd7039084f752c4b6"
|
||||||
integrity sha512-NR9+KrpSajr2qBVp/Yt5TU/rp+b5Mayi3+OlMlcg2cVCfRmcG5PWZ7S4+MG9PZ5gWBoc9Pd0BKSRViuBCRPu0A==
|
integrity sha512-FM6AOb3khNkNIXPnHFDYaHerSv8uN22C91z098AnGccVu+Pcdhi+pNUFDi0iLmPIsVE0JBD0KVS7mzUYt4nRzQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types "~5.26.4"
|
undici-types "~5.26.4"
|
||||||
|
|
||||||
@ -1382,6 +1382,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43"
|
resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43"
|
||||||
integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==
|
integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==
|
||||||
|
|
||||||
|
"@types/user-agents@^1.0.4":
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/user-agents/-/user-agents-1.0.4.tgz#49bf6760d9ca3858d91e6258835bf2c329b87cf0"
|
||||||
|
integrity sha512-AjeFc4oX5WPPflgKfRWWJfkEk7Wu82fnj1rROPsiqFt6yElpdGFg8Srtm/4PU4rA9UiDUZlruGPgcwTMQlwq4w==
|
||||||
|
|
||||||
"@types/verror@^1.10.3":
|
"@types/verror@^1.10.3":
|
||||||
version "1.10.10"
|
version "1.10.10"
|
||||||
resolved "https://registry.yarnpkg.com/@types/verror/-/verror-1.10.10.tgz#d5a4b56abac169bfbc8b23d291363a682e6fa087"
|
resolved "https://registry.yarnpkg.com/@types/verror/-/verror-1.10.10.tgz#d5a4b56abac169bfbc8b23d291363a682e6fa087"
|
||||||
@ -2719,13 +2724,13 @@ electron-vite@^2.0.0:
|
|||||||
magic-string "^0.30.5"
|
magic-string "^0.30.5"
|
||||||
picocolors "^1.0.0"
|
picocolors "^1.0.0"
|
||||||
|
|
||||||
electron@^28.2.0:
|
electron@^30.0.9:
|
||||||
version "28.3.1"
|
version "30.0.9"
|
||||||
resolved "https://registry.yarnpkg.com/electron/-/electron-28.3.1.tgz#babb3ff8e246336e9cd1c1966f16a55ba723ea06"
|
resolved "https://registry.yarnpkg.com/electron/-/electron-30.0.9.tgz#b11400e4642a4b635e79244ba365f1d401ee60b5"
|
||||||
integrity sha512-aF9fONuhVDJlctJS7YOw76ynxVAQdfIWmlhRMKits24tDcdSL0eMHUS0wWYiRfGWbQnUKB6V49Rf17o32f4/fg==
|
integrity sha512-ArxgdGHVu3o5uaP+Tqj8cJDvU03R6vrGrOqiMs7JXLnvQHMqXJIIxmFKQAIdJW8VoT3ac3hD21tA7cPO10RLow==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@electron/get" "^2.0.0"
|
"@electron/get" "^2.0.0"
|
||||||
"@types/node" "^18.11.18"
|
"@types/node" "^20.9.0"
|
||||||
extract-zip "^2.0.1"
|
extract-zip "^2.0.1"
|
||||||
|
|
||||||
emoji-regex@^8.0.0:
|
emoji-regex@^8.0.0:
|
||||||
@ -4567,6 +4572,14 @@ natural-compare@^1.4.0:
|
|||||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||||
|
|
||||||
|
nice-napi@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/nice-napi/-/nice-napi-1.0.2.tgz#dc0ab5a1eac20ce548802fc5686eaa6bc654927b"
|
||||||
|
integrity sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==
|
||||||
|
dependencies:
|
||||||
|
node-addon-api "^3.0.0"
|
||||||
|
node-gyp-build "^4.2.2"
|
||||||
|
|
||||||
no-case@^3.0.4:
|
no-case@^3.0.4:
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d"
|
resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d"
|
||||||
@ -4587,6 +4600,11 @@ node-addon-api@^1.6.3:
|
|||||||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.7.2.tgz#3df30b95720b53c24e59948b49532b662444f54d"
|
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.7.2.tgz#3df30b95720b53c24e59948b49532b662444f54d"
|
||||||
integrity sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==
|
integrity sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==
|
||||||
|
|
||||||
|
node-addon-api@^3.0.0:
|
||||||
|
version "3.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161"
|
||||||
|
integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==
|
||||||
|
|
||||||
node-domexception@^1.0.0:
|
node-domexception@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
|
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
|
||||||
@ -4608,6 +4626,11 @@ node-fetch@^3.3.0:
|
|||||||
fetch-blob "^3.1.4"
|
fetch-blob "^3.1.4"
|
||||||
formdata-polyfill "^4.0.10"
|
formdata-polyfill "^4.0.10"
|
||||||
|
|
||||||
|
node-gyp-build@^4.2.2:
|
||||||
|
version "4.8.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.1.tgz#976d3ad905e71b76086f4f0b0d3637fe79b6cda5"
|
||||||
|
integrity sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==
|
||||||
|
|
||||||
node-releases@^2.0.14:
|
node-releases@^2.0.14:
|
||||||
version "2.0.14"
|
version "2.0.14"
|
||||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b"
|
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b"
|
||||||
@ -4867,6 +4890,13 @@ picomatch@^2.3.1:
|
|||||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||||
|
|
||||||
|
piscina@^4.5.1:
|
||||||
|
version "4.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/piscina/-/piscina-4.5.1.tgz#103f4237ce88c3f9e8b63c278673bcbddc10a9a2"
|
||||||
|
integrity sha512-DVhySLPfqAW+uRH9dF0bjA2xEWr5ANLAzkYXx5adSLMFnwssSIVJYhg0FlvgYsnT/khILQJ3WkjqbAlBvt+maw==
|
||||||
|
optionalDependencies:
|
||||||
|
nice-napi "^1.0.2"
|
||||||
|
|
||||||
pkg-types@^1.1.0:
|
pkg-types@^1.1.0:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.1.1.tgz#07b626880749beb607b0c817af63aac1845a73f2"
|
resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.1.1.tgz#07b626880749beb607b0c817af63aac1845a73f2"
|
||||||
@ -6269,3 +6299,8 @@ yocto-queue@^1.0.0:
|
|||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251"
|
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251"
|
||||||
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
|
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
|
||||||
|
|
||||||
|
zod@^3.23.8:
|
||||||
|
version "3.23.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
|
||||||
|
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
|
||||||
|
Loading…
Reference in New Issue
Block a user