mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-12 11:12:07 +00:00
Merge branch 'feat/migration-to-leveldb' into feature/torbox-integration
# Conflicts: # src/locales/en/translation.json # src/locales/pt-BR/translation.json # src/main/entity/user-preferences.entity.ts # src/main/events/auth/sign-out.ts # src/main/knex-client.ts # src/main/main.ts # src/main/services/download/download-manager.ts # src/main/services/process-watcher.ts # src/renderer/src/pages/downloads/download-group.tsx # src/types/index.ts # src/types/torbox.types.ts
This commit is contained in:
commit
3d571edccb
161 changed files with 2590 additions and 2802 deletions
|
@ -125,6 +125,10 @@ cd hydra
|
||||||
yarn
|
yarn
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### <a name="install-openssl-11"></a> Instale OpenSSL 1.1
|
||||||
|
|
||||||
|
[OpenSSL 1.1](https://slproweb.com/download/Win64OpenSSL-1_1_1w.exe) é exigido pelo libtorrent em ambientes Windows.
|
||||||
|
|
||||||
### <a name="install-python-39"></a> Instale Python 3.9
|
### <a name="install-python-39"></a> Instale Python 3.9
|
||||||
|
|
||||||
Certifique-se de ter o Python 3.9 instalado em sua máquina. Você pode baixá-lo e instalá-lo em [python.org](https://www.python.org/downloads/release/python-3913/).
|
Certifique-se de ter o Python 3.9 instalado em sua máquina. Você pode baixá-lo e instalá-lo em [python.org](https://www.python.org/downloads/release/python-3913/).
|
||||||
|
|
|
@ -38,6 +38,13 @@ export default defineConfig(({ mode }) => {
|
||||||
build: {
|
build: {
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
},
|
},
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
scss: {
|
||||||
|
api: "modern",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@renderer": resolve("src/renderer/src"),
|
"@renderer": resolve("src/renderer/src"),
|
||||||
|
|
|
@ -47,6 +47,7 @@
|
||||||
"auto-launch": "^5.0.6",
|
"auto-launch": "^5.0.6",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"better-sqlite3": "^11.7.0",
|
"better-sqlite3": "^11.7.0",
|
||||||
|
"classic-level": "^2.0.0",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"color": "^4.2.3",
|
"color": "^4.2.3",
|
||||||
"color.js": "^1.2.0",
|
"color.js": "^1.2.0",
|
||||||
|
@ -74,7 +75,6 @@
|
||||||
"sound-play": "^1.1.0",
|
"sound-play": "^1.1.0",
|
||||||
"sudo-prompt": "^9.2.1",
|
"sudo-prompt": "^9.2.1",
|
||||||
"tar": "^7.4.3",
|
"tar": "^7.4.3",
|
||||||
"typeorm": "^0.3.20",
|
|
||||||
"user-agents": "^1.1.387",
|
"user-agents": "^1.1.387",
|
||||||
"yaml": "^2.6.1",
|
"yaml": "^2.6.1",
|
||||||
"yup": "^1.5.0",
|
"yup": "^1.5.0",
|
||||||
|
|
|
@ -94,7 +94,7 @@ def seed_status():
|
||||||
|
|
||||||
@app.route("/healthcheck", methods=["GET"])
|
@app.route("/healthcheck", methods=["GET"])
|
||||||
def healthcheck():
|
def healthcheck():
|
||||||
return "", 200
|
return "ok", 200
|
||||||
|
|
||||||
@app.route("/process-list", methods=["GET"])
|
@app.route("/process-list", methods=["GET"])
|
||||||
def process_list():
|
def process_list():
|
||||||
|
|
|
@ -49,14 +49,14 @@ fs.readdir(dist, async (err, files) => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
if (uploads.length > 0) {
|
for (const upload of uploads) {
|
||||||
await fetch(process.env.BUILD_WEBHOOK_URL, {
|
await fetch(process.env.BUILD_WEBHOOK_URL, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
uploads,
|
upload,
|
||||||
branchName: process.env.BRANCH_NAME,
|
branchName: process.env.BRANCH_NAME,
|
||||||
version: packageJson.version,
|
version: packageJson.version,
|
||||||
githubActor: process.env.GITHUB_ACTOR,
|
githubActor: process.env.GITHUB_ACTOR,
|
||||||
|
|
|
@ -281,7 +281,23 @@
|
||||||
"disable_nsfw_alert": "Disable NSFW alert",
|
"disable_nsfw_alert": "Disable NSFW alert",
|
||||||
"seed_after_download_complete": "Seed after download complete",
|
"seed_after_download_complete": "Seed after download complete",
|
||||||
"show_hidden_achievement_description": "Show hidden achievements description before unlocking them",
|
"show_hidden_achievement_description": "Show hidden achievements description before unlocking them",
|
||||||
"debrid_services": "Debrid Services"
|
"debrid_services": "Debrid Services",
|
||||||
|
"account": "Account",
|
||||||
|
"no_users_blocked": "You have no blocked users",
|
||||||
|
"subscription_active_until": "Your Hydra Cloud is active until {{date}}",
|
||||||
|
"manage_subscription": "Manage subscription",
|
||||||
|
"update_email": "Update email",
|
||||||
|
"update_password": "Update password",
|
||||||
|
"current_email": "Current email:",
|
||||||
|
"no_email_account": "You have not set an email yet",
|
||||||
|
"account_data_updated_successfully": "Account data updated successfully",
|
||||||
|
"renew_subscription": "Renew Hydra Cloud",
|
||||||
|
"subscription_expired_at": "Your subscription expired at {{date}}",
|
||||||
|
"no_subscription": "Enjoy Hydra in the best possible way",
|
||||||
|
"become_subscriber": "Be Hydra Cloud",
|
||||||
|
"subscription_renew_cancelled": "Automatic renewal is disabled",
|
||||||
|
"subscription_renews_on": "Your subscription renews on {{date}}",
|
||||||
|
"bill_sent_until": "Your next bill will be sent until this day"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download complete",
|
"download_complete": "Download complete",
|
||||||
|
|
|
@ -172,7 +172,8 @@
|
||||||
"reset_achievements_description": "Isso irá resetar todas as conquistas de {{game}}",
|
"reset_achievements_description": "Isso irá resetar todas as conquistas de {{game}}",
|
||||||
"reset_achievements_title": "Tem certeza?",
|
"reset_achievements_title": "Tem certeza?",
|
||||||
"reset_achievements_success": "Conquistas resetadas com sucesso",
|
"reset_achievements_success": "Conquistas resetadas com sucesso",
|
||||||
"reset_achievements_error": "Falha ao resetar conquistas"
|
"reset_achievements_error": "Falha ao resetar conquistas",
|
||||||
|
"no_write_permission": "Não é possível baixar nesse diretório. Clique aqui para saber mais."
|
||||||
},
|
},
|
||||||
"activation": {
|
"activation": {
|
||||||
"title": "Ativação",
|
"title": "Ativação",
|
||||||
|
@ -269,7 +270,23 @@
|
||||||
"disable_nsfw_alert": "Desativar alerta de conteúdo inapropriado",
|
"disable_nsfw_alert": "Desativar alerta de conteúdo inapropriado",
|
||||||
"seed_after_download_complete": "Semear após a conclusão do download",
|
"seed_after_download_complete": "Semear após a conclusão do download",
|
||||||
"show_hidden_achievement_description": "Mostrar descrição de conquistas ocultas antes de debloqueá-las",
|
"show_hidden_achievement_description": "Mostrar descrição de conquistas ocultas antes de debloqueá-las",
|
||||||
"debrid_services": "Serviços Debrid"
|
"debrid_services": "Serviços Debrid",
|
||||||
|
"account": "Conta",
|
||||||
|
"no_users_blocked": "Você não bloqueou nenhum usuário",
|
||||||
|
"subscription_active_until": "Sua assinatura Hydra Cloud ficará ativa até {{date}}",
|
||||||
|
"manage_subscription": "Gerenciar assinatura",
|
||||||
|
"update_email": "Atualizar email",
|
||||||
|
"update_password": "Atualizar senha",
|
||||||
|
"current_email": "Email atual:",
|
||||||
|
"no_email_account": "Você ainda não adicionou um email a sua conta",
|
||||||
|
"account_data_updated_successfully": "Dados da conta atualizados com sucesso",
|
||||||
|
"renew_subscription": "Renovar Hydra Cloud",
|
||||||
|
"subscription_expired_at": "Sua assinatura expirou em {{date}}",
|
||||||
|
"no_subscription": "Aproveite o Hydra da melhor forma possível",
|
||||||
|
"become_subscriber": "Seja Hydra Cloud",
|
||||||
|
"subscription_renew_cancelled": "A renovação automática está desativada",
|
||||||
|
"subscription_renews_on": "Sua assinatura renova dia {{date}}",
|
||||||
|
"bill_sent_until": "Sua próxima cobrança será enviada até esse dia"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download concluído",
|
"download_complete": "Download concluído",
|
||||||
|
@ -398,7 +415,7 @@
|
||||||
"new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos",
|
"new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos",
|
||||||
"achievement_progress": "{{unlockedCount}}/{{totalCount}} conquistas",
|
"achievement_progress": "{{unlockedCount}}/{{totalCount}} conquistas",
|
||||||
"achievements_unlocked_for_game": "Desbloqueadas {{achievementCount}} novas conquistas em {{gameTitle}}",
|
"achievements_unlocked_for_game": "Desbloqueadas {{achievementCount}} novas conquistas em {{gameTitle}}",
|
||||||
"hidden_achievement_tooltip": "Está é uma conquista oculta",
|
"hidden_achievement_tooltip": "Esta é uma conquista oculta",
|
||||||
"achievement_earn_points": "Ganhe {{points}} pontos com essa conquista",
|
"achievement_earn_points": "Ganhe {{points}} pontos com essa conquista",
|
||||||
"earned_points": "Pontos ganhos:",
|
"earned_points": "Pontos ganhos:",
|
||||||
"available_points": "Pontos disponíveis:",
|
"available_points": "Pontos disponíveis:",
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
"featured": "Рекомендации",
|
"featured": "Рекомендации",
|
||||||
"surprise_me": "Удиви меня",
|
"surprise_me": "Удиви меня",
|
||||||
"no_results": "Ничего не найдено",
|
"no_results": "Ничего не найдено",
|
||||||
"hot": "Сейчас в топе",
|
"hot": "Сейчас популярно",
|
||||||
"start_typing": "Начинаю вводить текст для поиска...",
|
"start_typing": "Начинаю вводить текст...",
|
||||||
"weekly": "📅 Лучшие игры недели",
|
"weekly": "📅 Лучшие игры недели",
|
||||||
"achievements": "🏆 Игры, в которых нужно победить"
|
"achievements": "🏆 Игры, в которых нужно победить"
|
||||||
},
|
},
|
||||||
|
@ -278,7 +278,23 @@
|
||||||
"source_already_exists": "Этот источник уже добавлен",
|
"source_already_exists": "Этот источник уже добавлен",
|
||||||
"user_unblocked": "Пользователь разблокирован",
|
"user_unblocked": "Пользователь разблокирован",
|
||||||
"seed_after_download_complete": "Раздавать после завершения загрузки",
|
"seed_after_download_complete": "Раздавать после завершения загрузки",
|
||||||
"show_hidden_achievement_description": "Показывать описание скрытых достижений перед их получением"
|
"show_hidden_achievement_description": "Показывать описание скрытых достижений перед их получением",
|
||||||
|
"account": "Аккаунт",
|
||||||
|
"no_users_blocked": "У вас нет заблокированных пользователей",
|
||||||
|
"subscription_active_until": "Ваша подписка на Hydra Cloud активна до {{date}}",
|
||||||
|
"manage_subscription": "Управлять подпиской",
|
||||||
|
"update_email": "Обновить электронную почту",
|
||||||
|
"update_password": "Обновить пароль",
|
||||||
|
"current_email": "Текущий email:",
|
||||||
|
"no_email_account": "Вы еще не установили электронную почту",
|
||||||
|
"account_data_updated_successfully": "Данные учетной записи успешно обновлены",
|
||||||
|
"renew_subscription": "Обновить подписку Hydra Cloud",
|
||||||
|
"subscription_expired_at": "Срок действия вашей подписки истек в {{date}}",
|
||||||
|
"no_subscription": "Наслаждайтесь Hydra по максимуму",
|
||||||
|
"become_subscriber": "Станьте обладателем Hydra Cloud",
|
||||||
|
"subscription_renew_cancelled": "Автоматическое продление отключено",
|
||||||
|
"subscription_renews_on": "Ваша подписка продлевается на {{date}}",
|
||||||
|
"bill_sent_until": "Ваш следующий счет будет отправлен до этого дня"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Загрузка завершена",
|
"download_complete": "Загрузка завершена",
|
||||||
|
@ -408,7 +424,7 @@
|
||||||
"subscribe_now": "Подпишитесь прямо сейчас",
|
"subscribe_now": "Подпишитесь прямо сейчас",
|
||||||
"cloud_saving": "Сохранение в облаке",
|
"cloud_saving": "Сохранение в облаке",
|
||||||
"cloud_achievements": "Сохраняйте свои достижения в облаке",
|
"cloud_achievements": "Сохраняйте свои достижения в облаке",
|
||||||
"animated_profile_picture": "Анимированные фотографии профиля",
|
"animated_profile_picture": "Анимированные аватарки",
|
||||||
"premium_support": "Премиальная поддержка",
|
"premium_support": "Премиальная поддержка",
|
||||||
"show_and_compare_achievements": "Показывайте и сравнивайте свои достижения с достижениями других пользователей",
|
"show_and_compare_achievements": "Показывайте и сравнивайте свои достижения с достижениями других пользователей",
|
||||||
"animated_profile_banner": "Анимированный баннер профиля",
|
"animated_profile_banner": "Анимированный баннер профиля",
|
||||||
|
|
|
@ -7,13 +7,18 @@ export const defaultDownloadsPath = app.getPath("downloads");
|
||||||
|
|
||||||
export const isStaging = import.meta.env.MAIN_VITE_API_URL.includes("staging");
|
export const isStaging = import.meta.env.MAIN_VITE_API_URL.includes("staging");
|
||||||
|
|
||||||
|
export const levelDatabasePath = path.join(
|
||||||
|
app.getPath("userData"),
|
||||||
|
`hydra-db${isStaging ? "-staging" : ""}`
|
||||||
|
);
|
||||||
|
|
||||||
export const databaseDirectory = path.join(app.getPath("appData"), "hydra");
|
export const databaseDirectory = path.join(app.getPath("appData"), "hydra");
|
||||||
export const databasePath = path.join(
|
export const databasePath = path.join(
|
||||||
databaseDirectory,
|
databaseDirectory,
|
||||||
isStaging ? "hydra_test.db" : "hydra.db"
|
isStaging ? "hydra_test.db" : "hydra.db"
|
||||||
);
|
);
|
||||||
|
|
||||||
export const logsPath = path.join(app.getPath("appData"), "hydra", "logs");
|
export const logsPath = path.join(app.getPath("userData"), "logs");
|
||||||
|
|
||||||
export const seedsPath = app.isPackaged
|
export const seedsPath = app.isPackaged
|
||||||
? path.join(process.resourcesPath, "seeds")
|
? path.join(process.resourcesPath, "seeds")
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
import { DataSource } from "typeorm";
|
|
||||||
import {
|
|
||||||
DownloadQueue,
|
|
||||||
Game,
|
|
||||||
GameShopCache,
|
|
||||||
UserPreferences,
|
|
||||||
UserAuth,
|
|
||||||
GameAchievement,
|
|
||||||
UserSubscription,
|
|
||||||
} from "@main/entity";
|
|
||||||
|
|
||||||
import { databasePath } from "./constants";
|
|
||||||
|
|
||||||
export const dataSource = new DataSource({
|
|
||||||
type: "better-sqlite3",
|
|
||||||
entities: [
|
|
||||||
Game,
|
|
||||||
UserAuth,
|
|
||||||
UserPreferences,
|
|
||||||
UserSubscription,
|
|
||||||
GameShopCache,
|
|
||||||
DownloadQueue,
|
|
||||||
GameAchievement,
|
|
||||||
],
|
|
||||||
synchronize: false,
|
|
||||||
database: databasePath,
|
|
||||||
});
|
|
|
@ -1,25 +0,0 @@
|
||||||
import {
|
|
||||||
Entity,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
CreateDateColumn,
|
|
||||||
UpdateDateColumn,
|
|
||||||
OneToOne,
|
|
||||||
JoinColumn,
|
|
||||||
} from "typeorm";
|
|
||||||
import type { Game } from "./game.entity";
|
|
||||||
|
|
||||||
@Entity("download_queue")
|
|
||||||
export class DownloadQueue {
|
|
||||||
@PrimaryGeneratedColumn()
|
|
||||||
id: number;
|
|
||||||
|
|
||||||
@OneToOne("Game", "downloadQueue")
|
|
||||||
@JoinColumn()
|
|
||||||
game: Game;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
|
||||||
|
|
||||||
@Entity("game_achievement")
|
|
||||||
export class GameAchievement {
|
|
||||||
@PrimaryGeneratedColumn()
|
|
||||||
id: number;
|
|
||||||
|
|
||||||
@Column("text")
|
|
||||||
objectId: string;
|
|
||||||
|
|
||||||
@Column("text")
|
|
||||||
shop: string;
|
|
||||||
|
|
||||||
@Column("text", { nullable: true })
|
|
||||||
unlockedAchievements: string | null;
|
|
||||||
|
|
||||||
@Column("text", { nullable: true })
|
|
||||||
achievements: string | null;
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
import {
|
|
||||||
Entity,
|
|
||||||
PrimaryColumn,
|
|
||||||
Column,
|
|
||||||
CreateDateColumn,
|
|
||||||
UpdateDateColumn,
|
|
||||||
} from "typeorm";
|
|
||||||
import type { GameShop } from "@types";
|
|
||||||
|
|
||||||
@Entity("game_shop_cache")
|
|
||||||
export class GameShopCache {
|
|
||||||
@PrimaryColumn("text", { unique: true })
|
|
||||||
objectID: string;
|
|
||||||
|
|
||||||
@Column("text")
|
|
||||||
shop: GameShop;
|
|
||||||
|
|
||||||
@Column("text", { nullable: true })
|
|
||||||
serializedData: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use IndexedDB's `howLongToBeatEntries` instead
|
|
||||||
*/
|
|
||||||
@Column("text", { nullable: true })
|
|
||||||
howLongToBeatSerializedData: string;
|
|
||||||
|
|
||||||
@Column("text", { nullable: true })
|
|
||||||
language: string;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
|
@ -1,90 +0,0 @@
|
||||||
import {
|
|
||||||
Entity,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Column,
|
|
||||||
CreateDateColumn,
|
|
||||||
UpdateDateColumn,
|
|
||||||
OneToOne,
|
|
||||||
} from "typeorm";
|
|
||||||
|
|
||||||
import type { GameShop, GameStatus } from "@types";
|
|
||||||
import { Downloader } from "@shared";
|
|
||||||
import type { DownloadQueue } from "./download-queue.entity";
|
|
||||||
|
|
||||||
@Entity("game")
|
|
||||||
export class Game {
|
|
||||||
@PrimaryGeneratedColumn()
|
|
||||||
id: number;
|
|
||||||
|
|
||||||
@Column("text", { unique: true })
|
|
||||||
objectID: string;
|
|
||||||
|
|
||||||
@Column("text", { unique: true, nullable: true })
|
|
||||||
remoteId: string | null;
|
|
||||||
|
|
||||||
@Column("text")
|
|
||||||
title: string;
|
|
||||||
|
|
||||||
@Column("text", { nullable: true })
|
|
||||||
iconUrl: string | null;
|
|
||||||
|
|
||||||
@Column("text", { nullable: true })
|
|
||||||
folderName: string | null;
|
|
||||||
|
|
||||||
@Column("text", { nullable: true })
|
|
||||||
downloadPath: string | null;
|
|
||||||
|
|
||||||
@Column("text", { nullable: true })
|
|
||||||
executablePath: string | null;
|
|
||||||
|
|
||||||
@Column("text", { nullable: true })
|
|
||||||
launchOptions: string | null;
|
|
||||||
|
|
||||||
@Column("text", { nullable: true })
|
|
||||||
winePrefixPath: string | null;
|
|
||||||
|
|
||||||
@Column("int", { default: 0 })
|
|
||||||
playTimeInMilliseconds: number;
|
|
||||||
|
|
||||||
@Column("text")
|
|
||||||
shop: GameShop;
|
|
||||||
|
|
||||||
@Column("text", { nullable: true })
|
|
||||||
status: GameStatus | null;
|
|
||||||
|
|
||||||
@Column("int", { default: Downloader.Torrent })
|
|
||||||
downloader: Downloader;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Progress is a float between 0 and 1
|
|
||||||
*/
|
|
||||||
@Column("float", { default: 0 })
|
|
||||||
progress: number;
|
|
||||||
|
|
||||||
@Column("int", { default: 0 })
|
|
||||||
bytesDownloaded: number;
|
|
||||||
|
|
||||||
@Column("datetime", { nullable: true })
|
|
||||||
lastTimePlayed: Date | null;
|
|
||||||
|
|
||||||
@Column("float", { default: 0 })
|
|
||||||
fileSize: number;
|
|
||||||
|
|
||||||
@Column("text", { nullable: true })
|
|
||||||
uri: string | null;
|
|
||||||
|
|
||||||
@OneToOne("DownloadQueue", "game")
|
|
||||||
downloadQueue: DownloadQueue;
|
|
||||||
|
|
||||||
@Column("boolean", { default: false })
|
|
||||||
isDeleted: boolean;
|
|
||||||
|
|
||||||
@Column("boolean", { default: false })
|
|
||||||
shouldSeed: boolean;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
export * from "./game.entity";
|
|
||||||
export * from "./user-auth.entity";
|
|
||||||
export * from "./user-preferences.entity";
|
|
||||||
export * from "./user-subscription.entity";
|
|
||||||
export * from "./game-shop-cache.entity";
|
|
||||||
export * from "./game.entity";
|
|
||||||
export * from "./game-achievements.entity";
|
|
||||||
export * from "./download-queue.entity";
|
|
|
@ -1,45 +0,0 @@
|
||||||
import {
|
|
||||||
Entity,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Column,
|
|
||||||
CreateDateColumn,
|
|
||||||
UpdateDateColumn,
|
|
||||||
OneToOne,
|
|
||||||
} from "typeorm";
|
|
||||||
import { UserSubscription } from "./user-subscription.entity";
|
|
||||||
|
|
||||||
@Entity("user_auth")
|
|
||||||
export class UserAuth {
|
|
||||||
@PrimaryGeneratedColumn()
|
|
||||||
id: number;
|
|
||||||
|
|
||||||
@Column("text", { default: "" })
|
|
||||||
userId: string;
|
|
||||||
|
|
||||||
@Column("text", { default: "" })
|
|
||||||
displayName: string;
|
|
||||||
|
|
||||||
@Column("text", { nullable: true })
|
|
||||||
profileImageUrl: string | null;
|
|
||||||
|
|
||||||
@Column("text", { nullable: true })
|
|
||||||
backgroundImageUrl: string | null;
|
|
||||||
|
|
||||||
@Column("text", { default: "" })
|
|
||||||
accessToken: string;
|
|
||||||
|
|
||||||
@Column("text", { default: "" })
|
|
||||||
refreshToken: string;
|
|
||||||
|
|
||||||
@Column("int", { default: 0 })
|
|
||||||
tokenExpirationTimestamp: number;
|
|
||||||
|
|
||||||
@OneToOne("UserSubscription", "user")
|
|
||||||
subscription: UserSubscription | null;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
import {
|
|
||||||
Entity,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Column,
|
|
||||||
CreateDateColumn,
|
|
||||||
UpdateDateColumn,
|
|
||||||
} from "typeorm";
|
|
||||||
|
|
||||||
@Entity("user_preferences")
|
|
||||||
export class UserPreferences {
|
|
||||||
@PrimaryGeneratedColumn()
|
|
||||||
id: number;
|
|
||||||
|
|
||||||
@Column("text", { nullable: true })
|
|
||||||
downloadsPath: string | null;
|
|
||||||
|
|
||||||
@Column("text", { default: "en" })
|
|
||||||
language: string;
|
|
||||||
|
|
||||||
@Column("text", { nullable: true })
|
|
||||||
realDebridApiToken: string | null;
|
|
||||||
|
|
||||||
@Column("text", { nullable: true })
|
|
||||||
torBoxApiToken: string | null;
|
|
||||||
|
|
||||||
@Column("boolean", { default: false })
|
|
||||||
downloadNotificationsEnabled: boolean;
|
|
||||||
|
|
||||||
@Column("boolean", { default: false })
|
|
||||||
repackUpdatesNotificationsEnabled: boolean;
|
|
||||||
|
|
||||||
@Column("boolean", { default: true })
|
|
||||||
achievementNotificationsEnabled: boolean;
|
|
||||||
|
|
||||||
@Column("boolean", { default: false })
|
|
||||||
preferQuitInsteadOfHiding: boolean;
|
|
||||||
|
|
||||||
@Column("boolean", { default: false })
|
|
||||||
runAtStartup: boolean;
|
|
||||||
|
|
||||||
@Column("boolean", { default: false })
|
|
||||||
startMinimized: boolean;
|
|
||||||
|
|
||||||
@Column("boolean", { default: false })
|
|
||||||
disableNsfwAlert: boolean;
|
|
||||||
|
|
||||||
@Column("boolean", { default: true })
|
|
||||||
seedAfterDownloadComplete: boolean;
|
|
||||||
|
|
||||||
@Column("boolean", { default: false })
|
|
||||||
showHiddenAchievementsDescription: boolean;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
import type { SubscriptionStatus } from "@types";
|
|
||||||
import {
|
|
||||||
Entity,
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Column,
|
|
||||||
CreateDateColumn,
|
|
||||||
UpdateDateColumn,
|
|
||||||
OneToOne,
|
|
||||||
JoinColumn,
|
|
||||||
} from "typeorm";
|
|
||||||
import { UserAuth } from "./user-auth.entity";
|
|
||||||
|
|
||||||
@Entity("user_subscription")
|
|
||||||
export class UserSubscription {
|
|
||||||
@PrimaryGeneratedColumn()
|
|
||||||
id: number;
|
|
||||||
|
|
||||||
@Column("text", { default: "" })
|
|
||||||
subscriptionId: string;
|
|
||||||
|
|
||||||
@OneToOne("UserAuth", "subscription")
|
|
||||||
@JoinColumn()
|
|
||||||
user: UserAuth;
|
|
||||||
|
|
||||||
@Column("text", { default: "" })
|
|
||||||
status: SubscriptionStatus;
|
|
||||||
|
|
||||||
@Column("text", { default: "" })
|
|
||||||
planId: string;
|
|
||||||
|
|
||||||
@Column("text", { default: "" })
|
|
||||||
planName: string;
|
|
||||||
|
|
||||||
@Column("datetime", { nullable: true })
|
|
||||||
expiresAt: Date | null;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
|
@ -1,13 +1,19 @@
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
import { userAuthRepository } from "@main/repository";
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import { db, levelKeys } from "@main/level";
|
||||||
|
import type { Auth } from "@types";
|
||||||
|
import { Crypto } from "@main/services";
|
||||||
|
|
||||||
const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => {
|
const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
const auth = await userAuthRepository.findOne({ where: { id: 1 } });
|
const auth = await db.get<string, Auth>(levelKeys.auth, {
|
||||||
|
valueEncoding: "json",
|
||||||
|
});
|
||||||
|
|
||||||
if (!auth) return null;
|
if (!auth) return null;
|
||||||
const payload = jwt.decode(auth.accessToken) as jwt.JwtPayload;
|
const payload = jwt.decode(
|
||||||
|
Crypto.decrypt(auth.accessToken)
|
||||||
|
) as jwt.JwtPayload;
|
||||||
|
|
||||||
if (!payload) return null;
|
if (!payload) return null;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,24 @@
|
||||||
|
import i18next from "i18next";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { WindowManager } from "@main/services";
|
import { HydraApi, WindowManager } from "@main/services";
|
||||||
|
import { AuthPage } from "@shared";
|
||||||
|
|
||||||
const openAuthWindow = async (_event: Electron.IpcMainInvokeEvent) =>
|
const openAuthWindow = async (
|
||||||
WindowManager.openAuthWindow();
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
page: AuthPage
|
||||||
|
) => {
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
lng: i18next.language,
|
||||||
|
});
|
||||||
|
|
||||||
|
if ([AuthPage.UpdateEmail, AuthPage.UpdatePassword].includes(page)) {
|
||||||
|
const { accessToken } = await HydraApi.refreshToken().catch(() => {
|
||||||
|
return { accessToken: "" };
|
||||||
|
});
|
||||||
|
searchParams.set("token", accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowManager.openAuthWindow(page, searchParams);
|
||||||
|
};
|
||||||
|
|
||||||
registerEvent("openAuthWindow", openAuthWindow);
|
registerEvent("openAuthWindow", openAuthWindow);
|
||||||
|
|
|
@ -1,26 +1,25 @@
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { DownloadManager, HydraApi, gamesPlaytime } from "@main/services";
|
import { DownloadManager, HydraApi, gamesPlaytime } from "@main/services";
|
||||||
import { dataSource } from "@main/data-source";
|
import { PythonRPC } from "@main/services/python-rpc";
|
||||||
import { DownloadQueue, Game, UserAuth, UserSubscription } from "@main/entity";
|
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
|
const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
const databaseOperations = dataSource
|
const databaseOperations = db
|
||||||
.transaction(async (transactionalEntityManager) => {
|
.batch([
|
||||||
await transactionalEntityManager.getRepository(DownloadQueue).delete({});
|
{
|
||||||
|
type: "del",
|
||||||
await transactionalEntityManager.getRepository(Game).delete({});
|
key: levelKeys.auth,
|
||||||
|
},
|
||||||
await transactionalEntityManager
|
{
|
||||||
.getRepository(UserAuth)
|
type: "del",
|
||||||
.delete({ id: 1 });
|
key: levelKeys.user,
|
||||||
|
},
|
||||||
await transactionalEntityManager
|
])
|
||||||
.getRepository(UserSubscription)
|
|
||||||
.delete({ id: 1 });
|
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
/* Removes all games being played */
|
/* Removes all games being played */
|
||||||
gamesPlaytime.clear();
|
gamesPlaytime.clear();
|
||||||
|
|
||||||
|
return Promise.all([gamesSublevel.clear(), downloadsSublevel.clear()]);
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Cancels any ongoing downloads */
|
/* Cancels any ongoing downloads */
|
||||||
|
|
|
@ -1,47 +1,8 @@
|
||||||
import type { AppUpdaterEvent } from "@types";
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import updater, { UpdateInfo } from "electron-updater";
|
import { UpdateManager } from "@main/services/update-manager";
|
||||||
import { WindowManager } from "@main/services";
|
|
||||||
import { app } from "electron";
|
|
||||||
import { publishNotificationUpdateReadyToInstall } from "@main/services/notifications";
|
|
||||||
|
|
||||||
const { autoUpdater } = updater;
|
|
||||||
|
|
||||||
const sendEvent = (event: AppUpdaterEvent) => {
|
|
||||||
WindowManager.mainWindow?.webContents.send("autoUpdaterEvent", event);
|
|
||||||
};
|
|
||||||
|
|
||||||
const sendEventsForDebug = false;
|
|
||||||
|
|
||||||
const isAutoInstallAvailable =
|
|
||||||
process.platform !== "darwin" && process.env.PORTABLE_EXECUTABLE_FILE == null;
|
|
||||||
|
|
||||||
const mockValuesForDebug = () => {
|
|
||||||
sendEvent({ type: "update-available", info: { version: "1.3.0" } });
|
|
||||||
sendEvent({ type: "update-downloaded" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const newVersionInfo = { version: "" };
|
|
||||||
|
|
||||||
const checkForUpdates = async (_event: Electron.IpcMainInvokeEvent) => {
|
const checkForUpdates = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
autoUpdater
|
return UpdateManager.checkForUpdates();
|
||||||
.once("update-available", (info: UpdateInfo) => {
|
|
||||||
sendEvent({ type: "update-available", info });
|
|
||||||
newVersionInfo.version = info.version;
|
|
||||||
})
|
|
||||||
.once("update-downloaded", () => {
|
|
||||||
sendEvent({ type: "update-downloaded" });
|
|
||||||
publishNotificationUpdateReadyToInstall(newVersionInfo.version);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (app.isPackaged) {
|
|
||||||
autoUpdater.autoDownload = isAutoInstallAvailable;
|
|
||||||
autoUpdater.checkForUpdates();
|
|
||||||
} else if (sendEventsForDebug) {
|
|
||||||
mockValuesForDebug();
|
|
||||||
}
|
|
||||||
|
|
||||||
return isAutoInstallAvailable;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("checkForUpdates", checkForUpdates);
|
registerEvent("checkForUpdates", checkForUpdates);
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { gameShopCacheRepository } from "@main/repository";
|
import { getSteamAppDetails, logger } from "@main/services";
|
||||||
import { getSteamAppDetails } from "@main/services";
|
|
||||||
|
|
||||||
import type { ShopDetails, GameShop, SteamAppDetails } from "@types";
|
import type { ShopDetails, GameShop } from "@types";
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
|
import { gamesShopCacheSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const getLocalizedSteamAppDetails = async (
|
const getLocalizedSteamAppDetails = async (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
|
@ -39,35 +39,27 @@ const getGameShopDetails = async (
|
||||||
language: string
|
language: string
|
||||||
): Promise<ShopDetails | null> => {
|
): Promise<ShopDetails | null> => {
|
||||||
if (shop === "steam") {
|
if (shop === "steam") {
|
||||||
const cachedData = await gameShopCacheRepository.findOne({
|
const cachedData = await gamesShopCacheSublevel.get(
|
||||||
where: { objectID: objectId, language },
|
levelKeys.gameShopCacheItem(shop, objectId, language)
|
||||||
});
|
);
|
||||||
|
|
||||||
const appDetails = getLocalizedSteamAppDetails(objectId, language).then(
|
const appDetails = getLocalizedSteamAppDetails(objectId, language).then(
|
||||||
(result) => {
|
(result) => {
|
||||||
if (result) {
|
if (result) {
|
||||||
gameShopCacheRepository.upsert(
|
gamesShopCacheSublevel
|
||||||
{
|
.put(levelKeys.gameShopCacheItem(shop, objectId, language), result)
|
||||||
objectID: objectId,
|
.catch((err) => {
|
||||||
shop: "steam",
|
logger.error("Could not cache game details", err);
|
||||||
language,
|
});
|
||||||
serializedData: JSON.stringify(result),
|
|
||||||
},
|
|
||||||
["objectID"]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const cachedGame = cachedData?.serializedData
|
if (cachedData) {
|
||||||
? (JSON.parse(cachedData?.serializedData) as SteamAppDetails)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (cachedGame) {
|
|
||||||
return {
|
return {
|
||||||
...cachedGame,
|
...cachedData,
|
||||||
objectId,
|
objectId,
|
||||||
} as ShopDetails;
|
} as ShopDetails;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
|
import { db, levelKeys } from "@main/level";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { HydraApi } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
import { userPreferencesRepository } from "@main/repository";
|
|
||||||
import type { TrendingGame } from "@types";
|
import type { TrendingGame } from "@types";
|
||||||
|
|
||||||
const getTrendingGames = async (_event: Electron.IpcMainInvokeEvent) => {
|
const getTrendingGames = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
const userPreferences = await userPreferencesRepository.findOne({
|
const language = await db
|
||||||
where: { id: 1 },
|
.get<string, string>(levelKeys.language, {
|
||||||
});
|
valueEncoding: "utf-8",
|
||||||
|
})
|
||||||
const language = userPreferences?.language || "en";
|
.then((language) => language || "en");
|
||||||
|
|
||||||
const trendingGames = await HydraApi.get<TrendingGame[]>(
|
const trendingGames = await HydraApi.get<TrendingGame[]>(
|
||||||
"/games/trending",
|
"/games/trending",
|
||||||
|
|
|
@ -1,19 +1,14 @@
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import type { GameShop } from "@types";
|
import type { GameShop } from "@types";
|
||||||
import { Ludusavi } from "@main/services";
|
import { Ludusavi } from "@main/services";
|
||||||
import { gameRepository } from "@main/repository";
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const getGameBackupPreview = async (
|
const getGameBackupPreview = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop
|
shop: GameShop
|
||||||
) => {
|
) => {
|
||||||
const game = await gameRepository.findOne({
|
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
||||||
where: {
|
|
||||||
objectID: objectId,
|
|
||||||
shop,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return Ludusavi.getBackupPreview(shop, objectId, game?.winePrefixPath);
|
return Ludusavi.getBackupPreview(shop, objectId, game?.winePrefixPath);
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,7 +10,7 @@ import os from "node:os";
|
||||||
import { backupsPath } from "@main/constants";
|
import { backupsPath } from "@main/constants";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import { normalizePath } from "@main/helpers";
|
import { normalizePath } from "@main/helpers";
|
||||||
import { gameRepository } from "@main/repository";
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const bundleBackup = async (
|
const bundleBackup = async (
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
|
@ -45,12 +45,7 @@ export const createBackup = async (
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
downloadOptionTitle: string | null
|
downloadOptionTitle: string | null
|
||||||
) => {
|
) => {
|
||||||
const game = await gameRepository.findOne({
|
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
||||||
where: {
|
|
||||||
objectID: objectId,
|
|
||||||
shop,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const bundleLocation = await bundleBackup(
|
const bundleLocation = await bundleBackup(
|
||||||
shop,
|
shop,
|
||||||
|
|
|
@ -1,15 +1,21 @@
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
const checkFolderWritePermission = async (
|
const checkFolderWritePermission = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
path: string
|
testPath: string
|
||||||
) =>
|
) => {
|
||||||
new Promise((resolve) => {
|
const testFilePath = path.join(testPath, ".hydra-write-test");
|
||||||
fs.access(path, fs.constants.W_OK, (err) => {
|
|
||||||
resolve(!err);
|
try {
|
||||||
});
|
fs.writeFileSync(testFilePath, "");
|
||||||
});
|
fs.rmSync(testFilePath);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
registerEvent("checkFolderWritePermission", checkFolderWritePermission);
|
registerEvent("checkFolderWritePermission", checkFolderWritePermission);
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
import { Document as YMLDocument } from "yaml";
|
|
||||||
import { Game } from "@main/entity";
|
|
||||||
import path from "node:path";
|
|
||||||
|
|
||||||
export const generateYML = (game: Game) => {
|
|
||||||
const slugifiedGameTitle = game.title.replace(/\s/g, "-").toLocaleLowerCase();
|
|
||||||
|
|
||||||
const doc = new YMLDocument({
|
|
||||||
name: game.title,
|
|
||||||
game_slug: slugifiedGameTitle,
|
|
||||||
slug: `${slugifiedGameTitle}-installer`,
|
|
||||||
version: "Installer",
|
|
||||||
runner: "wine",
|
|
||||||
script: {
|
|
||||||
game: {
|
|
||||||
prefix: "$GAMEDIR",
|
|
||||||
arch: "win64",
|
|
||||||
working_dir: "$GAMEDIR",
|
|
||||||
},
|
|
||||||
installer: [
|
|
||||||
{
|
|
||||||
task: {
|
|
||||||
name: "create_prefix",
|
|
||||||
arch: "win64",
|
|
||||||
prefix: "$GAMEDIR",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
task: {
|
|
||||||
executable: path.join(
|
|
||||||
game.downloadPath!,
|
|
||||||
game.folderName!,
|
|
||||||
"setup.exe"
|
|
||||||
),
|
|
||||||
name: "wineexec",
|
|
||||||
prefix: "$GAMEDIR",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return doc.toString();
|
|
||||||
};
|
|
|
@ -1,12 +1,14 @@
|
||||||
import { userPreferencesRepository } from "@main/repository";
|
|
||||||
import { defaultDownloadsPath } from "@main/constants";
|
import { defaultDownloadsPath } from "@main/constants";
|
||||||
|
import { db, levelKeys } from "@main/level";
|
||||||
|
import type { UserPreferences } from "@types";
|
||||||
|
|
||||||
export const getDownloadsPath = async () => {
|
export const getDownloadsPath = async () => {
|
||||||
const userPreferences = await userPreferencesRepository.findOne({
|
const userPreferences = await db.get<string, UserPreferences>(
|
||||||
where: {
|
levelKeys.userPreferences,
|
||||||
id: 1,
|
{
|
||||||
},
|
valueEncoding: "json",
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (userPreferences && userPreferences.downloadsPath)
|
if (userPreferences && userPreferences.downloadsPath)
|
||||||
return userPreferences.downloadsPath;
|
return userPreferences.downloadsPath;
|
||||||
|
|
7
src/main/events/helpers/parse-launch-options.ts
Normal file
7
src/main/events/helpers/parse-launch-options.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export const parseLaunchOptions = (params?: string | null): string[] => {
|
||||||
|
if (!params) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return params.split(" ");
|
||||||
|
};
|
|
@ -1,57 +1,55 @@
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
import type { GameShop } from "@types";
|
import type { Game, GameShop } from "@types";
|
||||||
|
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
import { createGame } from "@main/services/library-sync";
|
import { createGame } from "@main/services/library-sync";
|
||||||
import { steamUrlBuilder } from "@shared";
|
import { steamUrlBuilder } from "@shared";
|
||||||
import { updateLocalUnlockedAchivements } from "@main/services/achievements/update-local-unlocked-achivements";
|
import { updateLocalUnlockedAchivements } from "@main/services/achievements/update-local-unlocked-achivements";
|
||||||
|
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const addGameToLibrary = async (
|
const addGameToLibrary = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
shop: GameShop,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
title: string,
|
title: string
|
||||||
shop: GameShop
|
|
||||||
) => {
|
) => {
|
||||||
return gameRepository
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
.update(
|
const game = await gamesSublevel.get(gameKey);
|
||||||
{
|
|
||||||
objectID: objectId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
shop,
|
|
||||||
status: null,
|
|
||||||
isDeleted: false,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(async ({ affected }) => {
|
|
||||||
if (!affected) {
|
|
||||||
const steamGame = await steamGamesWorker.run(Number(objectId), {
|
|
||||||
name: "getById",
|
|
||||||
});
|
|
||||||
|
|
||||||
const iconUrl = steamGame?.clientIcon
|
if (game) {
|
||||||
? steamUrlBuilder.icon(objectId, steamGame.clientIcon)
|
await downloadsSublevel.del(gameKey);
|
||||||
: null;
|
|
||||||
|
|
||||||
await gameRepository.insert({
|
await gamesSublevel.put(gameKey, {
|
||||||
title,
|
...game,
|
||||||
iconUrl,
|
isDeleted: false,
|
||||||
objectID: objectId,
|
|
||||||
shop,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const game = await gameRepository.findOne({
|
|
||||||
where: { objectID: objectId },
|
|
||||||
});
|
|
||||||
|
|
||||||
updateLocalUnlockedAchivements(game!);
|
|
||||||
|
|
||||||
createGame(game!).catch(() => {});
|
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
const steamGame = await steamGamesWorker.run(Number(objectId), {
|
||||||
|
name: "getById",
|
||||||
|
});
|
||||||
|
|
||||||
|
const iconUrl = steamGame?.clientIcon
|
||||||
|
? steamUrlBuilder.icon(objectId, steamGame.clientIcon)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const game: Game = {
|
||||||
|
title,
|
||||||
|
iconUrl,
|
||||||
|
objectId,
|
||||||
|
shop,
|
||||||
|
remoteId: null,
|
||||||
|
isDeleted: false,
|
||||||
|
playTimeInMilliseconds: 0,
|
||||||
|
lastTimePlayed: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
await gamesSublevel.put(levelKeys.game(shop, objectId), game);
|
||||||
|
|
||||||
|
updateLocalUnlockedAchivements(game!);
|
||||||
|
|
||||||
|
createGame(game!).catch(() => {});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("addGameToLibrary", addGameToLibrary);
|
registerEvent("addGameToLibrary", addGameToLibrary);
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { logger } from "@main/services";
|
import { logger } from "@main/services";
|
||||||
import sudo from "sudo-prompt";
|
import sudo from "sudo-prompt";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import { PythonRPC } from "@main/services/python-rpc";
|
import { PythonRPC } from "@main/services/python-rpc";
|
||||||
import { ProcessPayload } from "@main/services/download/types";
|
import { ProcessPayload } from "@main/services/download/types";
|
||||||
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
import { GameShop } from "@types";
|
||||||
|
|
||||||
const getKillCommand = (pid: number) => {
|
const getKillCommand = (pid: number) => {
|
||||||
if (process.platform == "win32") {
|
if (process.platform == "win32") {
|
||||||
|
@ -16,15 +17,14 @@ const getKillCommand = (pid: number) => {
|
||||||
|
|
||||||
const closeGame = async (
|
const closeGame = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
) => {
|
) => {
|
||||||
const processes =
|
const processes =
|
||||||
(await PythonRPC.rpc.get<ProcessPayload[] | null>("/process-list")).data ||
|
(await PythonRPC.rpc.get<ProcessPayload[] | null>("/process-list")).data ||
|
||||||
[];
|
[];
|
||||||
|
|
||||||
const game = await gameRepository.findOne({
|
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
||||||
where: { id: gameId, isDeleted: false },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!game) return;
|
if (!game) return;
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { IsNull, Not } from "typeorm";
|
|
||||||
import createDesktopShortcut from "create-desktop-shortcuts";
|
import createDesktopShortcut from "create-desktop-shortcuts";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import { removeSymbolsFromName } from "@shared";
|
import { removeSymbolsFromName } from "@shared";
|
||||||
|
import { GameShop } from "@types";
|
||||||
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const createGameShortcut = async (
|
const createGameShortcut = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
id: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
const game = await gameRepository.findOne({
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
where: { id, executablePath: Not(IsNull()) },
|
const game = await gamesSublevel.get(gameKey);
|
||||||
});
|
|
||||||
|
|
||||||
if (game) {
|
if (game) {
|
||||||
const filePath = game.executablePath;
|
const filePath = game.executablePath;
|
||||||
|
|
|
@ -1,37 +1,27 @@
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
|
|
||||||
import { getDownloadsPath } from "../helpers/get-downloads-path";
|
import { getDownloadsPath } from "../helpers/get-downloads-path";
|
||||||
import { logger } from "@main/services";
|
import { logger } from "@main/services";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import { GameShop } from "@types";
|
||||||
|
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const deleteGameFolder = async (
|
const deleteGameFolder = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const game = await gameRepository.findOne({
|
const downloadKey = levelKeys.game(shop, objectId);
|
||||||
where: [
|
|
||||||
{
|
|
||||||
id: gameId,
|
|
||||||
isDeleted: false,
|
|
||||||
status: "removed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: gameId,
|
|
||||||
progress: 1,
|
|
||||||
isDeleted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!game) return;
|
const download = await downloadsSublevel.get(downloadKey);
|
||||||
|
|
||||||
if (game.folderName) {
|
if (!download) return;
|
||||||
|
|
||||||
|
if (download.folderName) {
|
||||||
const folderPath = path.join(
|
const folderPath = path.join(
|
||||||
game.downloadPath ?? (await getDownloadsPath()),
|
download.downloadPath ?? (await getDownloadsPath()),
|
||||||
game.folderName
|
download.folderName
|
||||||
);
|
);
|
||||||
|
|
||||||
if (fs.existsSync(folderPath)) {
|
if (fs.existsSync(folderPath)) {
|
||||||
|
@ -52,10 +42,7 @@ const deleteGameFolder = async (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await gameRepository.update(
|
await downloadsSublevel.del(downloadKey);
|
||||||
{ id: gameId },
|
|
||||||
{ downloadPath: null, folderName: null, status: null, progress: 0 }
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("deleteGameFolder", deleteGameFolder);
|
registerEvent("deleteGameFolder", deleteGameFolder);
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import { gamesSublevel, downloadsSublevel, levelKeys } from "@main/level";
|
||||||
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
const getGameByObjectId = async (
|
const getGameByObjectId = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
shop: GameShop,
|
||||||
objectId: string
|
objectId: string
|
||||||
) =>
|
) => {
|
||||||
gameRepository.findOne({
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
where: {
|
const [game, download] = await Promise.all([
|
||||||
objectID: objectId,
|
gamesSublevel.get(gameKey),
|
||||||
isDeleted: false,
|
downloadsSublevel.get(gameKey),
|
||||||
},
|
]);
|
||||||
});
|
|
||||||
|
if (!game || game.isDeleted) return null;
|
||||||
|
|
||||||
|
return { id: gameKey, ...game, download };
|
||||||
|
};
|
||||||
|
|
||||||
registerEvent("getGameByObjectId", getGameByObjectId);
|
registerEvent("getGameByObjectId", getGameByObjectId);
|
||||||
|
|
|
@ -1,17 +1,26 @@
|
||||||
import { gameRepository } from "@main/repository";
|
import type { LibraryGame } from "@types";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import { downloadsSublevel, gamesSublevel } from "@main/level";
|
||||||
|
|
||||||
const getLibrary = async () =>
|
const getLibrary = async (): Promise<LibraryGame[]> => {
|
||||||
gameRepository.find({
|
return gamesSublevel
|
||||||
where: {
|
.iterator()
|
||||||
isDeleted: false,
|
.all()
|
||||||
},
|
.then((results) => {
|
||||||
relations: {
|
return Promise.all(
|
||||||
downloadQueue: true,
|
results
|
||||||
},
|
.filter(([_key, game]) => game.isDeleted === false)
|
||||||
order: {
|
.map(async ([key, game]) => {
|
||||||
createdAt: "desc",
|
const download = await downloadsSublevel.get(key);
|
||||||
},
|
|
||||||
});
|
return {
|
||||||
|
id: key,
|
||||||
|
...game,
|
||||||
|
download: download ?? null,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
registerEvent("getLibrary", getLibrary);
|
registerEvent("getLibrary", getLibrary);
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { shell } from "electron";
|
import { shell } from "electron";
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
import { GameShop } from "@types";
|
||||||
|
|
||||||
const openGameExecutablePath = async (
|
const openGameExecutablePath = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
) => {
|
) => {
|
||||||
const game = await gameRepository.findOne({
|
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
||||||
where: { id: gameId, isDeleted: false },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!game || !game.executablePath) return;
|
if (!game || !game.executablePath) return;
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
import { shell } from "electron";
|
import { shell } from "electron";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
import { getDownloadsPath } from "../helpers/get-downloads-path";
|
import { getDownloadsPath } from "../helpers/get-downloads-path";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import { GameShop } from "@types";
|
||||||
|
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const openGameInstallerPath = async (
|
const openGameInstallerPath = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
) => {
|
) => {
|
||||||
const game = await gameRepository.findOne({
|
const download = await downloadsSublevel.get(levelKeys.game(shop, objectId));
|
||||||
where: { id: gameId, isDeleted: false },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!game || !game.folderName || !game.downloadPath) return true;
|
if (!download || !download.folderName || !download.downloadPath) return true;
|
||||||
|
|
||||||
const gamePath = path.join(
|
const gamePath = path.join(
|
||||||
game.downloadPath ?? (await getDownloadsPath()),
|
download.downloadPath ?? (await getDownloadsPath()),
|
||||||
game.folderName!
|
download.folderName!
|
||||||
);
|
);
|
||||||
|
|
||||||
shell.showItemInFolder(gamePath);
|
shell.showItemInFolder(gamePath);
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
import { shell } from "electron";
|
import { shell } from "electron";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { writeFile } from "node:fs/promises";
|
|
||||||
import { spawnSync, exec } from "node:child_process";
|
import { spawnSync, exec } from "node:child_process";
|
||||||
|
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
|
|
||||||
import { generateYML } from "../helpers/generate-lutris-yaml";
|
|
||||||
import { getDownloadsPath } from "../helpers/get-downloads-path";
|
import { getDownloadsPath } from "../helpers/get-downloads-path";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||||
|
import { GameShop } from "@types";
|
||||||
|
|
||||||
const executeGameInstaller = (filePath: string) => {
|
const executeGameInstaller = (filePath: string) => {
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
|
@ -26,21 +24,21 @@ const executeGameInstaller = (filePath: string) => {
|
||||||
|
|
||||||
const openGameInstaller = async (
|
const openGameInstaller = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
) => {
|
) => {
|
||||||
const game = await gameRepository.findOne({
|
const downloadKey = levelKeys.game(shop, objectId);
|
||||||
where: { id: gameId, isDeleted: false },
|
const download = await downloadsSublevel.get(downloadKey);
|
||||||
});
|
|
||||||
|
|
||||||
if (!game || !game.folderName) return true;
|
if (!download?.folderName) return true;
|
||||||
|
|
||||||
const gamePath = path.join(
|
const gamePath = path.join(
|
||||||
game.downloadPath ?? (await getDownloadsPath()),
|
download.downloadPath ?? (await getDownloadsPath()),
|
||||||
game.folderName!
|
download.folderName
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!fs.existsSync(gamePath)) {
|
if (!fs.existsSync(gamePath)) {
|
||||||
await gameRepository.update({ id: gameId }, { status: null });
|
await downloadsSublevel.del(downloadKey);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,13 +68,6 @@ const openGameInstaller = async (
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (spawnSync("which", ["lutris"]).status === 0) {
|
|
||||||
const ymlPath = path.join(gamePath, "setup.yml");
|
|
||||||
await writeFile(ymlPath, generateYML(game));
|
|
||||||
exec(`lutris --install "${ymlPath}"`);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
shell.openPath(gamePath);
|
shell.openPath(gamePath);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,24 +1,39 @@
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { shell } from "electron";
|
import { shell } from "electron";
|
||||||
|
import { spawn } from "child_process";
|
||||||
import { parseExecutablePath } from "../helpers/parse-executable-path";
|
import { parseExecutablePath } from "../helpers/parse-executable-path";
|
||||||
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
import { GameShop } from "@types";
|
||||||
|
import { parseLaunchOptions } from "../helpers/parse-launch-options";
|
||||||
|
|
||||||
const openGame = async (
|
const openGame = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number,
|
shop: GameShop,
|
||||||
|
objectId: string,
|
||||||
executablePath: string,
|
executablePath: string,
|
||||||
launchOptions: string | null
|
launchOptions?: string | null
|
||||||
) => {
|
) => {
|
||||||
// TODO: revisit this for launchOptions
|
|
||||||
const parsedPath = parseExecutablePath(executablePath);
|
const parsedPath = parseExecutablePath(executablePath);
|
||||||
|
const parsedParams = parseLaunchOptions(launchOptions);
|
||||||
|
|
||||||
await gameRepository.update(
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
{ id: gameId },
|
|
||||||
{ executablePath: parsedPath, launchOptions }
|
|
||||||
);
|
|
||||||
|
|
||||||
shell.openPath(parsedPath);
|
const game = await gamesSublevel.get(gameKey);
|
||||||
|
|
||||||
|
if (!game) return;
|
||||||
|
|
||||||
|
await gamesSublevel.put(gameKey, {
|
||||||
|
...game,
|
||||||
|
executablePath: parsedPath,
|
||||||
|
launchOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (parsedParams.length === 0) {
|
||||||
|
shell.openPath(parsedPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
spawn(parsedPath, parsedParams, { shell: false, detached: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("openGame", openGame);
|
registerEvent("openGame", openGame);
|
||||||
|
|
|
@ -1,26 +1,26 @@
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { gameRepository } from "../../repository";
|
import { HydraApi } from "@main/services";
|
||||||
import { HydraApi, logger } from "@main/services";
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
const removeGameFromLibrary = async (
|
const removeGameFromLibrary = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
) => {
|
) => {
|
||||||
gameRepository.update(
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
{ id: gameId },
|
const game = await gamesSublevel.get(gameKey);
|
||||||
{ isDeleted: true, executablePath: null }
|
|
||||||
);
|
|
||||||
|
|
||||||
removeRemoveGameFromLibrary(gameId).catch((err) => {
|
if (game) {
|
||||||
logger.error("removeRemoveGameFromLibrary", err);
|
await gamesSublevel.put(gameKey, {
|
||||||
});
|
...game,
|
||||||
};
|
isDeleted: true,
|
||||||
|
executablePath: null,
|
||||||
|
});
|
||||||
|
|
||||||
const removeRemoveGameFromLibrary = async (gameId: number) => {
|
if (game?.remoteId) {
|
||||||
const game = await gameRepository.findOne({ where: { id: gameId } });
|
HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {});
|
||||||
|
}
|
||||||
if (game?.remoteId) {
|
|
||||||
HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,14 @@
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { gameRepository } from "../../repository";
|
import { levelKeys, downloadsSublevel } from "@main/level";
|
||||||
|
import { GameShop } from "@types";
|
||||||
|
|
||||||
const removeGame = async (
|
const removeGame = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
) => {
|
) => {
|
||||||
await gameRepository.update(
|
const downloadKey = levelKeys.game(shop, objectId);
|
||||||
{
|
await downloadsSublevel.del(downloadKey);
|
||||||
id: gameId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: "removed",
|
|
||||||
downloadPath: null,
|
|
||||||
bytesDownloaded: 0,
|
|
||||||
progress: 0,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("removeGame", removeGame);
|
registerEvent("removeGame", removeGame);
|
||||||
|
|
|
@ -1,16 +1,22 @@
|
||||||
import { gameAchievementRepository, gameRepository } from "@main/repository";
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { findAchievementFiles } from "@main/services/achievements/find-achivement-files";
|
import { findAchievementFiles } from "@main/services/achievements/find-achivement-files";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { achievementsLogger, HydraApi, WindowManager } from "@main/services";
|
import { achievementsLogger, HydraApi, WindowManager } from "@main/services";
|
||||||
import { getUnlockedAchievements } from "../user/get-unlocked-achievements";
|
import { getUnlockedAchievements } from "../user/get-unlocked-achievements";
|
||||||
|
import {
|
||||||
|
gameAchievementsSublevel,
|
||||||
|
gamesSublevel,
|
||||||
|
levelKeys,
|
||||||
|
} from "@main/level";
|
||||||
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
const resetGameAchievements = async (
|
const resetGameAchievements = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const game = await gameRepository.findOne({ where: { id: gameId } });
|
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
||||||
|
|
||||||
if (!game) return;
|
if (!game) return;
|
||||||
|
|
||||||
|
@ -23,28 +29,34 @@ const resetGameAchievements = async (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await gameAchievementRepository.update(
|
const levelKey = levelKeys.game(game.shop, game.objectId);
|
||||||
{ objectId: game.objectID },
|
|
||||||
{
|
await gameAchievementsSublevel
|
||||||
unlockedAchievements: null,
|
.get(levelKey)
|
||||||
}
|
.then(async (gameAchievements) => {
|
||||||
);
|
if (gameAchievements) {
|
||||||
|
await gameAchievementsSublevel.put(levelKey, {
|
||||||
|
...gameAchievements,
|
||||||
|
unlockedAchievements: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
await HydraApi.delete(`/profile/games/achievements/${game.remoteId}`).then(
|
await HydraApi.delete(`/profile/games/achievements/${game.remoteId}`).then(
|
||||||
() =>
|
() =>
|
||||||
achievementsLogger.log(
|
achievementsLogger.log(
|
||||||
`Deleted achievements from ${game.remoteId} - ${game.objectID} - ${game.title}`
|
`Deleted achievements from ${game.remoteId} - ${game.objectId} - ${game.title}`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const gameAchievements = await getUnlockedAchievements(
|
const gameAchievements = await getUnlockedAchievements(
|
||||||
game.objectID,
|
game.objectId,
|
||||||
game.shop,
|
game.shop,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
WindowManager.mainWindow?.webContents.send(
|
WindowManager.mainWindow?.webContents.send(
|
||||||
`on-update-achievements-${game.objectID}-${game.shop}`,
|
`on-update-achievements-${game.objectId}-${game.shop}`,
|
||||||
gameAchievements
|
gameAchievements
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -1,13 +1,23 @@
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import { levelKeys, gamesSublevel } from "@main/level";
|
||||||
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
const selectGameWinePrefix = async (
|
const selectGameWinePrefix = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
id: number,
|
shop: GameShop,
|
||||||
|
objectId: string,
|
||||||
winePrefixPath: string | null
|
winePrefixPath: string | null
|
||||||
) => {
|
) => {
|
||||||
return gameRepository.update({ id }, { winePrefixPath: winePrefixPath });
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
|
|
||||||
|
const game = await gamesSublevel.get(gameKey);
|
||||||
|
|
||||||
|
if (!game) return;
|
||||||
|
|
||||||
|
await gamesSublevel.put(gameKey, {
|
||||||
|
...game,
|
||||||
|
winePrefixPath: winePrefixPath,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("selectGameWinePrefix", selectGameWinePrefix);
|
registerEvent("selectGameWinePrefix", selectGameWinePrefix);
|
||||||
|
|
|
@ -1,25 +1,27 @@
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { parseExecutablePath } from "../helpers/parse-executable-path";
|
import { parseExecutablePath } from "../helpers/parse-executable-path";
|
||||||
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
const updateExecutablePath = async (
|
const updateExecutablePath = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
id: number,
|
shop: GameShop,
|
||||||
|
objectId: string,
|
||||||
executablePath: string | null
|
executablePath: string | null
|
||||||
) => {
|
) => {
|
||||||
const parsedPath = executablePath
|
const parsedPath = executablePath
|
||||||
? parseExecutablePath(executablePath)
|
? parseExecutablePath(executablePath)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return gameRepository.update(
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
{
|
|
||||||
id,
|
const game = await gamesSublevel.get(gameKey);
|
||||||
},
|
if (!game) return;
|
||||||
{
|
|
||||||
executablePath: parsedPath,
|
await gamesSublevel.put(gameKey, {
|
||||||
}
|
...game,
|
||||||
);
|
executablePath: parsedPath,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("updateExecutablePath", updateExecutablePath);
|
registerEvent("updateExecutablePath", updateExecutablePath);
|
||||||
|
|
|
@ -1,19 +1,23 @@
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
import { GameShop } from "@types";
|
||||||
|
|
||||||
const updateLaunchOptions = async (
|
const updateLaunchOptions = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
id: number,
|
shop: GameShop,
|
||||||
|
objectId: string,
|
||||||
launchOptions: string | null
|
launchOptions: string | null
|
||||||
) => {
|
) => {
|
||||||
return gameRepository.update(
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
{
|
|
||||||
id,
|
const game = await gamesSublevel.get(gameKey);
|
||||||
},
|
|
||||||
{
|
if (game) {
|
||||||
|
await gamesSublevel.put(gameKey, {
|
||||||
|
...game,
|
||||||
launchOptions: launchOptions?.trim() != "" ? launchOptions : null,
|
launchOptions: launchOptions?.trim() != "" ? launchOptions : null,
|
||||||
}
|
});
|
||||||
);
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("updateLaunchOptions", updateLaunchOptions);
|
registerEvent("updateLaunchOptions", updateLaunchOptions);
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import { gamesSublevel } from "@main/level";
|
||||||
|
|
||||||
const verifyExecutablePathInUse = async (
|
const verifyExecutablePathInUse = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
executablePath: string
|
executablePath: string
|
||||||
) => {
|
) => {
|
||||||
return gameRepository.findOne({
|
for await (const game of gamesSublevel.values()) {
|
||||||
where: { executablePath },
|
if (game.executablePath === executablePath) {
|
||||||
});
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("verifyExecutablePathInUse", verifyExecutablePathInUse);
|
registerEvent("verifyExecutablePathInUse", verifyExecutablePathInUse);
|
||||||
|
|
|
@ -1,17 +1,20 @@
|
||||||
import { shell } from "electron";
|
import { shell } from "electron";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { userAuthRepository } from "@main/repository";
|
import { Crypto, HydraApi } from "@main/services";
|
||||||
import { HydraApi } from "@main/services";
|
import { db, levelKeys } from "@main/level";
|
||||||
|
import type { Auth } from "@types";
|
||||||
|
|
||||||
const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => {
|
const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => {
|
||||||
const userAuth = await userAuthRepository.findOne({ where: { id: 1 } });
|
const auth = await db.get<string, Auth>(levelKeys.auth, {
|
||||||
|
valueEncoding: "json",
|
||||||
|
});
|
||||||
|
|
||||||
if (!userAuth) {
|
if (!auth) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const paymentToken = await HydraApi.post("/auth/payment", {
|
const paymentToken = await HydraApi.post("/auth/payment", {
|
||||||
refreshToken: userAuth.refreshToken,
|
refreshToken: Crypto.decrypt(auth.refreshToken),
|
||||||
}).then((response) => response.accessToken);
|
}).then((response) => response.accessToken);
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { Notification } from "electron";
|
import { Notification } from "electron";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { userPreferencesRepository } from "@main/repository";
|
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
|
import { db, levelKeys } from "@main/level";
|
||||||
|
import type { UserPreferences } from "@types";
|
||||||
|
|
||||||
const publishNewRepacksNotification = async (
|
const publishNewRepacksNotification = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
@ -9,9 +10,12 @@ const publishNewRepacksNotification = async (
|
||||||
) => {
|
) => {
|
||||||
if (newRepacksCount < 1) return;
|
if (newRepacksCount < 1) return;
|
||||||
|
|
||||||
const userPreferences = await userPreferencesRepository.findOne({
|
const userPreferences = await db.get<string, UserPreferences>(
|
||||||
where: { id: 1 },
|
levelKeys.userPreferences,
|
||||||
});
|
{
|
||||||
|
valueEncoding: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (userPreferences?.repackUpdatesNotificationsEnabled) {
|
if (userPreferences?.repackUpdatesNotificationsEnabled) {
|
||||||
new Notification({
|
new Notification({
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { omit } from "lodash-es";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { fileTypeFromFile } from "file-type";
|
import { fileTypeFromFile } from "file-type";
|
||||||
|
|
||||||
const patchUserProfile = async (updateProfile: UpdateProfileRequest) => {
|
export const patchUserProfile = async (updateProfile: UpdateProfileRequest) => {
|
||||||
return HydraApi.patch<UserProfile>("/profile", updateProfile);
|
return HydraApi.patch<UserProfile>("/profile", updateProfile);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,31 +1,19 @@
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
import { DownloadManager } from "@main/services";
|
import { DownloadManager } from "@main/services";
|
||||||
import { dataSource } from "@main/data-source";
|
import { GameShop } from "@types";
|
||||||
import { DownloadQueue, Game } from "@main/entity";
|
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const cancelGameDownload = async (
|
const cancelGameDownload = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
) => {
|
) => {
|
||||||
await dataSource.transaction(async (transactionalEntityManager) => {
|
const downloadKey = levelKeys.game(shop, objectId);
|
||||||
await DownloadManager.cancelDownload(gameId);
|
|
||||||
|
|
||||||
await transactionalEntityManager.getRepository(DownloadQueue).delete({
|
await DownloadManager.cancelDownload(downloadKey);
|
||||||
game: { id: gameId },
|
|
||||||
});
|
|
||||||
|
|
||||||
await transactionalEntityManager.getRepository(Game).update(
|
await downloadsSublevel.del(downloadKey);
|
||||||
{
|
|
||||||
id: gameId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: "removed",
|
|
||||||
bytesDownloaded: 0,
|
|
||||||
progress: 0,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("cancelGameDownload", cancelGameDownload);
|
registerEvent("cancelGameDownload", cancelGameDownload);
|
||||||
|
|
|
@ -1,24 +1,27 @@
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
import { DownloadManager } from "@main/services";
|
import { DownloadManager } from "@main/services";
|
||||||
import { dataSource } from "@main/data-source";
|
import { GameShop } from "@types";
|
||||||
import { DownloadQueue, Game } from "@main/entity";
|
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const pauseGameDownload = async (
|
const pauseGameDownload = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
) => {
|
) => {
|
||||||
await dataSource.transaction(async (transactionalEntityManager) => {
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
await DownloadManager.pauseDownload();
|
|
||||||
|
|
||||||
await transactionalEntityManager.getRepository(DownloadQueue).delete({
|
const download = await downloadsSublevel.get(gameKey);
|
||||||
game: { id: gameId },
|
|
||||||
|
if (download) {
|
||||||
|
await DownloadManager.pauseDownload(gameKey);
|
||||||
|
|
||||||
|
await downloadsSublevel.put(gameKey, {
|
||||||
|
...download,
|
||||||
|
status: "paused",
|
||||||
|
queued: false,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
await transactionalEntityManager
|
|
||||||
.getRepository(Game)
|
|
||||||
.update({ id: gameId }, { status: "paused" });
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("pauseGameDownload", pauseGameDownload);
|
registerEvent("pauseGameDownload", pauseGameDownload);
|
||||||
|
|
|
@ -1,17 +1,24 @@
|
||||||
|
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { DownloadManager } from "@main/services";
|
import { DownloadManager } from "@main/services";
|
||||||
import { gameRepository } from "@main/repository";
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
const pauseGameSeed = async (
|
const pauseGameSeed = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
) => {
|
) => {
|
||||||
await gameRepository.update(gameId, {
|
const downloadKey = levelKeys.game(shop, objectId);
|
||||||
status: "complete",
|
const download = await downloadsSublevel.get(downloadKey);
|
||||||
|
|
||||||
|
if (!download) return;
|
||||||
|
|
||||||
|
await downloadsSublevel.put(downloadKey, {
|
||||||
|
...download,
|
||||||
shouldSeed: false,
|
shouldSeed: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
await DownloadManager.pauseSeeding(gameId);
|
await DownloadManager.pauseSeeding(downloadKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("pauseGameSeed", pauseGameSeed);
|
registerEvent("pauseGameSeed", pauseGameSeed);
|
||||||
|
|
|
@ -1,46 +1,37 @@
|
||||||
import { Not } from "typeorm";
|
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { gameRepository } from "../../repository";
|
|
||||||
|
|
||||||
import { DownloadManager } from "@main/services";
|
import { DownloadManager } from "@main/services";
|
||||||
import { dataSource } from "@main/data-source";
|
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||||
import { DownloadQueue, Game } from "@main/entity";
|
import { GameShop } from "@types";
|
||||||
|
|
||||||
const resumeGameDownload = async (
|
const resumeGameDownload = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
) => {
|
) => {
|
||||||
const game = await gameRepository.findOne({
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
where: {
|
|
||||||
id: gameId,
|
|
||||||
isDeleted: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!game) return;
|
const download = await downloadsSublevel.get(gameKey);
|
||||||
|
|
||||||
if (game.status === "paused") {
|
if (download?.status === "paused") {
|
||||||
await dataSource.transaction(async (transactionalEntityManager) => {
|
await DownloadManager.pauseDownload();
|
||||||
await DownloadManager.pauseDownload();
|
|
||||||
|
|
||||||
await transactionalEntityManager
|
for await (const [key, value] of downloadsSublevel.iterator()) {
|
||||||
.getRepository(Game)
|
if (value.status === "active" && value.progress !== 1) {
|
||||||
.update({ status: "active", progress: Not(1) }, { status: "paused" });
|
await downloadsSublevel.put(key, {
|
||||||
|
...value,
|
||||||
|
status: "paused",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await DownloadManager.resumeDownload(game);
|
await DownloadManager.resumeDownload(download);
|
||||||
|
|
||||||
await transactionalEntityManager
|
await downloadsSublevel.put(gameKey, {
|
||||||
.getRepository(DownloadQueue)
|
...download,
|
||||||
.delete({ game: { id: gameId } });
|
status: "active",
|
||||||
|
timestamp: Date.now(),
|
||||||
await transactionalEntityManager
|
queued: true,
|
||||||
.getRepository(DownloadQueue)
|
|
||||||
.insert({ game: { id: gameId } });
|
|
||||||
|
|
||||||
await transactionalEntityManager
|
|
||||||
.getRepository(Game)
|
|
||||||
.update({ id: gameId }, { status: "active" });
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,29 +1,23 @@
|
||||||
|
import { downloadsSublevel, levelKeys } from "@main/level";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { gameRepository } from "../../repository";
|
|
||||||
import { DownloadManager } from "@main/services";
|
import { DownloadManager } from "@main/services";
|
||||||
import { Downloader } from "@shared";
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
const resumeGameSeed = async (
|
const resumeGameSeed = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
gameId: number
|
shop: GameShop,
|
||||||
|
objectId: string
|
||||||
) => {
|
) => {
|
||||||
const game = await gameRepository.findOne({
|
const download = await downloadsSublevel.get(levelKeys.game(shop, objectId));
|
||||||
where: {
|
|
||||||
id: gameId,
|
|
||||||
isDeleted: false,
|
|
||||||
downloader: Downloader.Torrent,
|
|
||||||
progress: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!game) return;
|
if (!download) return;
|
||||||
|
|
||||||
await gameRepository.update(gameId, {
|
await downloadsSublevel.put(levelKeys.game(shop, objectId), {
|
||||||
status: "seeding",
|
...download,
|
||||||
shouldSeed: true,
|
shouldSeed: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
await DownloadManager.resumeSeeding(game);
|
await DownloadManager.resumeSeeding(download);
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("resumeGameSeed", resumeGameSeed);
|
registerEvent("resumeGameSeed", resumeGameSeed);
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import type { StartGameDownloadPayload } from "@types";
|
import type { Download, StartGameDownloadPayload } from "@types";
|
||||||
import { DownloadManager, HydraApi } from "@main/services";
|
import { DownloadManager, HydraApi } from "@main/services";
|
||||||
|
|
||||||
import { Not } from "typeorm";
|
|
||||||
import { steamGamesWorker } from "@main/workers";
|
import { steamGamesWorker } from "@main/workers";
|
||||||
import { createGame } from "@main/services/library-sync";
|
import { createGame } from "@main/services/library-sync";
|
||||||
import { steamUrlBuilder } from "@shared";
|
import { steamUrlBuilder } from "@shared";
|
||||||
import { dataSource } from "@main/data-source";
|
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||||
import { DownloadQueue, Game } from "@main/entity";
|
|
||||||
|
|
||||||
const startGameDownload = async (
|
const startGameDownload = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
@ -15,85 +13,85 @@ const startGameDownload = async (
|
||||||
) => {
|
) => {
|
||||||
const { objectId, title, shop, downloadPath, downloader, uri } = payload;
|
const { objectId, title, shop, downloadPath, downloader, uri } = payload;
|
||||||
|
|
||||||
return dataSource.transaction(async (transactionalEntityManager) => {
|
const gameKey = levelKeys.game(shop, objectId);
|
||||||
const gameRepository = transactionalEntityManager.getRepository(Game);
|
|
||||||
const downloadQueueRepository =
|
|
||||||
transactionalEntityManager.getRepository(DownloadQueue);
|
|
||||||
|
|
||||||
const game = await gameRepository.findOne({
|
await DownloadManager.pauseDownload();
|
||||||
where: {
|
|
||||||
objectID: objectId,
|
|
||||||
shop,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await DownloadManager.pauseDownload();
|
for await (const [key, value] of downloadsSublevel.iterator()) {
|
||||||
|
if (value.status === "active" && value.progress !== 1) {
|
||||||
await gameRepository.update(
|
await downloadsSublevel.put(key, {
|
||||||
{ status: "active", progress: Not(1) },
|
...value,
|
||||||
{ status: "paused" }
|
status: "paused",
|
||||||
);
|
|
||||||
|
|
||||||
if (game) {
|
|
||||||
await gameRepository.update(
|
|
||||||
{
|
|
||||||
id: game.id,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: "active",
|
|
||||||
progress: 0,
|
|
||||||
bytesDownloaded: 0,
|
|
||||||
downloadPath,
|
|
||||||
downloader,
|
|
||||||
uri,
|
|
||||||
isDeleted: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const steamGame = await steamGamesWorker.run(Number(objectId), {
|
|
||||||
name: "getById",
|
|
||||||
});
|
|
||||||
|
|
||||||
const iconUrl = steamGame?.clientIcon
|
|
||||||
? steamUrlBuilder.icon(objectId, steamGame.clientIcon)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
await gameRepository.insert({
|
|
||||||
title,
|
|
||||||
iconUrl,
|
|
||||||
objectID: objectId,
|
|
||||||
downloader,
|
|
||||||
shop,
|
|
||||||
status: "active",
|
|
||||||
downloadPath,
|
|
||||||
uri,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updatedGame = await gameRepository.findOne({
|
const game = await gamesSublevel.get(gameKey);
|
||||||
where: {
|
|
||||||
objectID: objectId,
|
/* Delete any previous download */
|
||||||
},
|
await downloadsSublevel.del(gameKey);
|
||||||
|
|
||||||
|
if (game?.isDeleted) {
|
||||||
|
await gamesSublevel.put(gameKey, {
|
||||||
|
...game,
|
||||||
|
isDeleted: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const steamGame = await steamGamesWorker.run(Number(objectId), {
|
||||||
|
name: "getById",
|
||||||
});
|
});
|
||||||
|
|
||||||
await DownloadManager.cancelDownload(updatedGame!.id);
|
const iconUrl = steamGame?.clientIcon
|
||||||
await DownloadManager.startDownload(updatedGame!);
|
? steamUrlBuilder.icon(objectId, steamGame.clientIcon)
|
||||||
|
: null;
|
||||||
|
|
||||||
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });
|
await gamesSublevel.put(gameKey, {
|
||||||
await downloadQueueRepository.insert({ game: { id: updatedGame!.id } });
|
title,
|
||||||
|
iconUrl,
|
||||||
|
objectId,
|
||||||
|
shop,
|
||||||
|
remoteId: null,
|
||||||
|
playTimeInMilliseconds: 0,
|
||||||
|
lastTimePlayed: null,
|
||||||
|
isDeleted: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all([
|
await DownloadManager.cancelDownload(gameKey);
|
||||||
createGame(updatedGame!).catch(() => {}),
|
|
||||||
HydraApi.post(
|
const download: Download = {
|
||||||
"/games/download",
|
shop,
|
||||||
{
|
objectId,
|
||||||
objectId: updatedGame!.objectID,
|
status: "active",
|
||||||
shop: updatedGame!.shop,
|
progress: 0,
|
||||||
},
|
bytesDownloaded: 0,
|
||||||
{ needsAuth: false }
|
downloadPath,
|
||||||
).catch(() => {}),
|
downloader,
|
||||||
]);
|
uri,
|
||||||
});
|
folderName: null,
|
||||||
|
fileSize: null,
|
||||||
|
shouldSeed: false,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
queued: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
await downloadsSublevel.put(gameKey, download);
|
||||||
|
|
||||||
|
await DownloadManager.startDownload(download);
|
||||||
|
|
||||||
|
const updatedGame = await gamesSublevel.get(gameKey);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
createGame(updatedGame!).catch(() => {}),
|
||||||
|
HydraApi.post(
|
||||||
|
"/games/download",
|
||||||
|
{
|
||||||
|
objectId,
|
||||||
|
shop,
|
||||||
|
},
|
||||||
|
{ needsAuth: false }
|
||||||
|
).catch(() => {}),
|
||||||
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("startGameDownload", startGameDownload);
|
registerEvent("startGameDownload", startGameDownload);
|
||||||
|
|
|
@ -1,9 +1,21 @@
|
||||||
import { userPreferencesRepository } from "@main/repository";
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
import { db, levelKeys } from "@main/level";
|
||||||
|
import { Crypto } from "@main/services";
|
||||||
|
import type { UserPreferences } from "@types";
|
||||||
|
|
||||||
const getUserPreferences = async () =>
|
const getUserPreferences = async () =>
|
||||||
userPreferencesRepository.findOne({
|
db
|
||||||
where: { id: 1 },
|
.get<string, UserPreferences>(levelKeys.userPreferences, {
|
||||||
});
|
valueEncoding: "json",
|
||||||
|
})
|
||||||
|
.then((userPreferences) => {
|
||||||
|
if (userPreferences.realDebridApiToken) {
|
||||||
|
userPreferences.realDebridApiToken = Crypto.decrypt(
|
||||||
|
userPreferences.realDebridApiToken
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return userPreferences;
|
||||||
|
});
|
||||||
|
|
||||||
registerEvent("getUserPreferences", getUserPreferences);
|
registerEvent("getUserPreferences", getUserPreferences);
|
||||||
|
|
|
@ -1,23 +1,37 @@
|
||||||
import { userPreferencesRepository } from "@main/repository";
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
|
|
||||||
import type { UserPreferences } from "@types";
|
import type { UserPreferences } from "@types";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
import { db, levelKeys } from "@main/level";
|
||||||
|
import { patchUserProfile } from "../profile/update-profile";
|
||||||
|
|
||||||
const updateUserPreferences = async (
|
const updateUserPreferences = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
preferences: Partial<UserPreferences>
|
preferences: Partial<UserPreferences>
|
||||||
) => {
|
) => {
|
||||||
|
const userPreferences = await db.get<string, UserPreferences>(
|
||||||
|
levelKeys.userPreferences,
|
||||||
|
{ valueEncoding: "json" }
|
||||||
|
);
|
||||||
|
|
||||||
if (preferences.language) {
|
if (preferences.language) {
|
||||||
|
await db.put<string, string>(levelKeys.language, preferences.language, {
|
||||||
|
valueEncoding: "utf-8",
|
||||||
|
});
|
||||||
|
|
||||||
i18next.changeLanguage(preferences.language);
|
i18next.changeLanguage(preferences.language);
|
||||||
|
patchUserProfile({ language: preferences.language }).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
return userPreferencesRepository.upsert(
|
await db.put<string, UserPreferences>(
|
||||||
|
levelKeys.userPreferences,
|
||||||
{
|
{
|
||||||
id: 1,
|
...userPreferences,
|
||||||
...preferences,
|
...preferences,
|
||||||
},
|
},
|
||||||
["id"]
|
{
|
||||||
|
valueEncoding: "json",
|
||||||
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import type { ComparedAchievements, GameShop } from "@types";
|
import type { ComparedAchievements, GameShop, UserPreferences } from "@types";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { userPreferencesRepository } from "@main/repository";
|
|
||||||
import { HydraApi } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
|
import { db, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const getComparedUnlockedAchievements = async (
|
const getComparedUnlockedAchievements = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
@ -9,9 +10,12 @@ const getComparedUnlockedAchievements = async (
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
userId: string
|
userId: string
|
||||||
) => {
|
) => {
|
||||||
const userPreferences = await userPreferencesRepository.findOne({
|
const userPreferences = await db.get<string, UserPreferences>(
|
||||||
where: { id: 1 },
|
levelKeys.userPreferences,
|
||||||
});
|
{
|
||||||
|
valueEncoding: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const showHiddenAchievementsDescription =
|
const showHiddenAchievementsDescription =
|
||||||
userPreferences?.showHiddenAchievementsDescription || false;
|
userPreferences?.showHiddenAchievementsDescription || false;
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
import type { GameShop, UnlockedAchievement, UserAchievement } from "@types";
|
import type { GameShop, UserAchievement, UserPreferences } from "@types";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import {
|
|
||||||
gameAchievementRepository,
|
|
||||||
userPreferencesRepository,
|
|
||||||
} from "@main/repository";
|
|
||||||
import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data";
|
import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data";
|
||||||
|
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
export const getUnlockedAchievements = async (
|
export const getUnlockedAchievements = async (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
useCachedData: boolean
|
useCachedData: boolean
|
||||||
): Promise<UserAchievement[]> => {
|
): Promise<UserAchievement[]> => {
|
||||||
const cachedAchievements = await gameAchievementRepository.findOne({
|
const cachedAchievements = await gameAchievementsSublevel.get(
|
||||||
where: { objectId, shop },
|
levelKeys.game(shop, objectId)
|
||||||
});
|
);
|
||||||
|
|
||||||
const userPreferences = await userPreferencesRepository.findOne({
|
const userPreferences = await db.get<string, UserPreferences>(
|
||||||
where: { id: 1 },
|
levelKeys.userPreferences,
|
||||||
});
|
{
|
||||||
|
valueEncoding: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const showHiddenAchievementsDescription =
|
const showHiddenAchievementsDescription =
|
||||||
userPreferences?.showHiddenAchievementsDescription || false;
|
userPreferences?.showHiddenAchievementsDescription || false;
|
||||||
|
@ -25,12 +25,10 @@ export const getUnlockedAchievements = async (
|
||||||
const achievementsData = await getGameAchievementData(
|
const achievementsData = await getGameAchievementData(
|
||||||
objectId,
|
objectId,
|
||||||
shop,
|
shop,
|
||||||
useCachedData ? cachedAchievements : null
|
useCachedData
|
||||||
);
|
);
|
||||||
|
|
||||||
const unlockedAchievements = JSON.parse(
|
const unlockedAchievements = cachedAchievements?.unlockedAchievements ?? [];
|
||||||
cachedAchievements?.unlockedAchievements || "[]"
|
|
||||||
) as UnlockedAchievement[];
|
|
||||||
|
|
||||||
return achievementsData
|
return achievementsData
|
||||||
.map((achievementData) => {
|
.map((achievementData) => {
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
import { userAuthRepository } from "@main/repository";
|
import { db } from "@main/level";
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { HydraApi } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
import type { UserFriends } from "@types";
|
import type { User, UserFriends } from "@types";
|
||||||
|
import { levelKeys } from "@main/level/sublevels";
|
||||||
|
|
||||||
export const getUserFriends = async (
|
export const getUserFriends = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
take: number,
|
take: number,
|
||||||
skip: number
|
skip: number
|
||||||
): Promise<UserFriends> => {
|
): Promise<UserFriends> => {
|
||||||
const loggedUser = await userAuthRepository.findOne({ where: { id: 1 } });
|
const user = await db.get<string, User>(levelKeys.user, {
|
||||||
|
valueEncoding: "json",
|
||||||
|
});
|
||||||
|
|
||||||
if (loggedUser?.userId === userId) {
|
if (user?.id === userId) {
|
||||||
return HydraApi.get(`/profile/friends`, { take, skip });
|
return HydraApi.get(`/profile/friends`, { take, skip });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,16 +3,13 @@ import updater from "electron-updater";
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import url from "node:url";
|
import url from "node:url";
|
||||||
import fs from "node:fs";
|
|
||||||
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
import { electronApp, optimizer } from "@electron-toolkit/utils";
|
||||||
import { logger, WindowManager } from "@main/services";
|
import { logger, WindowManager } from "@main/services";
|
||||||
import { dataSource } from "@main/data-source";
|
|
||||||
import resources from "@locales";
|
import resources from "@locales";
|
||||||
import { userPreferencesRepository } from "@main/repository";
|
|
||||||
import { knexClient, migrationConfig } from "./knex-client";
|
|
||||||
import { databaseDirectory } from "./constants";
|
|
||||||
import { PythonRPC } from "./services/python-rpc";
|
import { PythonRPC } from "./services/python-rpc";
|
||||||
import { Aria2 } from "./services/aria2";
|
import { Aria2 } from "./services/aria2";
|
||||||
|
import { db, levelKeys } from "./level";
|
||||||
|
import { loadState } from "./main";
|
||||||
|
|
||||||
const { autoUpdater } = updater;
|
const { autoUpdater } = updater;
|
||||||
|
|
||||||
|
@ -50,21 +47,6 @@ if (process.defaultApp) {
|
||||||
app.setAsDefaultProtocolClient(PROTOCOL);
|
app.setAsDefaultProtocolClient(PROTOCOL);
|
||||||
}
|
}
|
||||||
|
|
||||||
const runMigrations = async () => {
|
|
||||||
if (!fs.existsSync(databaseDirectory)) {
|
|
||||||
fs.mkdirSync(databaseDirectory, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
await knexClient.migrate.list(migrationConfig).then((result) => {
|
|
||||||
logger.log(
|
|
||||||
"Migrations to run:",
|
|
||||||
result[1].map((migration) => migration.name)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
await knexClient.migrate.latest(migrationConfig);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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.
|
||||||
|
@ -76,31 +58,19 @@ app.whenReady().then(async () => {
|
||||||
return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString());
|
return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
await runMigrations()
|
await loadState();
|
||||||
.then(() => {
|
|
||||||
logger.log("Migrations executed successfully");
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
logger.log("Migrations failed to run:", err);
|
|
||||||
});
|
|
||||||
|
|
||||||
await dataSource.initialize();
|
const language = await db.get<string, string>(levelKeys.language, {
|
||||||
|
valueEncoding: "utf-8",
|
||||||
await import("./main");
|
|
||||||
|
|
||||||
const userPreferences = await userPreferencesRepository.findOne({
|
|
||||||
where: { id: 1 },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (userPreferences?.language) {
|
if (language) i18n.changeLanguage(language);
|
||||||
i18n.changeLanguage(userPreferences.language);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!process.argv.includes("--hidden")) {
|
if (!process.argv.includes("--hidden")) {
|
||||||
WindowManager.createMainWindow();
|
WindowManager.createMainWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
WindowManager.createSystemTray(userPreferences?.language || "en");
|
WindowManager.createSystemTray(language || "en");
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("browser-window-created", (_, window) => {
|
app.on("browser-window-created", (_, window) => {
|
||||||
|
|
|
@ -1,55 +1,6 @@
|
||||||
import knex, { Knex } from "knex";
|
import knex from "knex";
|
||||||
import { databasePath } from "./constants";
|
import { databasePath } from "./constants";
|
||||||
import { Hydra2_0_3 } from "./migrations/20240830143811_Hydra_2_0_3";
|
|
||||||
import { RepackUris } from "./migrations/20240830143906_RepackUris";
|
|
||||||
import { UpdateUserLanguage } from "./migrations/20240913213944_update_user_language";
|
|
||||||
import { EnsureRepackUris } from "./migrations/20240915035339_ensure_repack_uris";
|
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import { FixMissingColumns } from "./migrations/20240918001920_FixMissingColumns";
|
|
||||||
import { CreateGameAchievement } from "./migrations/20240919030940_create_game_achievement";
|
|
||||||
import { AddAchievementNotificationPreference } from "./migrations/20241013012900_add_achievement_notification_preference";
|
|
||||||
import { CreateUserSubscription } from "./migrations/20241015235142_create_user_subscription";
|
|
||||||
import { AddBackgroundImageUrl } from "./migrations/20241016100249_add_background_image_url";
|
|
||||||
import { AddWinePrefixToGame } from "./migrations/20241019081648_add_wine_prefix_to_game";
|
|
||||||
import { AddStartMinimizedColumn } from "./migrations/20241030171454_add_start_minimized_column";
|
|
||||||
import { AddDisableNsfwAlertColumn } from "./migrations/20241106053733_add_disable_nsfw_alert_column";
|
|
||||||
import { AddShouldSeedColumn } from "./migrations/20241108200154_add_should_seed_colum";
|
|
||||||
import { AddSeedAfterDownloadColumn } from "./migrations/20241108201806_add_seed_after_download";
|
|
||||||
import { AddHiddenAchievementDescriptionColumn } from "./migrations/20241216140633_add_hidden_achievement_description_column ";
|
|
||||||
import { AddLaunchOptionsColumnToGame } from "./migrations/20241226044022_add_launch_options_column_to_game";
|
|
||||||
import { AddTorBoxApiToken } from "./migrations/20250111182229_add_torbox_api_token_column";
|
|
||||||
|
|
||||||
export type HydraMigration = Knex.Migration & { name: string };
|
|
||||||
|
|
||||||
class MigrationSource implements Knex.MigrationSource<HydraMigration> {
|
|
||||||
getMigrations(): Promise<HydraMigration[]> {
|
|
||||||
return Promise.resolve([
|
|
||||||
Hydra2_0_3,
|
|
||||||
RepackUris,
|
|
||||||
UpdateUserLanguage,
|
|
||||||
EnsureRepackUris,
|
|
||||||
FixMissingColumns,
|
|
||||||
CreateGameAchievement,
|
|
||||||
AddAchievementNotificationPreference,
|
|
||||||
CreateUserSubscription,
|
|
||||||
AddBackgroundImageUrl,
|
|
||||||
AddWinePrefixToGame,
|
|
||||||
AddStartMinimizedColumn,
|
|
||||||
AddDisableNsfwAlertColumn,
|
|
||||||
AddShouldSeedColumn,
|
|
||||||
AddSeedAfterDownloadColumn,
|
|
||||||
AddHiddenAchievementDescriptionColumn,
|
|
||||||
AddLaunchOptionsColumnToGame,
|
|
||||||
AddTorBoxApiToken,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
getMigrationName(migration: HydraMigration): string {
|
|
||||||
return migration.name;
|
|
||||||
}
|
|
||||||
getMigration(migration: HydraMigration): Promise<Knex.Migration> {
|
|
||||||
return Promise.resolve(migration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const knexClient = knex({
|
export const knexClient = knex({
|
||||||
debug: !app.isPackaged,
|
debug: !app.isPackaged,
|
||||||
|
@ -58,7 +9,3 @@ export const knexClient = knex({
|
||||||
filename: databasePath,
|
filename: databasePath,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const migrationConfig: Knex.MigratorConfig = {
|
|
||||||
migrationSource: new MigrationSource(),
|
|
||||||
};
|
|
||||||
|
|
3
src/main/level/index.ts
Normal file
3
src/main/level/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export { db } from "./level";
|
||||||
|
|
||||||
|
export * from "./sublevels";
|
6
src/main/level/level.ts
Normal file
6
src/main/level/level.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { levelDatabasePath } from "@main/constants";
|
||||||
|
import { ClassicLevel } from "classic-level";
|
||||||
|
|
||||||
|
export const db = new ClassicLevel(levelDatabasePath, {
|
||||||
|
valueEncoding: "json",
|
||||||
|
});
|
11
src/main/level/sublevels/downloads.ts
Normal file
11
src/main/level/sublevels/downloads.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import type { Download } from "@types";
|
||||||
|
|
||||||
|
import { db } from "../level";
|
||||||
|
import { levelKeys } from "./keys";
|
||||||
|
|
||||||
|
export const downloadsSublevel = db.sublevel<string, Download>(
|
||||||
|
levelKeys.downloads,
|
||||||
|
{
|
||||||
|
valueEncoding: "json",
|
||||||
|
}
|
||||||
|
);
|
11
src/main/level/sublevels/game-achievements.ts
Normal file
11
src/main/level/sublevels/game-achievements.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import type { GameAchievement } from "@types";
|
||||||
|
|
||||||
|
import { db } from "../level";
|
||||||
|
import { levelKeys } from "./keys";
|
||||||
|
|
||||||
|
export const gameAchievementsSublevel = db.sublevel<string, GameAchievement>(
|
||||||
|
levelKeys.gameAchievements,
|
||||||
|
{
|
||||||
|
valueEncoding: "json",
|
||||||
|
}
|
||||||
|
);
|
11
src/main/level/sublevels/game-shop-cache.ts
Normal file
11
src/main/level/sublevels/game-shop-cache.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import type { ShopDetails } from "@types";
|
||||||
|
|
||||||
|
import { db } from "../level";
|
||||||
|
import { levelKeys } from "./keys";
|
||||||
|
|
||||||
|
export const gamesShopCacheSublevel = db.sublevel<string, ShopDetails>(
|
||||||
|
levelKeys.gameShopCache,
|
||||||
|
{
|
||||||
|
valueEncoding: "json",
|
||||||
|
}
|
||||||
|
);
|
8
src/main/level/sublevels/games.ts
Normal file
8
src/main/level/sublevels/games.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import type { Game } from "@types";
|
||||||
|
|
||||||
|
import { db } from "../level";
|
||||||
|
import { levelKeys } from "./keys";
|
||||||
|
|
||||||
|
export const gamesSublevel = db.sublevel<string, Game>(levelKeys.games, {
|
||||||
|
valueEncoding: "json",
|
||||||
|
});
|
6
src/main/level/sublevels/index.ts
Normal file
6
src/main/level/sublevels/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export * from "./downloads";
|
||||||
|
export * from "./games";
|
||||||
|
export * from "./game-shop-cache";
|
||||||
|
export * from "./game-achievements";
|
||||||
|
|
||||||
|
export * from "./keys";
|
16
src/main/level/sublevels/keys.ts
Normal file
16
src/main/level/sublevels/keys.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import type { GameShop } from "@types";
|
||||||
|
|
||||||
|
export const levelKeys = {
|
||||||
|
games: "games",
|
||||||
|
game: (shop: GameShop, objectId: string) => `${shop}:${objectId}`,
|
||||||
|
user: "user",
|
||||||
|
auth: "auth",
|
||||||
|
gameShopCache: "gameShopCache",
|
||||||
|
gameShopCacheItem: (shop: GameShop, objectId: string, language: string) =>
|
||||||
|
`${shop}:${objectId}:${language}`,
|
||||||
|
gameAchievements: "gameAchievements",
|
||||||
|
downloads: "downloads",
|
||||||
|
userPreferences: "userPreferences",
|
||||||
|
language: "language",
|
||||||
|
sqliteMigrationDone: "sqliteMigrationDone",
|
||||||
|
};
|
218
src/main/main.ts
218
src/main/main.ts
|
@ -1,29 +1,45 @@
|
||||||
import { DownloadManager, Ludusavi, startMainLoop } from "./services";
|
|
||||||
import {
|
import {
|
||||||
downloadQueueRepository,
|
Crypto,
|
||||||
gameRepository,
|
DownloadManager,
|
||||||
userPreferencesRepository,
|
logger,
|
||||||
} from "./repository";
|
Ludusavi,
|
||||||
import { UserPreferences } from "./entity";
|
startMainLoop,
|
||||||
|
} from "./services";
|
||||||
import { RealDebridClient } from "./services/download/real-debrid";
|
import { RealDebridClient } from "./services/download/real-debrid";
|
||||||
import { HydraApi } from "./services/hydra-api";
|
import { HydraApi } from "./services/hydra-api";
|
||||||
import { uploadGamesBatch } from "./services/library-sync";
|
import { uploadGamesBatch } from "./services/library-sync";
|
||||||
import { Aria2 } from "./services/aria2";
|
import { Aria2 } from "./services/aria2";
|
||||||
|
import { downloadsSublevel } from "./level/sublevels/downloads";
|
||||||
|
import { sortBy } from "lodash-es";
|
||||||
import { Downloader } from "@shared";
|
import { Downloader } from "@shared";
|
||||||
import { IsNull, Not } from "typeorm";
|
import {
|
||||||
import { TorBoxClient } from "./services/download/torbox";
|
gameAchievementsSublevel,
|
||||||
|
gamesSublevel,
|
||||||
|
levelKeys,
|
||||||
|
db,
|
||||||
|
} from "./level";
|
||||||
|
import { Auth, User, type UserPreferences } from "@types";
|
||||||
|
import { knexClient } from "./knex-client";
|
||||||
|
|
||||||
const loadState = async (userPreferences: UserPreferences | null) => {
|
export const loadState = async () => {
|
||||||
import("./events");
|
const userPreferences = await migrateFromSqlite().then(async () => {
|
||||||
|
await db.put<string, boolean>(levelKeys.sqliteMigrationDone, true, {
|
||||||
|
valueEncoding: "json",
|
||||||
|
});
|
||||||
|
|
||||||
|
return db.get<string, UserPreferences>(levelKeys.userPreferences, {
|
||||||
|
valueEncoding: "json",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await import("./events");
|
||||||
|
|
||||||
Aria2.spawn();
|
Aria2.spawn();
|
||||||
|
|
||||||
if (userPreferences?.realDebridApiToken) {
|
if (userPreferences?.realDebridApiToken) {
|
||||||
RealDebridClient.authorize(userPreferences.realDebridApiToken);
|
RealDebridClient.authorize(
|
||||||
}
|
Crypto.decrypt(userPreferences.realDebridApiToken)
|
||||||
|
);
|
||||||
if (userPreferences?.torBoxApiToken) {
|
|
||||||
TorBoxClient.authorize(userPreferences?.torBoxApiToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ludusavi.addManifestToLudusaviConfig();
|
Ludusavi.addManifestToLudusaviConfig();
|
||||||
|
@ -32,33 +48,157 @@ const loadState = async (userPreferences: UserPreferences | null) => {
|
||||||
uploadGamesBatch();
|
uploadGamesBatch();
|
||||||
});
|
});
|
||||||
|
|
||||||
const [nextQueueItem] = await downloadQueueRepository.find({
|
const downloads = await downloadsSublevel
|
||||||
order: {
|
.values()
|
||||||
id: "DESC",
|
.all()
|
||||||
},
|
.then((games) => {
|
||||||
relations: {
|
return sortBy(
|
||||||
game: true,
|
games.filter((game) => game.queued),
|
||||||
},
|
"timestamp",
|
||||||
});
|
"DESC"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const seedList = await gameRepository.find({
|
const [nextItemOnQueue] = downloads;
|
||||||
where: {
|
|
||||||
shouldSeed: true,
|
|
||||||
downloader: Downloader.Torrent,
|
|
||||||
progress: 1,
|
|
||||||
uri: Not(IsNull()),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await DownloadManager.startRPC(nextQueueItem?.game, seedList);
|
const downloadsToSeed = downloads.filter(
|
||||||
|
(download) =>
|
||||||
|
download.shouldSeed &&
|
||||||
|
download.downloader === Downloader.Torrent &&
|
||||||
|
download.progress === 1 &&
|
||||||
|
download.uri !== null
|
||||||
|
);
|
||||||
|
|
||||||
|
await DownloadManager.startRPC(nextItemOnQueue, downloadsToSeed);
|
||||||
|
|
||||||
startMainLoop();
|
startMainLoop();
|
||||||
};
|
};
|
||||||
|
|
||||||
userPreferencesRepository
|
const migrateFromSqlite = async () => {
|
||||||
.findOne({
|
const sqliteMigrationDone = await db.get(levelKeys.sqliteMigrationDone);
|
||||||
where: { id: 1 },
|
|
||||||
})
|
if (sqliteMigrationDone) {
|
||||||
.then((userPreferences) => {
|
return;
|
||||||
loadState(userPreferences);
|
}
|
||||||
});
|
|
||||||
|
const migrateGames = knexClient("game")
|
||||||
|
.select("*")
|
||||||
|
.then((games) => {
|
||||||
|
return gamesSublevel.batch(
|
||||||
|
games.map((game) => ({
|
||||||
|
type: "put",
|
||||||
|
key: levelKeys.game(game.shop, game.objectID),
|
||||||
|
value: {
|
||||||
|
objectId: game.objectID,
|
||||||
|
shop: game.shop,
|
||||||
|
title: game.title,
|
||||||
|
iconUrl: game.iconUrl,
|
||||||
|
playTimeInMilliseconds: game.playTimeInMilliseconds,
|
||||||
|
lastTimePlayed: game.lastTimePlayed,
|
||||||
|
remoteId: game.remoteId,
|
||||||
|
winePrefixPath: game.winePrefixPath,
|
||||||
|
launchOptions: game.launchOptions,
|
||||||
|
executablePath: game.executablePath,
|
||||||
|
isDeleted: game.isDeleted === 1,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
logger.info("Games migrated successfully");
|
||||||
|
});
|
||||||
|
|
||||||
|
const migrateUserPreferences = knexClient("user_preferences")
|
||||||
|
.select("*")
|
||||||
|
.then(async (userPreferences) => {
|
||||||
|
if (userPreferences.length > 0) {
|
||||||
|
const { realDebridApiToken, ...rest } = userPreferences[0];
|
||||||
|
|
||||||
|
await db.put(levelKeys.userPreferences, {
|
||||||
|
...rest,
|
||||||
|
realDebridApiToken: realDebridApiToken
|
||||||
|
? Crypto.encrypt(realDebridApiToken)
|
||||||
|
: null,
|
||||||
|
preferQuitInsteadOfHiding: rest.preferQuitInsteadOfHiding === 1,
|
||||||
|
runAtStartup: rest.runAtStartup === 1,
|
||||||
|
startMinimized: rest.startMinimized === 1,
|
||||||
|
disableNsfwAlert: rest.disableNsfwAlert === 1,
|
||||||
|
seedAfterDownloadComplete: rest.seedAfterDownloadComplete === 1,
|
||||||
|
showHiddenAchievementsDescription:
|
||||||
|
rest.showHiddenAchievementsDescription === 1,
|
||||||
|
downloadNotificationsEnabled: rest.downloadNotificationsEnabled === 1,
|
||||||
|
repackUpdatesNotificationsEnabled:
|
||||||
|
rest.repackUpdatesNotificationsEnabled === 1,
|
||||||
|
achievementNotificationsEnabled:
|
||||||
|
rest.achievementNotificationsEnabled === 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rest.language) {
|
||||||
|
await db.put(levelKeys.language, rest.language);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
logger.info("User preferences migrated successfully");
|
||||||
|
});
|
||||||
|
|
||||||
|
const migrateAchievements = knexClient("game_achievement")
|
||||||
|
.select("*")
|
||||||
|
.then((achievements) => {
|
||||||
|
return gameAchievementsSublevel.batch(
|
||||||
|
achievements.map((achievement) => ({
|
||||||
|
type: "put",
|
||||||
|
key: levelKeys.game(achievement.shop, achievement.objectId),
|
||||||
|
value: {
|
||||||
|
achievements: JSON.parse(achievement.achievements),
|
||||||
|
unlockedAchievements: JSON.parse(achievement.unlockedAchievements),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
logger.info("Achievements migrated successfully");
|
||||||
|
});
|
||||||
|
|
||||||
|
const migrateUser = knexClient("user_auth")
|
||||||
|
.select("*")
|
||||||
|
.then(async (users) => {
|
||||||
|
if (users.length > 0) {
|
||||||
|
await db.put<string, User>(
|
||||||
|
levelKeys.user,
|
||||||
|
{
|
||||||
|
id: users[0].userId,
|
||||||
|
displayName: users[0].displayName,
|
||||||
|
profileImageUrl: users[0].profileImageUrl,
|
||||||
|
backgroundImageUrl: users[0].backgroundImageUrl,
|
||||||
|
subscription: users[0].subscription,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
valueEncoding: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.put<string, Auth>(
|
||||||
|
levelKeys.auth,
|
||||||
|
{
|
||||||
|
accessToken: Crypto.encrypt(users[0].accessToken),
|
||||||
|
refreshToken: Crypto.encrypt(users[0].refreshToken),
|
||||||
|
tokenExpirationTimestamp: users[0].tokenExpirationTimestamp,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
valueEncoding: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
logger.info("User data migrated successfully");
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.allSettled([
|
||||||
|
migrateGames,
|
||||||
|
migrateUserPreferences,
|
||||||
|
migrateAchievements,
|
||||||
|
migrateUser,
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
|
@ -1,171 +0,0 @@
|
||||||
import type { HydraMigration } from "@main/knex-client";
|
|
||||||
import type { Knex } from "knex";
|
|
||||||
|
|
||||||
export const Hydra2_0_3: HydraMigration = {
|
|
||||||
name: "Hydra_2_0_3",
|
|
||||||
up: async (knex: Knex) => {
|
|
||||||
const timestamp = new Date().getTime();
|
|
||||||
|
|
||||||
await knex.schema.hasTable("migrations").then(async (exists) => {
|
|
||||||
if (exists) {
|
|
||||||
await knex.schema.dropTable("migrations");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await knex.schema.hasTable("download_source").then(async (exists) => {
|
|
||||||
if (!exists) {
|
|
||||||
await knex.schema.createTable("download_source", (table) => {
|
|
||||||
table.increments("id").primary();
|
|
||||||
table
|
|
||||||
.text("url")
|
|
||||||
.unique({ indexName: "download_source_url_unique_" + timestamp });
|
|
||||||
table.text("name").notNullable();
|
|
||||||
table.text("etag");
|
|
||||||
table.integer("downloadCount").notNullable().defaultTo(0);
|
|
||||||
table.text("status").notNullable().defaultTo(0);
|
|
||||||
table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
|
|
||||||
table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await knex.schema.hasTable("repack").then(async (exists) => {
|
|
||||||
if (!exists) {
|
|
||||||
await knex.schema.createTable("repack", (table) => {
|
|
||||||
table.increments("id").primary();
|
|
||||||
table
|
|
||||||
.text("title")
|
|
||||||
.notNullable()
|
|
||||||
.unique({ indexName: "repack_title_unique_" + timestamp });
|
|
||||||
table
|
|
||||||
.text("magnet")
|
|
||||||
.notNullable()
|
|
||||||
.unique({ indexName: "repack_magnet_unique_" + timestamp });
|
|
||||||
table.integer("page");
|
|
||||||
table.text("repacker").notNullable();
|
|
||||||
table.text("fileSize").notNullable();
|
|
||||||
table.datetime("uploadDate").notNullable();
|
|
||||||
table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
|
|
||||||
table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
|
|
||||||
table
|
|
||||||
.integer("downloadSourceId")
|
|
||||||
.references("download_source.id")
|
|
||||||
.onDelete("CASCADE");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await knex.schema.hasTable("game").then(async (exists) => {
|
|
||||||
if (!exists) {
|
|
||||||
await knex.schema.createTable("game", (table) => {
|
|
||||||
table.increments("id").primary();
|
|
||||||
table
|
|
||||||
.text("objectID")
|
|
||||||
.notNullable()
|
|
||||||
.unique({ indexName: "game_objectID_unique_" + timestamp });
|
|
||||||
table
|
|
||||||
.text("remoteId")
|
|
||||||
.unique({ indexName: "game_remoteId_unique_" + timestamp });
|
|
||||||
table.text("title").notNullable();
|
|
||||||
table.text("iconUrl");
|
|
||||||
table.text("folderName");
|
|
||||||
table.text("downloadPath");
|
|
||||||
table.text("executablePath");
|
|
||||||
table.integer("playTimeInMilliseconds").notNullable().defaultTo(0);
|
|
||||||
table.text("shop").notNullable();
|
|
||||||
table.text("status");
|
|
||||||
table.integer("downloader").notNullable().defaultTo(1);
|
|
||||||
table.float("progress").notNullable().defaultTo(0);
|
|
||||||
table.integer("bytesDownloaded").notNullable().defaultTo(0);
|
|
||||||
table.datetime("lastTimePlayed");
|
|
||||||
table.float("fileSize").notNullable().defaultTo(0);
|
|
||||||
table.text("uri");
|
|
||||||
table.boolean("isDeleted").notNullable().defaultTo(0);
|
|
||||||
table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
|
|
||||||
table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
|
|
||||||
table
|
|
||||||
.integer("repackId")
|
|
||||||
.references("repack.id")
|
|
||||||
.unique("repack_repackId_unique_" + timestamp);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await knex.schema.hasTable("user_preferences").then(async (exists) => {
|
|
||||||
if (!exists) {
|
|
||||||
await knex.schema.createTable("user_preferences", (table) => {
|
|
||||||
table.increments("id").primary();
|
|
||||||
table.text("downloadsPath");
|
|
||||||
table.text("language").notNullable().defaultTo("en");
|
|
||||||
table.text("realDebridApiToken");
|
|
||||||
table
|
|
||||||
.boolean("downloadNotificationsEnabled")
|
|
||||||
.notNullable()
|
|
||||||
.defaultTo(0);
|
|
||||||
table
|
|
||||||
.boolean("repackUpdatesNotificationsEnabled")
|
|
||||||
.notNullable()
|
|
||||||
.defaultTo(0);
|
|
||||||
table.boolean("preferQuitInsteadOfHiding").notNullable().defaultTo(0);
|
|
||||||
table.boolean("runAtStartup").notNullable().defaultTo(0);
|
|
||||||
table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
|
|
||||||
table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await knex.schema.hasTable("game_shop_cache").then(async (exists) => {
|
|
||||||
if (!exists) {
|
|
||||||
await knex.schema.createTable("game_shop_cache", (table) => {
|
|
||||||
table.text("objectID").primary().notNullable();
|
|
||||||
table.text("shop").notNullable();
|
|
||||||
table.text("serializedData");
|
|
||||||
table.text("howLongToBeatSerializedData");
|
|
||||||
table.text("language");
|
|
||||||
table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
|
|
||||||
table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await knex.schema.hasTable("download_queue").then(async (exists) => {
|
|
||||||
if (!exists) {
|
|
||||||
await knex.schema.createTable("download_queue", (table) => {
|
|
||||||
table.increments("id").primary();
|
|
||||||
table
|
|
||||||
.integer("gameId")
|
|
||||||
.references("game.id")
|
|
||||||
.unique("download_queue_gameId_unique_" + timestamp);
|
|
||||||
table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
|
|
||||||
table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await knex.schema.hasTable("user_auth").then(async (exists) => {
|
|
||||||
if (!exists) {
|
|
||||||
await knex.schema.createTable("user_auth", (table) => {
|
|
||||||
table.increments("id").primary();
|
|
||||||
table.text("userId").notNullable().defaultTo("");
|
|
||||||
table.text("displayName").notNullable().defaultTo("");
|
|
||||||
table.text("profileImageUrl");
|
|
||||||
table.text("accessToken").notNullable().defaultTo("");
|
|
||||||
table.text("refreshToken").notNullable().defaultTo("");
|
|
||||||
table.integer("tokenExpirationTimestamp").notNullable().defaultTo(0);
|
|
||||||
table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
|
|
||||||
table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
down: async (knex: Knex) => {
|
|
||||||
await knex.schema.dropTableIfExists("game");
|
|
||||||
await knex.schema.dropTableIfExists("repack");
|
|
||||||
await knex.schema.dropTableIfExists("download_queue");
|
|
||||||
await knex.schema.dropTableIfExists("user_auth");
|
|
||||||
await knex.schema.dropTableIfExists("game_shop_cache");
|
|
||||||
await knex.schema.dropTableIfExists("user_preferences");
|
|
||||||
await knex.schema.dropTableIfExists("download_source");
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,18 +0,0 @@
|
||||||
import type { HydraMigration } from "@main/knex-client";
|
|
||||||
import type { Knex } from "knex";
|
|
||||||
|
|
||||||
export const RepackUris: HydraMigration = {
|
|
||||||
name: "RepackUris",
|
|
||||||
up: async (knex: Knex) => {
|
|
||||||
await knex.schema.alterTable("repack", (table) => {
|
|
||||||
table.text("uris").notNullable().defaultTo("[]");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
down: async (knex: Knex) => {
|
|
||||||
await knex.schema.alterTable("repack", (table) => {
|
|
||||||
table.integer("page");
|
|
||||||
table.dropColumn("uris");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,13 +0,0 @@
|
||||||
import type { HydraMigration } from "@main/knex-client";
|
|
||||||
import type { Knex } from "knex";
|
|
||||||
|
|
||||||
export const UpdateUserLanguage: HydraMigration = {
|
|
||||||
name: "UpdateUserLanguage",
|
|
||||||
up: async (knex: Knex) => {
|
|
||||||
await knex("user_preferences")
|
|
||||||
.update("language", "pt-BR")
|
|
||||||
.where("language", "pt");
|
|
||||||
},
|
|
||||||
|
|
||||||
down: async (_knex: Knex) => {},
|
|
||||||
};
|
|
|
@ -1,17 +0,0 @@
|
||||||
import type { HydraMigration } from "@main/knex-client";
|
|
||||||
import type { Knex } from "knex";
|
|
||||||
|
|
||||||
export const EnsureRepackUris: HydraMigration = {
|
|
||||||
name: "EnsureRepackUris",
|
|
||||||
up: async (knex: Knex) => {
|
|
||||||
await knex.schema.hasColumn("repack", "uris").then(async (exists) => {
|
|
||||||
if (!exists) {
|
|
||||||
await knex.schema.table("repack", (table) => {
|
|
||||||
table.text("uris").notNullable().defaultTo("[]");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
down: async (_knex: Knex) => {},
|
|
||||||
};
|
|
|
@ -1,41 +0,0 @@
|
||||||
import type { HydraMigration } from "@main/knex-client";
|
|
||||||
import type { Knex } from "knex";
|
|
||||||
|
|
||||||
export const FixMissingColumns: HydraMigration = {
|
|
||||||
name: "FixMissingColumns",
|
|
||||||
up: async (knex: Knex) => {
|
|
||||||
const timestamp = new Date().getTime();
|
|
||||||
await knex.schema
|
|
||||||
.hasColumn("repack", "downloadSourceId")
|
|
||||||
.then(async (exists) => {
|
|
||||||
if (!exists) {
|
|
||||||
await knex.schema.table("repack", (table) => {
|
|
||||||
table
|
|
||||||
.integer("downloadSourceId")
|
|
||||||
.references("download_source.id")
|
|
||||||
.onDelete("CASCADE");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await knex.schema.hasColumn("game", "remoteId").then(async (exists) => {
|
|
||||||
if (!exists) {
|
|
||||||
await knex.schema.table("game", (table) => {
|
|
||||||
table
|
|
||||||
.text("remoteId")
|
|
||||||
.unique({ indexName: "game_remoteId_unique_" + timestamp });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await knex.schema.hasColumn("game", "uri").then(async (exists) => {
|
|
||||||
if (!exists) {
|
|
||||||
await knex.schema.table("game", (table) => {
|
|
||||||
table.text("uri");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
down: async (_knex: Knex) => {},
|
|
||||||
};
|
|
|
@ -1,20 +0,0 @@
|
||||||
import type { HydraMigration } from "@main/knex-client";
|
|
||||||
import type { Knex } from "knex";
|
|
||||||
|
|
||||||
export const CreateGameAchievement: HydraMigration = {
|
|
||||||
name: "CreateGameAchievement",
|
|
||||||
up: (knex: Knex) => {
|
|
||||||
return knex.schema.createTable("game_achievement", (table) => {
|
|
||||||
table.increments("id").primary();
|
|
||||||
table.text("objectId").notNullable();
|
|
||||||
table.text("shop").notNullable();
|
|
||||||
table.text("achievements");
|
|
||||||
table.text("unlockedAchievements");
|
|
||||||
table.unique(["objectId", "shop"]);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
down: (knex: Knex) => {
|
|
||||||
return knex.schema.dropTable("game_achievement");
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,17 +0,0 @@
|
||||||
import type { HydraMigration } from "@main/knex-client";
|
|
||||||
import type { Knex } from "knex";
|
|
||||||
|
|
||||||
export const AddAchievementNotificationPreference: HydraMigration = {
|
|
||||||
name: "AddAchievementNotificationPreference",
|
|
||||||
up: (knex: Knex) => {
|
|
||||||
return knex.schema.alterTable("user_preferences", (table) => {
|
|
||||||
return table.boolean("achievementNotificationsEnabled").defaultTo(true);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
down: (knex: Knex) => {
|
|
||||||
return knex.schema.alterTable("user_preferences", (table) => {
|
|
||||||
return table.dropColumn("achievementNotificationsEnabled");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,27 +0,0 @@
|
||||||
import type { HydraMigration } from "@main/knex-client";
|
|
||||||
import type { Knex } from "knex";
|
|
||||||
|
|
||||||
export const CreateUserSubscription: HydraMigration = {
|
|
||||||
name: "CreateUserSubscription",
|
|
||||||
up: async (knex: Knex) => {
|
|
||||||
return knex.schema.createTable("user_subscription", (table) => {
|
|
||||||
table.increments("id").primary();
|
|
||||||
table.string("subscriptionId").defaultTo("");
|
|
||||||
table
|
|
||||||
.text("userId")
|
|
||||||
.notNullable()
|
|
||||||
.references("user_auth.id")
|
|
||||||
.onDelete("CASCADE");
|
|
||||||
table.string("status").defaultTo("");
|
|
||||||
table.string("planId").defaultTo("");
|
|
||||||
table.string("planName").defaultTo("");
|
|
||||||
table.dateTime("expiresAt").nullable();
|
|
||||||
table.dateTime("createdAt").defaultTo(knex.fn.now());
|
|
||||||
table.dateTime("updatedAt").defaultTo(knex.fn.now());
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
down: async (knex: Knex) => {
|
|
||||||
return knex.schema.dropTable("user_subscription");
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,17 +0,0 @@
|
||||||
import type { HydraMigration } from "@main/knex-client";
|
|
||||||
import type { Knex } from "knex";
|
|
||||||
|
|
||||||
export const AddBackgroundImageUrl: HydraMigration = {
|
|
||||||
name: "AddBackgroundImageUrl",
|
|
||||||
up: (knex: Knex) => {
|
|
||||||
return knex.schema.alterTable("user_auth", (table) => {
|
|
||||||
return table.text("backgroundImageUrl").nullable();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
down: async (knex: Knex) => {
|
|
||||||
return knex.schema.alterTable("user_auth", (table) => {
|
|
||||||
return table.dropColumn("backgroundImageUrl");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,17 +0,0 @@
|
||||||
import type { HydraMigration } from "@main/knex-client";
|
|
||||||
import type { Knex } from "knex";
|
|
||||||
|
|
||||||
export const AddWinePrefixToGame: HydraMigration = {
|
|
||||||
name: "AddWinePrefixToGame",
|
|
||||||
up: (knex: Knex) => {
|
|
||||||
return knex.schema.alterTable("game", (table) => {
|
|
||||||
return table.text("winePrefixPath").nullable();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
down: async (knex: Knex) => {
|
|
||||||
return knex.schema.alterTable("game", (table) => {
|
|
||||||
return table.dropColumn("winePrefixPath");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,17 +0,0 @@
|
||||||
import type { HydraMigration } from "@main/knex-client";
|
|
||||||
import type { Knex } from "knex";
|
|
||||||
|
|
||||||
export const AddStartMinimizedColumn: HydraMigration = {
|
|
||||||
name: "AddStartMinimizedColumn",
|
|
||||||
up: (knex: Knex) => {
|
|
||||||
return knex.schema.alterTable("user_preferences", (table) => {
|
|
||||||
return table.boolean("startMinimized").notNullable().defaultTo(0);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
down: async (knex: Knex) => {
|
|
||||||
return knex.schema.alterTable("user_preferences", (table) => {
|
|
||||||
return table.dropColumn("startMinimized");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,17 +0,0 @@
|
||||||
import type { HydraMigration } from "@main/knex-client";
|
|
||||||
import type { Knex } from "knex";
|
|
||||||
|
|
||||||
export const AddDisableNsfwAlertColumn: HydraMigration = {
|
|
||||||
name: "AddDisableNsfwAlertColumn",
|
|
||||||
up: (knex: Knex) => {
|
|
||||||
return knex.schema.alterTable("user_preferences", (table) => {
|
|
||||||
return table.boolean("disableNsfwAlert").notNullable().defaultTo(0);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
down: async (knex: Knex) => {
|
|
||||||
return knex.schema.alterTable("user_preferences", (table) => {
|
|
||||||
return table.dropColumn("disableNsfwAlert");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,17 +0,0 @@
|
||||||
import type { HydraMigration } from "@main/knex-client";
|
|
||||||
import type { Knex } from "knex";
|
|
||||||
|
|
||||||
export const AddShouldSeedColumn: HydraMigration = {
|
|
||||||
name: "AddShouldSeedColumn",
|
|
||||||
up: (knex: Knex) => {
|
|
||||||
return knex.schema.alterTable("game", (table) => {
|
|
||||||
return table.boolean("shouldSeed").notNullable().defaultTo(true);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
down: async (knex: Knex) => {
|
|
||||||
return knex.schema.alterTable("game", (table) => {
|
|
||||||
return table.dropColumn("shouldSeed");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,20 +0,0 @@
|
||||||
import type { HydraMigration } from "@main/knex-client";
|
|
||||||
import type { Knex } from "knex";
|
|
||||||
|
|
||||||
export const AddSeedAfterDownloadColumn: HydraMigration = {
|
|
||||||
name: "AddSeedAfterDownloadColumn",
|
|
||||||
up: (knex: Knex) => {
|
|
||||||
return knex.schema.alterTable("user_preferences", (table) => {
|
|
||||||
return table
|
|
||||||
.boolean("seedAfterDownloadComplete")
|
|
||||||
.notNullable()
|
|
||||||
.defaultTo(true);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
down: async (knex: Knex) => {
|
|
||||||
return knex.schema.alterTable("user_preferences", (table) => {
|
|
||||||
return table.dropColumn("seedAfterDownloadComplete");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,20 +0,0 @@
|
||||||
import type { HydraMigration } from "@main/knex-client";
|
|
||||||
import type { Knex } from "knex";
|
|
||||||
|
|
||||||
export const AddHiddenAchievementDescriptionColumn: HydraMigration = {
|
|
||||||
name: "AddHiddenAchievementDescriptionColumn",
|
|
||||||
up: (knex: Knex) => {
|
|
||||||
return knex.schema.alterTable("user_preferences", (table) => {
|
|
||||||
return table
|
|
||||||
.boolean("showHiddenAchievementsDescription")
|
|
||||||
.notNullable()
|
|
||||||
.defaultTo(0);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
down: async (knex: Knex) => {
|
|
||||||
return knex.schema.alterTable("user_preferences", (table) => {
|
|
||||||
return table.dropColumn("showHiddenAchievementsDescription");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,17 +0,0 @@
|
||||||
import type { HydraMigration } from "@main/knex-client";
|
|
||||||
import type { Knex } from "knex";
|
|
||||||
|
|
||||||
export const AddLaunchOptionsColumnToGame: HydraMigration = {
|
|
||||||
name: "AddLaunchOptionsColumnToGame",
|
|
||||||
up: (knex: Knex) => {
|
|
||||||
return knex.schema.alterTable("game", (table) => {
|
|
||||||
return table.string("launchOptions").nullable();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
down: async (knex: Knex) => {
|
|
||||||
return knex.schema.alterTable("game", (table) => {
|
|
||||||
return table.dropColumn("launchOptions");
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,11 +0,0 @@
|
||||||
import type { HydraMigration } from "@main/knex-client";
|
|
||||||
import type { Knex } from "knex";
|
|
||||||
|
|
||||||
export const MigrationName: HydraMigration = {
|
|
||||||
name: "MigrationName",
|
|
||||||
up: (knex: Knex) => {
|
|
||||||
return knex.schema.createTable("table_name", async (table) => {});
|
|
||||||
},
|
|
||||||
|
|
||||||
down: async (knex: Knex) => {},
|
|
||||||
};
|
|
|
@ -1,27 +0,0 @@
|
||||||
import { dataSource } from "./data-source";
|
|
||||||
import {
|
|
||||||
DownloadQueue,
|
|
||||||
Game,
|
|
||||||
GameShopCache,
|
|
||||||
UserPreferences,
|
|
||||||
UserAuth,
|
|
||||||
GameAchievement,
|
|
||||||
UserSubscription,
|
|
||||||
} from "@main/entity";
|
|
||||||
|
|
||||||
export const gameRepository = dataSource.getRepository(Game);
|
|
||||||
|
|
||||||
export const userPreferencesRepository =
|
|
||||||
dataSource.getRepository(UserPreferences);
|
|
||||||
|
|
||||||
export const gameShopCacheRepository = dataSource.getRepository(GameShopCache);
|
|
||||||
|
|
||||||
export const downloadQueueRepository = dataSource.getRepository(DownloadQueue);
|
|
||||||
|
|
||||||
export const userAuthRepository = dataSource.getRepository(UserAuth);
|
|
||||||
|
|
||||||
export const userSubscriptionRepository =
|
|
||||||
dataSource.getRepository(UserSubscription);
|
|
||||||
|
|
||||||
export const gameAchievementRepository =
|
|
||||||
dataSource.getRepository(GameAchievement);
|
|
|
@ -1,6 +1,4 @@
|
||||||
import { gameRepository } from "@main/repository";
|
|
||||||
import { parseAchievementFile } from "./parse-achievement-file";
|
import { parseAchievementFile } from "./parse-achievement-file";
|
||||||
import { Game } from "@main/entity";
|
|
||||||
import { mergeAchievements } from "./merge-achievements";
|
import { mergeAchievements } from "./merge-achievements";
|
||||||
import fs, { readdirSync } from "node:fs";
|
import fs, { readdirSync } from "node:fs";
|
||||||
import {
|
import {
|
||||||
|
@ -9,21 +7,20 @@ import {
|
||||||
findAllAchievementFiles,
|
findAllAchievementFiles,
|
||||||
getAlternativeObjectIds,
|
getAlternativeObjectIds,
|
||||||
} from "./find-achivement-files";
|
} from "./find-achivement-files";
|
||||||
import type { AchievementFile, UnlockedAchievement } from "@types";
|
import type { AchievementFile, Game, UnlockedAchievement } from "@types";
|
||||||
import { achievementsLogger } from "../logger";
|
import { achievementsLogger } from "../logger";
|
||||||
import { Cracker } from "@shared";
|
import { Cracker } from "@shared";
|
||||||
import { IsNull, Not } from "typeorm";
|
|
||||||
import { publishCombinedNewAchievementNotification } from "../notifications";
|
import { publishCombinedNewAchievementNotification } from "../notifications";
|
||||||
|
import { gamesSublevel } from "@main/level";
|
||||||
|
|
||||||
const fileStats: Map<string, number> = new Map();
|
const fileStats: Map<string, number> = new Map();
|
||||||
const fltFiles: Map<string, Set<string>> = new Map();
|
const fltFiles: Map<string, Set<string>> = new Map();
|
||||||
|
|
||||||
const watchAchievementsWindows = async () => {
|
const watchAchievementsWindows = async () => {
|
||||||
const games = await gameRepository.find({
|
const games = await gamesSublevel
|
||||||
where: {
|
.values()
|
||||||
isDeleted: false,
|
.all()
|
||||||
},
|
.then((games) => games.filter((game) => !game.isDeleted));
|
||||||
});
|
|
||||||
|
|
||||||
if (games.length === 0) return;
|
if (games.length === 0) return;
|
||||||
|
|
||||||
|
@ -32,7 +29,7 @@ const watchAchievementsWindows = async () => {
|
||||||
for (const game of games) {
|
for (const game of games) {
|
||||||
const gameAchievementFiles: AchievementFile[] = [];
|
const gameAchievementFiles: AchievementFile[] = [];
|
||||||
|
|
||||||
for (const objectId of getAlternativeObjectIds(game.objectID)) {
|
for (const objectId of getAlternativeObjectIds(game.objectId)) {
|
||||||
gameAchievementFiles.push(...(achievementFiles.get(objectId) || []));
|
gameAchievementFiles.push(...(achievementFiles.get(objectId) || []));
|
||||||
|
|
||||||
gameAchievementFiles.push(
|
gameAchievementFiles.push(
|
||||||
|
@ -47,12 +44,12 @@ const watchAchievementsWindows = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const watchAchievementsWithWine = async () => {
|
const watchAchievementsWithWine = async () => {
|
||||||
const games = await gameRepository.find({
|
const games = await gamesSublevel
|
||||||
where: {
|
.values()
|
||||||
isDeleted: false,
|
.all()
|
||||||
winePrefixPath: Not(IsNull()),
|
.then((games) =>
|
||||||
},
|
games.filter((game) => !game.isDeleted && game.winePrefixPath)
|
||||||
});
|
);
|
||||||
|
|
||||||
for (const game of games) {
|
for (const game of games) {
|
||||||
const gameAchievementFiles = findAchievementFiles(game);
|
const gameAchievementFiles = findAchievementFiles(game);
|
||||||
|
@ -144,7 +141,7 @@ const processAchievementFileDiff = async (
|
||||||
export class AchievementWatcherManager {
|
export class AchievementWatcherManager {
|
||||||
private static hasFinishedMergingWithRemote = false;
|
private static hasFinishedMergingWithRemote = false;
|
||||||
|
|
||||||
public static watchAchievements = () => {
|
public static watchAchievements() {
|
||||||
if (!this.hasFinishedMergingWithRemote) return;
|
if (!this.hasFinishedMergingWithRemote) return;
|
||||||
|
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
|
@ -152,12 +149,12 @@ export class AchievementWatcherManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
return watchAchievementsWithWine();
|
return watchAchievementsWithWine();
|
||||||
};
|
}
|
||||||
|
|
||||||
private static preProcessGameAchievementFiles = (
|
private static preProcessGameAchievementFiles(
|
||||||
game: Game,
|
game: Game,
|
||||||
gameAchievementFiles: AchievementFile[]
|
gameAchievementFiles: AchievementFile[]
|
||||||
) => {
|
) {
|
||||||
const unlockedAchievements: UnlockedAchievement[] = [];
|
const unlockedAchievements: UnlockedAchievement[] = [];
|
||||||
for (const achievementFile of gameAchievementFiles) {
|
for (const achievementFile of gameAchievementFiles) {
|
||||||
const parsedAchievements = parseAchievementFile(
|
const parsedAchievements = parseAchievementFile(
|
||||||
|
@ -185,14 +182,13 @@ export class AchievementWatcherManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
return mergeAchievements(game, unlockedAchievements, false);
|
return mergeAchievements(game, unlockedAchievements, false);
|
||||||
};
|
}
|
||||||
|
|
||||||
private static preSearchAchievementsWindows = async () => {
|
private static preSearchAchievementsWindows = async () => {
|
||||||
const games = await gameRepository.find({
|
const games = await gamesSublevel
|
||||||
where: {
|
.values()
|
||||||
isDeleted: false,
|
.all()
|
||||||
},
|
.then((games) => games.filter((game) => !game.isDeleted));
|
||||||
});
|
|
||||||
|
|
||||||
const gameAchievementFilesMap = findAllAchievementFiles();
|
const gameAchievementFilesMap = findAllAchievementFiles();
|
||||||
|
|
||||||
|
@ -200,7 +196,7 @@ export class AchievementWatcherManager {
|
||||||
games.map((game) => {
|
games.map((game) => {
|
||||||
const gameAchievementFiles: AchievementFile[] = [];
|
const gameAchievementFiles: AchievementFile[] = [];
|
||||||
|
|
||||||
for (const objectId of getAlternativeObjectIds(game.objectID)) {
|
for (const objectId of getAlternativeObjectIds(game.objectId)) {
|
||||||
gameAchievementFiles.push(
|
gameAchievementFiles.push(
|
||||||
...(gameAchievementFilesMap.get(objectId) || [])
|
...(gameAchievementFilesMap.get(objectId) || [])
|
||||||
);
|
);
|
||||||
|
@ -216,11 +212,10 @@ export class AchievementWatcherManager {
|
||||||
};
|
};
|
||||||
|
|
||||||
private static preSearchAchievementsWithWine = async () => {
|
private static preSearchAchievementsWithWine = async () => {
|
||||||
const games = await gameRepository.find({
|
const games = await gamesSublevel
|
||||||
where: {
|
.values()
|
||||||
isDeleted: false,
|
.all()
|
||||||
},
|
.then((games) => games.filter((game) => !game.isDeleted));
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
games.map((game) => {
|
games.map((game) => {
|
||||||
|
@ -235,7 +230,7 @@ export class AchievementWatcherManager {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
public static preSearchAchievements = async () => {
|
public static async preSearchAchievements() {
|
||||||
try {
|
try {
|
||||||
const newAchievementsCount =
|
const newAchievementsCount =
|
||||||
process.platform === "win32"
|
process.platform === "win32"
|
||||||
|
@ -261,5 +256,5 @@ export class AchievementWatcherManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.hasFinishedMergingWithRemote = true;
|
this.hasFinishedMergingWithRemote = true;
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import type { AchievementFile } from "@types";
|
import type { Game, AchievementFile } from "@types";
|
||||||
import { Cracker } from "@shared";
|
import { Cracker } from "@shared";
|
||||||
import { Game } from "@main/entity";
|
|
||||||
import { achievementsLogger } from "../logger";
|
import { achievementsLogger } from "../logger";
|
||||||
|
|
||||||
const getAppDataPath = () => {
|
const getAppDataPath = () => {
|
||||||
|
@ -254,7 +253,7 @@ export const findAchievementFiles = (game: Game) => {
|
||||||
|
|
||||||
for (const cracker of crackers) {
|
for (const cracker of crackers) {
|
||||||
for (const { folderPath, fileLocation } of getPathFromCracker(cracker)) {
|
for (const { folderPath, fileLocation } of getPathFromCracker(cracker)) {
|
||||||
for (const objectId of getAlternativeObjectIds(game.objectID)) {
|
for (const objectId of getAlternativeObjectIds(game.objectId)) {
|
||||||
const filePath = path.join(
|
const filePath = path.join(
|
||||||
game.winePrefixPath ?? "",
|
game.winePrefixPath ?? "",
|
||||||
folderPath,
|
folderPath,
|
||||||
|
|
|
@ -1,40 +1,37 @@
|
||||||
import {
|
|
||||||
gameAchievementRepository,
|
|
||||||
userPreferencesRepository,
|
|
||||||
} from "@main/repository";
|
|
||||||
import { HydraApi } from "../hydra-api";
|
import { HydraApi } from "../hydra-api";
|
||||||
import type { AchievementData, GameShop } from "@types";
|
import type { GameShop, SteamAchievement } from "@types";
|
||||||
import { UserNotLoggedInError } from "@shared";
|
import { UserNotLoggedInError } from "@shared";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { GameAchievement } from "@main/entity";
|
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
export const getGameAchievementData = async (
|
export const getGameAchievementData = async (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
cachedAchievements: GameAchievement | null
|
useCachedData: boolean
|
||||||
) => {
|
) => {
|
||||||
if (cachedAchievements && cachedAchievements.achievements) {
|
const cachedAchievements = await gameAchievementsSublevel.get(
|
||||||
return JSON.parse(cachedAchievements.achievements) as AchievementData[];
|
levelKeys.game(shop, objectId)
|
||||||
}
|
);
|
||||||
|
|
||||||
const userPreferences = await userPreferencesRepository.findOne({
|
if (cachedAchievements && useCachedData)
|
||||||
where: { id: 1 },
|
return cachedAchievements.achievements;
|
||||||
});
|
|
||||||
|
|
||||||
return HydraApi.get<AchievementData[]>("/games/achievements", {
|
const language = await db
|
||||||
|
.get<string, string>(levelKeys.language, {
|
||||||
|
valueEncoding: "utf-8",
|
||||||
|
})
|
||||||
|
.then((language) => language || "en");
|
||||||
|
|
||||||
|
return HydraApi.get<SteamAchievement[]>("/games/achievements", {
|
||||||
shop,
|
shop,
|
||||||
objectId,
|
objectId,
|
||||||
language: userPreferences?.language || "en",
|
language,
|
||||||
})
|
})
|
||||||
.then((achievements) => {
|
.then(async (achievements) => {
|
||||||
gameAchievementRepository.upsert(
|
await gameAchievementsSublevel.put(levelKeys.game(shop, objectId), {
|
||||||
{
|
unlockedAchievements: cachedAchievements?.unlockedAchievements ?? [],
|
||||||
objectId,
|
achievements,
|
||||||
shop,
|
});
|
||||||
achievements: JSON.stringify(achievements),
|
|
||||||
},
|
|
||||||
["objectId", "shop"]
|
|
||||||
);
|
|
||||||
|
|
||||||
return achievements;
|
return achievements;
|
||||||
})
|
})
|
||||||
|
@ -42,15 +39,9 @@ export const getGameAchievementData = async (
|
||||||
if (err instanceof UserNotLoggedInError) {
|
if (err instanceof UserNotLoggedInError) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
logger.error("Failed to get game achievements", err);
|
|
||||||
return gameAchievementRepository
|
logger.error("Failed to get game achievements for", objectId, err);
|
||||||
.findOne({
|
|
||||||
where: { objectId, shop },
|
return [];
|
||||||
})
|
|
||||||
.then((gameAchievements) => {
|
|
||||||
return JSON.parse(
|
|
||||||
gameAchievements?.achievements || "[]"
|
|
||||||
) as AchievementData[];
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,42 +1,45 @@
|
||||||
import {
|
import type {
|
||||||
gameAchievementRepository,
|
Game,
|
||||||
userPreferencesRepository,
|
GameShop,
|
||||||
} from "@main/repository";
|
UnlockedAchievement,
|
||||||
import type { AchievementData, GameShop, UnlockedAchievement } from "@types";
|
UserPreferences,
|
||||||
|
} from "@types";
|
||||||
import { WindowManager } from "../window-manager";
|
import { WindowManager } from "../window-manager";
|
||||||
import { HydraApi } from "../hydra-api";
|
import { HydraApi } from "../hydra-api";
|
||||||
import { getUnlockedAchievements } from "@main/events/user/get-unlocked-achievements";
|
import { getUnlockedAchievements } from "@main/events/user/get-unlocked-achievements";
|
||||||
import { Game } from "@main/entity";
|
|
||||||
import { publishNewAchievementNotification } from "../notifications";
|
import { publishNewAchievementNotification } from "../notifications";
|
||||||
import { SubscriptionRequiredError } from "@shared";
|
import { SubscriptionRequiredError } from "@shared";
|
||||||
import { achievementsLogger } from "../logger";
|
import { achievementsLogger } from "../logger";
|
||||||
|
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
|
||||||
|
|
||||||
const saveAchievementsOnLocal = async (
|
const saveAchievementsOnLocal = async (
|
||||||
objectId: string,
|
objectId: string,
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
achievements: UnlockedAchievement[],
|
unlockedAchievements: UnlockedAchievement[],
|
||||||
sendUpdateEvent: boolean
|
sendUpdateEvent: boolean
|
||||||
) => {
|
) => {
|
||||||
return gameAchievementRepository
|
const levelKey = levelKeys.game(shop, objectId);
|
||||||
.upsert(
|
|
||||||
{
|
|
||||||
objectId,
|
|
||||||
shop,
|
|
||||||
unlockedAchievements: JSON.stringify(achievements),
|
|
||||||
},
|
|
||||||
["objectId", "shop"]
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
if (!sendUpdateEvent) return;
|
|
||||||
|
|
||||||
return getUnlockedAchievements(objectId, shop, true)
|
return gameAchievementsSublevel
|
||||||
.then((achievements) => {
|
.get(levelKey)
|
||||||
WindowManager.mainWindow?.webContents.send(
|
.then(async (gameAchievement) => {
|
||||||
`on-update-achievements-${objectId}-${shop}`,
|
if (gameAchievement) {
|
||||||
achievements
|
await gameAchievementsSublevel.put(levelKey, {
|
||||||
);
|
...gameAchievement,
|
||||||
})
|
unlockedAchievements: unlockedAchievements,
|
||||||
.catch(() => {});
|
});
|
||||||
|
|
||||||
|
if (!sendUpdateEvent) return;
|
||||||
|
|
||||||
|
return getUnlockedAchievements(objectId, shop, true)
|
||||||
|
.then((achievements) => {
|
||||||
|
WindowManager.mainWindow?.webContents.send(
|
||||||
|
`on-update-achievements-${objectId}-${shop}`,
|
||||||
|
achievements
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -46,25 +49,17 @@ export const mergeAchievements = async (
|
||||||
publishNotification: boolean
|
publishNotification: boolean
|
||||||
) => {
|
) => {
|
||||||
const [localGameAchievement, userPreferences] = await Promise.all([
|
const [localGameAchievement, userPreferences] = await Promise.all([
|
||||||
gameAchievementRepository.findOne({
|
gameAchievementsSublevel.get(levelKeys.game(game.shop, game.objectId)),
|
||||||
where: {
|
db.get<string, UserPreferences>(levelKeys.userPreferences, {
|
||||||
objectId: game.objectID,
|
valueEncoding: "json",
|
||||||
shop: game.shop,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
userPreferencesRepository.findOne({ where: { id: 1 } }),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const achievementsData = JSON.parse(
|
const achievementsData = localGameAchievement?.achievements ?? [];
|
||||||
localGameAchievement?.achievements || "[]"
|
const unlockedAchievements = localGameAchievement?.unlockedAchievements ?? [];
|
||||||
) as AchievementData[];
|
|
||||||
|
|
||||||
const unlockedAchievements = JSON.parse(
|
|
||||||
localGameAchievement?.unlockedAchievements || "[]"
|
|
||||||
).filter((achievement) => achievement.name) as UnlockedAchievement[];
|
|
||||||
|
|
||||||
const newAchievementsMap = new Map(
|
const newAchievementsMap = new Map(
|
||||||
achievements.reverse().map((achievement) => {
|
achievements.toReversed().map((achievement) => {
|
||||||
return [achievement.name.toUpperCase(), achievement];
|
return [achievement.name.toUpperCase(), achievement];
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -92,7 +87,7 @@ export const mergeAchievements = async (
|
||||||
userPreferences?.achievementNotificationsEnabled
|
userPreferences?.achievementNotificationsEnabled
|
||||||
) {
|
) {
|
||||||
const achievementsInfo = newAchievements
|
const achievementsInfo = newAchievements
|
||||||
.sort((a, b) => {
|
.toSorted((a, b) => {
|
||||||
return a.unlockTime - b.unlockTime;
|
return a.unlockTime - b.unlockTime;
|
||||||
})
|
})
|
||||||
.map((achievement) => {
|
.map((achievement) => {
|
||||||
|
@ -141,13 +136,13 @@ export const mergeAchievements = async (
|
||||||
if (err! instanceof SubscriptionRequiredError) {
|
if (err! instanceof SubscriptionRequiredError) {
|
||||||
achievementsLogger.log(
|
achievementsLogger.log(
|
||||||
"Achievements not synchronized on API due to lack of subscription",
|
"Achievements not synchronized on API due to lack of subscription",
|
||||||
game.objectID,
|
game.objectId,
|
||||||
game.title
|
game.title
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return saveAchievementsOnLocal(
|
return saveAchievementsOnLocal(
|
||||||
game.objectID,
|
game.objectId,
|
||||||
game.shop,
|
game.shop,
|
||||||
mergedLocalAchievements,
|
mergedLocalAchievements,
|
||||||
publishNotification
|
publishNotification
|
||||||
|
@ -155,7 +150,7 @@ export const mergeAchievements = async (
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await saveAchievementsOnLocal(
|
await saveAchievementsOnLocal(
|
||||||
game.objectID,
|
game.objectId,
|
||||||
game.shop,
|
game.shop,
|
||||||
mergedLocalAchievements,
|
mergedLocalAchievements,
|
||||||
publishNotification
|
publishNotification
|
||||||
|
|
|
@ -4,8 +4,7 @@ import {
|
||||||
} from "./find-achivement-files";
|
} from "./find-achivement-files";
|
||||||
import { parseAchievementFile } from "./parse-achievement-file";
|
import { parseAchievementFile } from "./parse-achievement-file";
|
||||||
import { mergeAchievements } from "./merge-achievements";
|
import { mergeAchievements } from "./merge-achievements";
|
||||||
import type { UnlockedAchievement } from "@types";
|
import type { Game, UnlockedAchievement } from "@types";
|
||||||
import { Game } from "@main/entity";
|
|
||||||
|
|
||||||
export const updateLocalUnlockedAchivements = async (game: Game) => {
|
export const updateLocalUnlockedAchivements = async (game: Game) => {
|
||||||
const gameAchievementFiles = findAchievementFiles(game);
|
const gameAchievementFiles = findAchievementFiles(game);
|
||||||
|
|
28
src/main/services/crypto.ts
Normal file
28
src/main/services/crypto.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { safeStorage } from "electron";
|
||||||
|
import { logger } from "./logger";
|
||||||
|
|
||||||
|
export class Crypto {
|
||||||
|
public static encrypt(str: string) {
|
||||||
|
if (safeStorage.isEncryptionAvailable()) {
|
||||||
|
return safeStorage.encryptString(str).toString("base64");
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
"Encrypt method returned raw string because encryption is not available"
|
||||||
|
);
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decrypt(b64: string) {
|
||||||
|
if (safeStorage.isEncryptionAvailable()) {
|
||||||
|
return safeStorage.decryptString(Buffer.from(b64, "base64"));
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
"Decrypt method returned raw string because encryption is not available"
|
||||||
|
);
|
||||||
|
|
||||||
|
return b64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,7 @@
|
||||||
import { Game } from "@main/entity";
|
|
||||||
import { Downloader } from "@shared";
|
import { Downloader } from "@shared";
|
||||||
import { WindowManager } from "../window-manager";
|
import { WindowManager } from "../window-manager";
|
||||||
import {
|
|
||||||
downloadQueueRepository,
|
|
||||||
gameRepository,
|
|
||||||
userPreferencesRepository,
|
|
||||||
} from "@main/repository";
|
|
||||||
import { publishDownloadCompleteNotification } from "../notifications";
|
import { publishDownloadCompleteNotification } from "../notifications";
|
||||||
import type { DownloadProgress } from "@types";
|
import type { Download, DownloadProgress, UserPreferences } from "@types";
|
||||||
import { GofileApi, QiwiApi, DatanodesApi } from "../hosters";
|
import { GofileApi, QiwiApi, DatanodesApi } from "../hosters";
|
||||||
import { PythonRPC } from "../python-rpc";
|
import { PythonRPC } from "../python-rpc";
|
||||||
import {
|
import {
|
||||||
|
@ -16,40 +10,43 @@ import {
|
||||||
PauseDownloadPayload,
|
PauseDownloadPayload,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { calculateETA, getDirSize } from "./helpers";
|
import { calculateETA, getDirSize } from "./helpers";
|
||||||
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
|
||||||
import { RealDebridClient } from "./real-debrid";
|
import { RealDebridClient } from "./real-debrid";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
|
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
import { sortBy } from "lodash-es";
|
||||||
import { TorBoxClient } from "./torbox";
|
import { TorBoxClient } from "./torbox";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
export class DownloadManager {
|
export class DownloadManager {
|
||||||
private static downloadingGameId: number | null = null;
|
private static downloadingGameId: string | null = null;
|
||||||
|
|
||||||
public static async startRPC(game?: Game, initialSeeding?: Game[]) {
|
public static async startRPC(
|
||||||
|
download?: Download,
|
||||||
|
downloadsToSeed?: Download[]
|
||||||
|
) {
|
||||||
PythonRPC.spawn(
|
PythonRPC.spawn(
|
||||||
game?.status === "active"
|
download?.status === "active"
|
||||||
? await this.getDownloadPayload(game).catch(() => undefined)
|
? await this.getDownloadPayload(download).catch(() => undefined)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
downloadsToSeed?.map((download) => ({
|
||||||
initialSeeding?.map((game) => ({
|
game_id: `${download.shop}-${download.objectId}`,
|
||||||
game_id: game.id,
|
url: download.uri,
|
||||||
url: game.uri!,
|
save_path: download.downloadPath,
|
||||||
save_path: game.downloadPath!,
|
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
this.downloadingGameId = game?.id ?? null;
|
if (download) {
|
||||||
|
this.downloadingGameId = `${download.shop}-${download.objectId}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async getDownloadStatus() {
|
private static async getDownloadStatus() {
|
||||||
const response = await PythonRPC.rpc.get<LibtorrentPayload | null>(
|
const response = await PythonRPC.rpc.get<LibtorrentPayload | null>(
|
||||||
"/status"
|
"/status"
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.data === null || !this.downloadingGameId) return null;
|
if (response.data === null || !this.downloadingGameId) return null;
|
||||||
|
const downloadId = this.downloadingGameId;
|
||||||
const gameId = this.downloadingGameId;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
|
@ -65,24 +62,21 @@ export class DownloadManager {
|
||||||
|
|
||||||
const isDownloadingMetadata =
|
const isDownloadingMetadata =
|
||||||
status === LibtorrentStatus.DownloadingMetadata;
|
status === LibtorrentStatus.DownloadingMetadata;
|
||||||
|
|
||||||
const isCheckingFiles = status === LibtorrentStatus.CheckingFiles;
|
const isCheckingFiles = status === LibtorrentStatus.CheckingFiles;
|
||||||
|
|
||||||
|
const download = await downloadsSublevel.get(downloadId);
|
||||||
|
|
||||||
if (!isDownloadingMetadata && !isCheckingFiles) {
|
if (!isDownloadingMetadata && !isCheckingFiles) {
|
||||||
const update: QueryDeepPartialEntity<Game> = {
|
if (!download) return null;
|
||||||
|
|
||||||
|
await downloadsSublevel.put(downloadId, {
|
||||||
|
...download,
|
||||||
bytesDownloaded,
|
bytesDownloaded,
|
||||||
fileSize,
|
fileSize,
|
||||||
progress,
|
progress,
|
||||||
|
folderName,
|
||||||
status: "active",
|
status: "active",
|
||||||
};
|
});
|
||||||
|
|
||||||
await gameRepository.update(
|
|
||||||
{ id: gameId },
|
|
||||||
{
|
|
||||||
...update,
|
|
||||||
folderName,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -93,7 +87,8 @@ export class DownloadManager {
|
||||||
isDownloadingMetadata,
|
isDownloadingMetadata,
|
||||||
isCheckingFiles,
|
isCheckingFiles,
|
||||||
progress,
|
progress,
|
||||||
gameId,
|
gameId: downloadId,
|
||||||
|
download,
|
||||||
} as DownloadProgress;
|
} as DownloadProgress;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -105,14 +100,22 @@ export class DownloadManager {
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
const { gameId, progress } = status;
|
const { gameId, progress } = status;
|
||||||
const game = await gameRepository.findOne({
|
|
||||||
where: { id: gameId, isDeleted: false },
|
|
||||||
});
|
|
||||||
const userPreferences = await userPreferencesRepository.findOneBy({
|
|
||||||
id: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (WindowManager.mainWindow && game) {
|
const [download, game] = await Promise.all([
|
||||||
|
downloadsSublevel.get(gameId),
|
||||||
|
gamesSublevel.get(gameId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!download || !game) return;
|
||||||
|
|
||||||
|
const userPreferences = await db.get<string, UserPreferences>(
|
||||||
|
levelKeys.userPreferences,
|
||||||
|
{
|
||||||
|
valueEncoding: "json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (WindowManager.mainWindow && download) {
|
||||||
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
|
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
|
||||||
WindowManager.mainWindow.webContents.send(
|
WindowManager.mainWindow.webContents.send(
|
||||||
"on-download-progress",
|
"on-download-progress",
|
||||||
|
@ -124,39 +127,48 @@ export class DownloadManager {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (progress === 1 && game) {
|
|
||||||
|
if (progress === 1 && download) {
|
||||||
publishDownloadCompleteNotification(game);
|
publishDownloadCompleteNotification(game);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
userPreferences?.seedAfterDownloadComplete &&
|
userPreferences?.seedAfterDownloadComplete &&
|
||||||
game.downloader === Downloader.Torrent
|
download.downloader === Downloader.Torrent
|
||||||
) {
|
) {
|
||||||
gameRepository.update(
|
downloadsSublevel.put(gameId, {
|
||||||
{ id: gameId },
|
...download,
|
||||||
{ status: "seeding", shouldSeed: true }
|
status: "seeding",
|
||||||
);
|
shouldSeed: true,
|
||||||
|
queued: false,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
gameRepository.update(
|
downloadsSublevel.put(gameId, {
|
||||||
{ id: gameId },
|
...download,
|
||||||
{ status: "complete", shouldSeed: false }
|
status: "complete",
|
||||||
);
|
shouldSeed: false,
|
||||||
|
queued: false,
|
||||||
|
});
|
||||||
|
|
||||||
this.cancelDownload(gameId);
|
this.cancelDownload(gameId);
|
||||||
}
|
}
|
||||||
|
|
||||||
await downloadQueueRepository.delete({ game });
|
const downloads = await downloadsSublevel
|
||||||
const [nextQueueItem] = await downloadQueueRepository.find({
|
.values()
|
||||||
order: {
|
.all()
|
||||||
id: "DESC",
|
.then((games) => {
|
||||||
},
|
return sortBy(
|
||||||
relations: {
|
games.filter((game) => game.status === "paused" && game.queued),
|
||||||
game: true,
|
"timestamp",
|
||||||
},
|
"DESC"
|
||||||
});
|
);
|
||||||
if (nextQueueItem) {
|
});
|
||||||
this.resumeDownload(nextQueueItem.game);
|
|
||||||
|
const [nextItemOnQueue] = downloads;
|
||||||
|
|
||||||
|
if (nextItemOnQueue) {
|
||||||
|
this.resumeDownload(nextItemOnQueue);
|
||||||
} else {
|
} else {
|
||||||
this.downloadingGameId = -1;
|
this.downloadingGameId = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -172,20 +184,19 @@ export class DownloadManager {
|
||||||
logger.log(seedStatus);
|
logger.log(seedStatus);
|
||||||
|
|
||||||
seedStatus.forEach(async (status) => {
|
seedStatus.forEach(async (status) => {
|
||||||
const game = await gameRepository.findOne({
|
const download = await downloadsSublevel.get(status.gameId);
|
||||||
where: { id: status.gameId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!game) return;
|
if (!download) return;
|
||||||
|
|
||||||
const totalSize = await getDirSize(
|
const totalSize = await getDirSize(
|
||||||
path.join(game.downloadPath!, status.folderName)
|
path.join(download.downloadPath, status.folderName)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (totalSize < status.fileSize) {
|
if (totalSize < status.fileSize) {
|
||||||
await this.cancelDownload(game.id);
|
await this.cancelDownload(status.gameId);
|
||||||
|
|
||||||
await gameRepository.update(game.id, {
|
await downloadsSublevel.put(status.gameId, {
|
||||||
|
...download,
|
||||||
status: "paused",
|
status: "paused",
|
||||||
shouldSeed: false,
|
shouldSeed: false,
|
||||||
progress: totalSize / status.fileSize,
|
progress: totalSize / status.fileSize,
|
||||||
|
@ -198,65 +209,64 @@ export class DownloadManager {
|
||||||
WindowManager.mainWindow?.webContents.send("on-seeding-status", seedStatus);
|
WindowManager.mainWindow?.webContents.send("on-seeding-status", seedStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async pauseDownload() {
|
static async pauseDownload(downloadKey = this.downloadingGameId) {
|
||||||
await PythonRPC.rpc
|
await PythonRPC.rpc
|
||||||
.post("/action", {
|
.post("/action", {
|
||||||
action: "pause",
|
action: "pause",
|
||||||
game_id: this.downloadingGameId,
|
game_id: downloadKey,
|
||||||
} as PauseDownloadPayload)
|
} as PauseDownloadPayload)
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
|
||||||
WindowManager.mainWindow?.setProgressBar(-1);
|
WindowManager.mainWindow?.setProgressBar(-1);
|
||||||
|
|
||||||
this.downloadingGameId = null;
|
this.downloadingGameId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async resumeDownload(game: Game) {
|
static async resumeDownload(download: Download) {
|
||||||
return this.startDownload(game);
|
return this.startDownload(download);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async cancelDownload(gameId = this.downloadingGameId!) {
|
static async cancelDownload(downloadKey = this.downloadingGameId) {
|
||||||
await PythonRPC.rpc.post("/action", {
|
await PythonRPC.rpc.post("/action", {
|
||||||
action: "cancel",
|
action: "cancel",
|
||||||
game_id: gameId,
|
game_id: downloadKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
WindowManager.mainWindow?.setProgressBar(-1);
|
WindowManager.mainWindow?.setProgressBar(-1);
|
||||||
|
if (downloadKey === this.downloadingGameId) {
|
||||||
if (gameId === this.downloadingGameId) {
|
|
||||||
this.downloadingGameId = null;
|
this.downloadingGameId = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async resumeSeeding(game: Game) {
|
static async resumeSeeding(download: Download) {
|
||||||
await PythonRPC.rpc.post("/action", {
|
await PythonRPC.rpc.post("/action", {
|
||||||
action: "resume_seeding",
|
action: "resume_seeding",
|
||||||
game_id: game.id,
|
game_id: levelKeys.game(download.shop, download.objectId),
|
||||||
url: game.uri,
|
url: download.uri,
|
||||||
save_path: game.downloadPath,
|
save_path: download.downloadPath,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static async pauseSeeding(gameId: number) {
|
static async pauseSeeding(downloadKey: string) {
|
||||||
await PythonRPC.rpc.post("/action", {
|
await PythonRPC.rpc.post("/action", {
|
||||||
action: "pause_seeding",
|
action: "pause_seeding",
|
||||||
game_id: gameId,
|
game_id: downloadKey,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async getDownloadPayload(game: Game) {
|
private static async getDownloadPayload(download: Download) {
|
||||||
switch (game.downloader) {
|
const downloadId = levelKeys.game(download.shop, download.objectId);
|
||||||
case Downloader.Gofile: {
|
|
||||||
const id = game.uri!.split("/").pop();
|
|
||||||
|
|
||||||
|
switch (download.downloader) {
|
||||||
|
case Downloader.Gofile: {
|
||||||
|
const id = download.uri.split("/").pop();
|
||||||
const token = await GofileApi.authorize();
|
const token = await GofileApi.authorize();
|
||||||
const downloadLink = await GofileApi.getDownloadLink(id!);
|
const downloadLink = await GofileApi.getDownloadLink(id!);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
action: "start",
|
action: "start",
|
||||||
game_id: game.id,
|
game_id: downloadId,
|
||||||
url: downloadLink,
|
url: downloadLink,
|
||||||
save_path: game.downloadPath!,
|
save_path: download.downloadPath,
|
||||||
header: `Cookie: accountToken=${token}`,
|
header: `Cookie: accountToken=${token}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -269,47 +279,50 @@ export class DownloadManager {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
action: "start",
|
action: "start",
|
||||||
game_id: game.id,
|
game_id: downloadId,
|
||||||
url: `https://pixeldrain.com/api/file/${id}?download`,
|
url: `https://pixeldrain.com/api/file/${id}?download`,
|
||||||
save_path: game.downloadPath!,
|
save_path: game.downloadPath!,
|
||||||
out: name,
|
out: name,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case Downloader.Qiwi: {
|
case Downloader.Qiwi: {
|
||||||
const downloadUrl = await QiwiApi.getDownloadUrl(game.uri!);
|
const downloadUrl = await QiwiApi.getDownloadUrl(download.uri);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
action: "start",
|
action: "start",
|
||||||
game_id: game.id,
|
game_id: downloadId,
|
||||||
url: downloadUrl,
|
url: downloadUrl,
|
||||||
save_path: game.downloadPath!,
|
save_path: download.downloadPath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case Downloader.Datanodes: {
|
case Downloader.Datanodes: {
|
||||||
const downloadUrl = await DatanodesApi.getDownloadUrl(game.uri!);
|
const downloadUrl = await DatanodesApi.getDownloadUrl(download.uri);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
action: "start",
|
action: "start",
|
||||||
game_id: game.id,
|
game_id: downloadId,
|
||||||
url: downloadUrl,
|
url: downloadUrl,
|
||||||
save_path: game.downloadPath!,
|
save_path: download.downloadPath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case Downloader.Torrent:
|
case Downloader.Torrent:
|
||||||
return {
|
return {
|
||||||
action: "start",
|
action: "start",
|
||||||
game_id: game.id,
|
game_id: downloadId,
|
||||||
url: game.uri!,
|
url: download.uri,
|
||||||
save_path: game.downloadPath!,
|
save_path: download.downloadPath,
|
||||||
};
|
};
|
||||||
case Downloader.RealDebrid: {
|
case Downloader.RealDebrid: {
|
||||||
const downloadUrl = await RealDebridClient.getDownloadUrl(game.uri!);
|
const downloadUrl = await RealDebridClient.getDownloadUrl(download.uri);
|
||||||
|
|
||||||
|
if (!downloadUrl)
|
||||||
|
throw new Error(
|
||||||
|
"This download is not available on Real-Debrid and polling download status from Real-Debrid is not yet available."
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
action: "start",
|
action: "start",
|
||||||
game_id: game.id,
|
game_id: downloadId,
|
||||||
url: downloadUrl!,
|
url: downloadUrl,
|
||||||
save_path: game.downloadPath!,
|
save_path: download.downloadPath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case Downloader.TorBox: {
|
case Downloader.TorBox: {
|
||||||
|
@ -328,11 +341,9 @@ export class DownloadManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async startDownload(game: Game) {
|
static async startDownload(download: Download) {
|
||||||
const payload = await this.getDownloadPayload(game);
|
const payload = await this.getDownloadPayload(download);
|
||||||
|
|
||||||
await PythonRPC.rpc.post("/action", payload);
|
await PythonRPC.rpc.post("/action", payload);
|
||||||
|
this.downloadingGameId = levelKeys.game(download.shop, download.objectId);
|
||||||
this.downloadingGameId = game.id;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
export interface PauseDownloadPayload {
|
export interface PauseDownloadPayload {
|
||||||
game_id: number;
|
game_id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CancelDownloadPayload {
|
export interface CancelDownloadPayload {
|
||||||
game_id: number;
|
game_id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum LibtorrentStatus {
|
export enum LibtorrentStatus {
|
||||||
|
@ -24,7 +24,7 @@ export interface LibtorrentPayload {
|
||||||
fileSize: number;
|
fileSize: number;
|
||||||
folderName: string;
|
folderName: string;
|
||||||
status: LibtorrentStatus;
|
status: LibtorrentStatus;
|
||||||
gameId: number;
|
gameId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProcessPayload {
|
export interface ProcessPayload {
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import {
|
|
||||||
userAuthRepository,
|
|
||||||
userSubscriptionRepository,
|
|
||||||
} from "@main/repository";
|
|
||||||
import axios, { AxiosError, AxiosInstance } from "axios";
|
import axios, { AxiosError, AxiosInstance } from "axios";
|
||||||
import { WindowManager } from "./window-manager";
|
import { WindowManager } from "./window-manager";
|
||||||
import url from "url";
|
import url from "url";
|
||||||
import { uploadGamesBatch } from "./library-sync";
|
import { uploadGamesBatch } from "./library-sync";
|
||||||
import { clearGamesRemoteIds } from "./library-sync/clear-games-remote-id";
|
import { clearGamesRemoteIds } from "./library-sync/clear-games-remote-id";
|
||||||
import { logger } from "./logger";
|
import { networkLogger as logger } from "./logger";
|
||||||
import { UserNotLoggedInError, SubscriptionRequiredError } from "@shared";
|
import { UserNotLoggedInError, SubscriptionRequiredError } from "@shared";
|
||||||
import { omit } from "lodash-es";
|
import { omit } from "lodash-es";
|
||||||
import { appVersion } from "@main/constants";
|
import { appVersion } from "@main/constants";
|
||||||
import { getUserData } from "./user/get-user-data";
|
import { getUserData } from "./user/get-user-data";
|
||||||
import { isFuture, isToday } from "date-fns";
|
import { isFuture, isToday } from "date-fns";
|
||||||
|
import { db } from "@main/level";
|
||||||
|
import { levelKeys } from "@main/level/sublevels";
|
||||||
|
import type { Auth, User } from "@types";
|
||||||
|
import { Crypto } from "./crypto";
|
||||||
|
|
||||||
interface HydraApiOptions {
|
interface HydraApiOptions {
|
||||||
needsAuth?: boolean;
|
needsAuth?: boolean;
|
||||||
|
@ -32,7 +32,8 @@ export class HydraApi {
|
||||||
private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; // 5 minutes
|
private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; // 5 minutes
|
||||||
private static readonly ADD_LOG_INTERCEPTOR = true;
|
private static readonly ADD_LOG_INTERCEPTOR = true;
|
||||||
|
|
||||||
private static secondsToMilliseconds = (seconds: number) => seconds * 1000;
|
private static readonly secondsToMilliseconds = (seconds: number) =>
|
||||||
|
seconds * 1000;
|
||||||
|
|
||||||
private static userAuth: HydraApiUserAuth = {
|
private static userAuth: HydraApiUserAuth = {
|
||||||
authToken: "",
|
authToken: "",
|
||||||
|
@ -77,14 +78,14 @@ export class HydraApi {
|
||||||
tokenExpirationTimestamp
|
tokenExpirationTimestamp
|
||||||
);
|
);
|
||||||
|
|
||||||
await userAuthRepository.upsert(
|
db.put<string, Auth>(
|
||||||
|
levelKeys.auth,
|
||||||
{
|
{
|
||||||
id: 1,
|
accessToken: Crypto.encrypt(accessToken),
|
||||||
accessToken,
|
refreshToken: Crypto.encrypt(refreshToken),
|
||||||
tokenExpirationTimestamp,
|
tokenExpirationTimestamp,
|
||||||
refreshToken,
|
|
||||||
},
|
},
|
||||||
["id"]
|
{ valueEncoding: "json" }
|
||||||
);
|
);
|
||||||
|
|
||||||
await getUserData().then((userDetails) => {
|
await getUserData().then((userDetails) => {
|
||||||
|
@ -153,7 +154,8 @@ export class HydraApi {
|
||||||
(error) => {
|
(error) => {
|
||||||
logger.error(" ---- RESPONSE ERROR -----");
|
logger.error(" ---- RESPONSE ERROR -----");
|
||||||
const { config } = error;
|
const { config } = error;
|
||||||
const data = JSON.parse(config.data);
|
|
||||||
|
const data = JSON.parse(config.data ?? null);
|
||||||
|
|
||||||
logger.error(
|
logger.error(
|
||||||
config.method,
|
config.method,
|
||||||
|
@ -174,29 +176,43 @@ export class HydraApi {
|
||||||
error.response.status,
|
error.response.status,
|
||||||
error.response.data
|
error.response.data
|
||||||
);
|
);
|
||||||
} else if (error.request) {
|
|
||||||
const errorData = error.toJSON();
|
return Promise.reject(error as Error);
|
||||||
logger.error("Request error:", errorData.message);
|
|
||||||
} else {
|
|
||||||
logger.error("Error", error.message);
|
|
||||||
}
|
}
|
||||||
logger.error(" ----- END RESPONSE ERROR -------");
|
|
||||||
return Promise.reject(error);
|
if (error.request) {
|
||||||
|
const errorData = error.toJSON();
|
||||||
|
logger.error("Request error:", errorData.code, errorData.message);
|
||||||
|
return Promise.reject(
|
||||||
|
new Error(
|
||||||
|
`Request failed with ${errorData.code} ${errorData.message}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error("Error", error.message);
|
||||||
|
return Promise.reject(error as Error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userAuth = await userAuthRepository.findOne({
|
const result = await db.getMany<string>([levelKeys.auth, levelKeys.user], {
|
||||||
where: { id: 1 },
|
valueEncoding: "json",
|
||||||
relations: { subscription: true },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const userAuth = result.at(0) as Auth | undefined;
|
||||||
|
const user = result.at(1) as User | undefined;
|
||||||
|
|
||||||
this.userAuth = {
|
this.userAuth = {
|
||||||
authToken: userAuth?.accessToken ?? "",
|
authToken: userAuth?.accessToken
|
||||||
refreshToken: userAuth?.refreshToken ?? "",
|
? Crypto.decrypt(userAuth.accessToken)
|
||||||
|
: "",
|
||||||
|
refreshToken: userAuth?.refreshToken
|
||||||
|
? Crypto.decrypt(userAuth.refreshToken)
|
||||||
|
: "",
|
||||||
expirationTimestamp: userAuth?.tokenExpirationTimestamp ?? 0,
|
expirationTimestamp: userAuth?.tokenExpirationTimestamp ?? 0,
|
||||||
subscription: userAuth?.subscription
|
subscription: user?.subscription
|
||||||
? { expiresAt: userAuth.subscription?.expiresAt }
|
? { expiresAt: user.subscription?.expiresAt }
|
||||||
: null,
|
: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -215,38 +231,47 @@ export class HydraApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async revalidateAccessTokenIfExpired() {
|
public static async refreshToken() {
|
||||||
const now = new Date();
|
const response = await this.instance.post(`/auth/refresh`, {
|
||||||
|
refreshToken: this.userAuth.refreshToken,
|
||||||
|
});
|
||||||
|
|
||||||
if (this.userAuth.expirationTimestamp < now.getTime()) {
|
const { accessToken, expiresIn } = response.data;
|
||||||
try {
|
|
||||||
const response = await this.instance.post(`/auth/refresh`, {
|
|
||||||
refreshToken: this.userAuth.refreshToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { accessToken, expiresIn } = response.data;
|
const tokenExpirationTimestamp =
|
||||||
|
Date.now() +
|
||||||
|
this.secondsToMilliseconds(expiresIn) -
|
||||||
|
this.EXPIRATION_OFFSET_IN_MS;
|
||||||
|
|
||||||
const tokenExpirationTimestamp =
|
this.userAuth.authToken = accessToken;
|
||||||
now.getTime() +
|
this.userAuth.expirationTimestamp = tokenExpirationTimestamp;
|
||||||
this.secondsToMilliseconds(expiresIn) -
|
|
||||||
this.EXPIRATION_OFFSET_IN_MS;
|
|
||||||
|
|
||||||
this.userAuth.authToken = accessToken;
|
logger.log(
|
||||||
this.userAuth.expirationTimestamp = tokenExpirationTimestamp;
|
"Token refreshed. New expiration:",
|
||||||
|
this.userAuth.expirationTimestamp
|
||||||
|
);
|
||||||
|
|
||||||
logger.log(
|
await db
|
||||||
"Token refreshed. New expiration:",
|
.get<string, Auth>(levelKeys.auth, { valueEncoding: "json" })
|
||||||
this.userAuth.expirationTimestamp
|
.then((auth) => {
|
||||||
);
|
return db.put<string, Auth>(
|
||||||
|
levelKeys.auth,
|
||||||
userAuthRepository.upsert(
|
|
||||||
{
|
{
|
||||||
id: 1,
|
...auth,
|
||||||
accessToken,
|
accessToken: Crypto.encrypt(accessToken),
|
||||||
tokenExpirationTimestamp,
|
tokenExpirationTimestamp,
|
||||||
},
|
},
|
||||||
["id"]
|
{ valueEncoding: "json" }
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { accessToken, expiresIn };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async revalidateAccessTokenIfExpired() {
|
||||||
|
if (this.userAuth.expirationTimestamp < Date.now()) {
|
||||||
|
try {
|
||||||
|
await this.refreshToken();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.handleUnauthorizedError(err);
|
this.handleUnauthorizedError(err);
|
||||||
}
|
}
|
||||||
|
@ -261,7 +286,7 @@ export class HydraApi {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static handleUnauthorizedError = (err) => {
|
private static readonly handleUnauthorizedError = (err) => {
|
||||||
if (err instanceof AxiosError && err.response?.status === 401) {
|
if (err instanceof AxiosError && err.response?.status === 401) {
|
||||||
logger.error(
|
logger.error(
|
||||||
"401 - Current credentials:",
|
"401 - Current credentials:",
|
||||||
|
@ -276,8 +301,16 @@ export class HydraApi {
|
||||||
subscription: null,
|
subscription: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
userAuthRepository.delete({ id: 1 });
|
db.batch([
|
||||||
userSubscriptionRepository.delete({ id: 1 });
|
{
|
||||||
|
type: "del",
|
||||||
|
key: levelKeys.auth,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "del",
|
||||||
|
key: levelKeys.user,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
this.sendSignOutEvent();
|
this.sendSignOutEvent();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
export * from "./crypto";
|
||||||
export * from "./logger";
|
export * from "./logger";
|
||||||
export * from "./steam";
|
export * from "./steam";
|
||||||
export * from "./steam-250";
|
export * from "./steam-250";
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue