diff --git a/.env.example b/.env.example index c2ad43d95..47d1a1e3a 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,4 @@ MAIN_VITE_STEAMGRIDDB_API_KEY=YOUR_API_KEY MAIN_VITE_API_URL=API_URL MAIN_VITE_SENTRY_DSN=YOUR_SENTRY_DSN +SENTRY_AUTH_TOKEN= diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 20a00ccfb..b55b280ec 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,8 +37,6 @@ jobs: if: matrix.os == 'ubuntu-latest' run: yarn build:linux env: - MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }} - MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }} MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }} @@ -48,8 +46,6 @@ jobs: if: matrix.os == 'windows-latest' run: yarn build:win env: - MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }} - MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }} MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a0684c6c6..d1bc89939 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,8 +39,6 @@ jobs: if: matrix.os == 'ubuntu-latest' run: yarn build:linux env: - MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }} - MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }} MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }} @@ -50,8 +48,6 @@ jobs: if: matrix.os == 'windows-latest' run: yarn build:win env: - MAIN_VITE_ONLINEFIX_USERNAME: ${{ secrets.ONLINEFIX_USERNAME }} - MAIN_VITE_ONLINEFIX_PASSWORD: ${{ secrets.ONLINEFIX_PASSWORD }} MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} MAIN_VITE_SENTRY_DSN: ${{ vars.MAIN_VITE_SENTRY_DSN }} diff --git a/.prettierignore b/.prettierignore index 05d298a1e..9b6e9df69 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,3 +5,4 @@ pnpm-lock.yaml LICENSE.md tsconfig.json tsconfig.*.json +src/main/migrations diff --git a/src/main/data-source.ts b/src/main/data-source.ts index a88a88833..446ccbdc2 100644 --- a/src/main/data-source.ts +++ b/src/main/data-source.ts @@ -8,30 +8,22 @@ import { UserPreferences, UserAuth, } from "@main/entity"; -import type { BetterSqlite3ConnectionOptions } from "typeorm/driver/better-sqlite3/BetterSqlite3ConnectionOptions"; import { databasePath } from "./constants"; -import migrations from "./migrations"; +import * as migrations from "./migrations"; -export const createDataSource = ( - options: Partial -) => - new DataSource({ - type: "better-sqlite3", - entities: [ - Game, - Repack, - UserPreferences, - GameShopCache, - DownloadSource, - DownloadQueue, - UserAuth, - ], - synchronize: true, - database: databasePath, - ...options, - }); - -export const dataSource = createDataSource({ +export const dataSource = new DataSource({ + type: "better-sqlite3", + entities: [ + Game, + Repack, + UserPreferences, + GameShopCache, + DownloadSource, + DownloadQueue, + UserAuth, + ], + synchronize: true, + database: databasePath, migrations, }); diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts index 468f5b267..a8fc8b017 100644 --- a/src/main/events/library/remove-game-from-library.ts +++ b/src/main/events/library/remove-game-from-library.ts @@ -20,7 +20,7 @@ const removeRemoveGameFromLibrary = async (gameId: number) => { const game = await gameRepository.findOne({ where: { id: gameId } }); if (game?.remoteId) { - HydraApi.delete(`/games/${game.remoteId}`).catch(() => {}); + HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {}); } }; diff --git a/src/main/events/user/block-user.ts b/src/main/events/user/block-user.ts index 8003f4788..c81231e52 100644 --- a/src/main/events/user/block-user.ts +++ b/src/main/events/user/block-user.ts @@ -5,7 +5,7 @@ const blockUser = async ( _event: Electron.IpcMainInvokeEvent, userId: string ) => { - await HydraApi.post(`/user/${userId}/block`); + await HydraApi.post(`/users/${userId}/block`); }; registerEvent("blockUser", blockUser); diff --git a/src/main/events/user/get-user-friends.ts b/src/main/events/user/get-user-friends.ts index 287834594..5ff4c8a48 100644 --- a/src/main/events/user/get-user-friends.ts +++ b/src/main/events/user/get-user-friends.ts @@ -14,7 +14,7 @@ export const getUserFriends = async ( return HydraApi.get(`/profile/friends`, { take, skip }); } - return HydraApi.get(`/user/${userId}/friends`, { take, skip }); + return HydraApi.get(`/users/${userId}/friends`, { take, skip }); }; const getUserFriendsEvent = async ( diff --git a/src/main/events/user/get-user.ts b/src/main/events/user/get-user.ts index eb4f06191..68d699691 100644 --- a/src/main/events/user/get-user.ts +++ b/src/main/events/user/get-user.ts @@ -1,7 +1,7 @@ import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services"; import { steamGamesWorker } from "@main/workers"; -import { UserProfile } from "@types"; +import { GameRunning, UserGame, UserProfile } from "@types"; import { convertSteamGameToCatalogueEntry } from "../helpers/search-games"; import { getSteamAppAsset } from "@main/helpers"; import { getUserFriends } from "./get-user-friends"; @@ -12,7 +12,7 @@ const getUser = async ( ): Promise => { try { const [profile, friends] = await Promise.all([ - HydraApi.get(`/user/${userId}`), + HydraApi.get(`/users/${userId}`), getUserFriends(userId, 12, 0).catch(() => { return { totalFriends: 0, friends: [] }; }), @@ -20,48 +20,57 @@ const getUser = async ( const recentGames = await Promise.all( profile.recentGames.map(async (game) => { - const steamGame = await steamGamesWorker.run(Number(game.objectId), { - name: "getById", - }); - const iconUrl = steamGame?.clientIcon - ? getSteamAppAsset("icon", game.objectId, steamGame.clientIcon) - : null; - - return { - ...game, - ...convertSteamGameToCatalogueEntry(steamGame), - iconUrl, - }; + return getSteamUserGame(game); }) ); const libraryGames = await Promise.all( profile.libraryGames.map(async (game) => { - const steamGame = await steamGamesWorker.run(Number(game.objectId), { - name: "getById", - }); - const iconUrl = steamGame?.clientIcon - ? getSteamAppAsset("icon", game.objectId, steamGame.clientIcon) - : null; - - return { - ...game, - ...convertSteamGameToCatalogueEntry(steamGame), - iconUrl, - }; + return getSteamUserGame(game); }) ); + const currentGame = await getGameRunning(profile.currentGame); + return { ...profile, libraryGames, recentGames, friends: friends.friends, totalFriends: friends.totalFriends, + currentGame, }; } catch (err) { return null; } }; +const getGameRunning = async (currentGame): Promise => { + if (!currentGame) { + return null; + } + + const gameRunning = await getSteamUserGame(currentGame); + + return { + ...gameRunning, + sessionDurationInMillis: currentGame.sessionDurationInSeconds * 1000, + }; +}; + +const getSteamUserGame = async (game): Promise => { + const steamGame = await steamGamesWorker.run(Number(game.objectId), { + name: "getById", + }); + const iconUrl = steamGame?.clientIcon + ? getSteamAppAsset("icon", game.objectId, steamGame.clientIcon) + : null; + + return { + ...game, + ...convertSteamGameToCatalogueEntry(steamGame), + iconUrl, + }; +}; + registerEvent("getUser", getUser); diff --git a/src/main/events/user/unblock-user.ts b/src/main/events/user/unblock-user.ts index ac678dbdf..c604a0b56 100644 --- a/src/main/events/user/unblock-user.ts +++ b/src/main/events/user/unblock-user.ts @@ -5,7 +5,7 @@ const unblockUser = async ( _event: Electron.IpcMainInvokeEvent, userId: string ) => { - await HydraApi.post(`/user/${userId}/unblock`); + await HydraApi.post(`/users/${userId}/unblock`); }; registerEvent("unblockUser", unblockUser); diff --git a/src/main/migrations/1715900413313-fix_repack_uploadDate.ts b/src/main/migrations/1715900413313-fix_repack_uploadDate.ts deleted file mode 100644 index e9d0a6c23..000000000 --- a/src/main/migrations/1715900413313-fix_repack_uploadDate.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class FixRepackUploadDate1715900413313 implements MigrationInterface { - public async up(_: QueryRunner): Promise { - return; - } - - public async down(_: QueryRunner): Promise { - return; - } -} diff --git a/src/main/migrations/1716776027208-alter_lastTimePlayed_to_datime.ts b/src/main/migrations/1716776027208-alter_lastTimePlayed_to_datime.ts deleted file mode 100644 index 6a5629157..000000000 --- a/src/main/migrations/1716776027208-alter_lastTimePlayed_to_datime.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Game } from "@main/entity"; -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class AlterLastTimePlayedToDatime1716776027208 - implements MigrationInterface -{ - public async up(queryRunner: QueryRunner): Promise { - // 2024-05-27 02:08:17 - // Mon, 27 May 2024 02:08:17 GMT - const updateLastTimePlayedValues = ` - UPDATE game SET lastTimePlayed = (SELECT - SUBSTR(lastTimePlayed, 13, 4) || '-' || -- Year - CASE SUBSTR(lastTimePlayed, 9, 3) - WHEN 'Jan' THEN '01' - WHEN 'Feb' THEN '02' - WHEN 'Mar' THEN '03' - WHEN 'Apr' THEN '04' - WHEN 'May' THEN '05' - WHEN 'Jun' THEN '06' - WHEN 'Jul' THEN '07' - WHEN 'Aug' THEN '08' - WHEN 'Sep' THEN '09' - WHEN 'Oct' THEN '10' - WHEN 'Nov' THEN '11' - WHEN 'Dec' THEN '12' - END || '-' || -- Month - SUBSTR(lastTimePlayed, 6, 2) || ' ' || -- Day - SUBSTR(lastTimePlayed, 18, 8) -- hh:mm:ss; - FROM game) - WHERE lastTimePlayed IS NOT NULL; - `; - - await queryRunner.query(updateLastTimePlayedValues); - } - - public async down(queryRunner: QueryRunner): Promise { - const queryBuilder = queryRunner.manager.createQueryBuilder(Game, "game"); - - const result = await queryBuilder.getMany(); - - for (const game of result) { - if (!game.lastTimePlayed) continue; - await queryRunner.query( - `UPDATE game set lastTimePlayed = ? WHERE id = ?;`, - [game.lastTimePlayed.toUTCString(), game.id] - ); - } - } -} diff --git a/src/main/migrations/1724081695967-Hydra_2_0_3.ts b/src/main/migrations/1724081695967-Hydra_2_0_3.ts new file mode 100644 index 000000000..5ab18acb1 --- /dev/null +++ b/src/main/migrations/1724081695967-Hydra_2_0_3.ts @@ -0,0 +1,50 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Hydra2031724081695967 implements MigrationInterface { + name = 'Hydra2031724081695967' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE IF NOT EXISTS "game" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "objectID" text NOT NULL, "remoteId" text, "title" text NOT NULL, "iconUrl" text, "folderName" text, "downloadPath" text, "executablePath" text, "playTimeInMilliseconds" integer NOT NULL DEFAULT (0), "shop" text NOT NULL, "status" text, "downloader" integer NOT NULL DEFAULT (1), "progress" float NOT NULL DEFAULT (0), "bytesDownloaded" integer NOT NULL DEFAULT (0), "lastTimePlayed" datetime, "fileSize" float NOT NULL DEFAULT (0), "uri" text, "isDeleted" boolean NOT NULL DEFAULT (0), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "repackId" integer, CONSTRAINT "UQ_04293f46e8db3deaec8dfb69264" UNIQUE ("objectID"), CONSTRAINT "UQ_6dac8c3148e141251a4864e94d4" UNIQUE ("remoteId"), CONSTRAINT "REL_0c1d6445ad047d9bbd256f961f" UNIQUE ("repackId"))`); + await queryRunner.query(`CREATE TABLE IF NOT EXISTS "download_source" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "url" text, "name" text NOT NULL, "etag" text, "downloadCount" integer NOT NULL DEFAULT (0), "status" text NOT NULL DEFAULT (0), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "UQ_aec2879321a87e9bb2ed477981a" UNIQUE ("url"))`); + await queryRunner.query(`CREATE TABLE IF NOT EXISTS "repack" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "title" text NOT NULL, "magnet" text NOT NULL, "page" integer, "repacker" text NOT NULL, "fileSize" text NOT NULL, "uploadDate" datetime NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "downloadSourceId" integer, CONSTRAINT "UQ_a46a68496754a4d429ddf9d48ec" UNIQUE ("title"), CONSTRAINT "UQ_5e8d57798643e693bced32095d2" UNIQUE ("magnet"))`); + await queryRunner.query(`CREATE TABLE IF NOT EXISTS "user_preferences" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "downloadsPath" text, "language" text NOT NULL DEFAULT ('en'), "realDebridApiToken" text, "downloadNotificationsEnabled" boolean NOT NULL DEFAULT (0), "repackUpdatesNotificationsEnabled" boolean NOT NULL DEFAULT (0), "preferQuitInsteadOfHiding" boolean NOT NULL DEFAULT (0), "runAtStartup" boolean NOT NULL DEFAULT (0), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')))`); + await queryRunner.query(`CREATE TABLE IF NOT EXISTS "game_shop_cache" ("objectID" text PRIMARY KEY NOT NULL, "shop" text NOT NULL, "serializedData" text, "howLongToBeatSerializedData" text, "language" text, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')))`); + await queryRunner.query(`CREATE TABLE IF NOT EXISTS "download_queue" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "gameId" integer, CONSTRAINT "REL_aed852c94d9ded617a7a07f541" UNIQUE ("gameId"))`); + await queryRunner.query(`CREATE TABLE IF NOT EXISTS "user_auth" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "userId" text NOT NULL DEFAULT (''), "displayName" text NOT NULL DEFAULT (''), "profileImageUrl" text, "accessToken" text NOT NULL DEFAULT (''), "refreshToken" text NOT NULL DEFAULT (''), "tokenExpirationTimestamp" integer NOT NULL DEFAULT (0), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')))`); + await queryRunner.query(`CREATE TABLE IF NOT EXISTS "temporary_game" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "objectID" text NOT NULL, "remoteId" text, "title" text NOT NULL, "iconUrl" text, "folderName" text, "downloadPath" text, "executablePath" text, "playTimeInMilliseconds" integer NOT NULL DEFAULT (0), "shop" text NOT NULL, "status" text, "downloader" integer NOT NULL DEFAULT (1), "progress" float NOT NULL DEFAULT (0), "bytesDownloaded" integer NOT NULL DEFAULT (0), "lastTimePlayed" datetime, "fileSize" float NOT NULL DEFAULT (0), "uri" text, "isDeleted" boolean NOT NULL DEFAULT (0), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "repackId" integer, CONSTRAINT "UQ_04293f46e8db3deaec8dfb69264" UNIQUE ("objectID"), CONSTRAINT "UQ_6dac8c3148e141251a4864e94d4" UNIQUE ("remoteId"), CONSTRAINT "REL_0c1d6445ad047d9bbd256f961f" UNIQUE ("repackId"), CONSTRAINT "FK_0c1d6445ad047d9bbd256f961f6" FOREIGN KEY ("repackId") REFERENCES "repack" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_game"("id", "objectID", "remoteId", "title", "iconUrl", "folderName", "downloadPath", "executablePath", "playTimeInMilliseconds", "shop", "status", "downloader", "progress", "bytesDownloaded", "lastTimePlayed", "fileSize", "uri", "isDeleted", "createdAt", "updatedAt", "repackId") SELECT "id", "objectID", "remoteId", "title", "iconUrl", "folderName", "downloadPath", "executablePath", "playTimeInMilliseconds", "shop", "status", "downloader", "progress", "bytesDownloaded", "lastTimePlayed", "fileSize", "uri", "isDeleted", "createdAt", "updatedAt", "repackId" FROM "game"`); + await queryRunner.query(`DROP TABLE "game"`); + await queryRunner.query(`ALTER TABLE "temporary_game" RENAME TO "game"`); + await queryRunner.query(`CREATE TABLE IF NOT EXISTS "temporary_repack" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "title" text NOT NULL, "magnet" text NOT NULL, "page" integer, "repacker" text NOT NULL, "fileSize" text NOT NULL, "uploadDate" datetime NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "downloadSourceId" integer, CONSTRAINT "UQ_a46a68496754a4d429ddf9d48ec" UNIQUE ("title"), CONSTRAINT "UQ_5e8d57798643e693bced32095d2" UNIQUE ("magnet"), CONSTRAINT "FK_13f131029be1dd361fd3cd9c2a6" FOREIGN KEY ("downloadSourceId") REFERENCES "download_source" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_repack"("id", "title", "magnet", "page", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId") SELECT "id", "title", "magnet", "page", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId" FROM "repack"`); + await queryRunner.query(`DROP TABLE "repack"`); + await queryRunner.query(`ALTER TABLE "temporary_repack" RENAME TO "repack"`); + await queryRunner.query(`CREATE TABLE IF NOT EXISTS "temporary_download_queue" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "gameId" integer, CONSTRAINT "REL_aed852c94d9ded617a7a07f541" UNIQUE ("gameId"), CONSTRAINT "FK_aed852c94d9ded617a7a07f5415" FOREIGN KEY ("gameId") REFERENCES "game" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_download_queue"("id", "createdAt", "updatedAt", "gameId") SELECT "id", "createdAt", "updatedAt", "gameId" FROM "download_queue"`); + await queryRunner.query(`DROP TABLE "download_queue"`); + await queryRunner.query(`ALTER TABLE "temporary_download_queue" RENAME TO "download_queue"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "download_queue" RENAME TO "temporary_download_queue"`); + await queryRunner.query(`CREATE TABLE IF NOT EXISTS "download_queue" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "gameId" integer, CONSTRAINT "REL_aed852c94d9ded617a7a07f541" UNIQUE ("gameId"))`); + await queryRunner.query(`INSERT INTO "download_queue"("id", "createdAt", "updatedAt", "gameId") SELECT "id", "createdAt", "updatedAt", "gameId" FROM "temporary_download_queue"`); + await queryRunner.query(`DROP TABLE "temporary_download_queue"`); + await queryRunner.query(`ALTER TABLE "repack" RENAME TO "temporary_repack"`); + await queryRunner.query(`CREATE TABLE IF NOT EXISTS "repack" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "title" text NOT NULL, "magnet" text NOT NULL, "page" integer, "repacker" text NOT NULL, "fileSize" text NOT NULL, "uploadDate" datetime NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "downloadSourceId" integer, CONSTRAINT "UQ_a46a68496754a4d429ddf9d48ec" UNIQUE ("title"), CONSTRAINT "UQ_5e8d57798643e693bced32095d2" UNIQUE ("magnet"))`); + await queryRunner.query(`INSERT INTO "repack"("id", "title", "magnet", "page", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId") SELECT "id", "title", "magnet", "page", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId" FROM "temporary_repack"`); + await queryRunner.query(`DROP TABLE "temporary_repack"`); + await queryRunner.query(`ALTER TABLE "game" RENAME TO "temporary_game"`); + await queryRunner.query(`CREATE TABLE IF NOT EXISTS "game" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "objectID" text NOT NULL, "remoteId" text, "title" text NOT NULL, "iconUrl" text, "folderName" text, "downloadPath" text, "executablePath" text, "playTimeInMilliseconds" integer NOT NULL DEFAULT (0), "shop" text NOT NULL, "status" text, "downloader" integer NOT NULL DEFAULT (1), "progress" float NOT NULL DEFAULT (0), "bytesDownloaded" integer NOT NULL DEFAULT (0), "lastTimePlayed" datetime, "fileSize" float NOT NULL DEFAULT (0), "uri" text, "isDeleted" boolean NOT NULL DEFAULT (0), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "repackId" integer, CONSTRAINT "UQ_04293f46e8db3deaec8dfb69264" UNIQUE ("objectID"), CONSTRAINT "UQ_6dac8c3148e141251a4864e94d4" UNIQUE ("remoteId"), CONSTRAINT "REL_0c1d6445ad047d9bbd256f961f" UNIQUE ("repackId"))`); + await queryRunner.query(`INSERT INTO "game"("id", "objectID", "remoteId", "title", "iconUrl", "folderName", "downloadPath", "executablePath", "playTimeInMilliseconds", "shop", "status", "downloader", "progress", "bytesDownloaded", "lastTimePlayed", "fileSize", "uri", "isDeleted", "createdAt", "updatedAt", "repackId") SELECT "id", "objectID", "remoteId", "title", "iconUrl", "folderName", "downloadPath", "executablePath", "playTimeInMilliseconds", "shop", "status", "downloader", "progress", "bytesDownloaded", "lastTimePlayed", "fileSize", "uri", "isDeleted", "createdAt", "updatedAt", "repackId" FROM "temporary_game"`); + await queryRunner.query(`DROP TABLE "temporary_game"`); + await queryRunner.query(`DROP TABLE "user_auth"`); + await queryRunner.query(`DROP TABLE "download_queue"`); + await queryRunner.query(`DROP TABLE "game_shop_cache"`); + await queryRunner.query(`DROP TABLE "user_preferences"`); + await queryRunner.query(`DROP TABLE "repack"`); + await queryRunner.query(`DROP TABLE "download_source"`); + await queryRunner.query(`DROP TABLE "game"`); + } + +} diff --git a/src/main/migrations/1724081984535-DowloadsRefactor.ts b/src/main/migrations/1724081984535-DowloadsRefactor.ts new file mode 100644 index 000000000..3afc8444c --- /dev/null +++ b/src/main/migrations/1724081984535-DowloadsRefactor.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class DowloadsRefactor1724081984535 implements MigrationInterface { + name = 'DowloadsRefactor1724081984535' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "temporary_repack" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "title" text NOT NULL, "magnet" text NOT NULL, "page" integer, "repacker" text NOT NULL, "fileSize" text NOT NULL, "uploadDate" datetime NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "downloadSourceId" integer, "uris" text NOT NULL DEFAULT ('[]'), CONSTRAINT "UQ_5e8d57798643e693bced32095d2" UNIQUE ("magnet"), CONSTRAINT "UQ_a46a68496754a4d429ddf9d48ec" UNIQUE ("title"), CONSTRAINT "FK_13f131029be1dd361fd3cd9c2a6" FOREIGN KEY ("downloadSourceId") REFERENCES "download_source" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_repack"("id", "title", "magnet", "page", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId") SELECT "id", "title", "magnet", "page", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId" FROM "repack"`); + await queryRunner.query(`DROP TABLE "repack"`); + await queryRunner.query(`ALTER TABLE "temporary_repack" RENAME TO "repack"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "repack" RENAME TO "temporary_repack"`); + await queryRunner.query(`CREATE TABLE "repack" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "title" text NOT NULL, "magnet" text NOT NULL, "page" integer, "repacker" text NOT NULL, "fileSize" text NOT NULL, "uploadDate" datetime NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "downloadSourceId" integer, CONSTRAINT "UQ_5e8d57798643e693bced32095d2" UNIQUE ("magnet"), CONSTRAINT "UQ_a46a68496754a4d429ddf9d48ec" UNIQUE ("title"), CONSTRAINT "FK_13f131029be1dd361fd3cd9c2a6" FOREIGN KEY ("downloadSourceId") REFERENCES "download_source" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "repack"("id", "title", "magnet", "page", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId") SELECT "id", "title", "magnet", "page", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId" FROM "temporary_repack"`); + await queryRunner.query(`DROP TABLE "temporary_repack"`); + } + +} diff --git a/src/main/migrations/index.ts b/src/main/migrations/index.ts index c0c96e45e..5546bce00 100644 --- a/src/main/migrations/index.ts +++ b/src/main/migrations/index.ts @@ -1,7 +1,2 @@ -import { FixRepackUploadDate1715900413313 } from "./1715900413313-fix_repack_uploadDate"; -import { AlterLastTimePlayedToDatime1716776027208 } from "./1716776027208-alter_lastTimePlayed_to_datime"; - -export default [ - FixRepackUploadDate1715900413313, - AlterLastTimePlayedToDatime1716776027208, -]; +export * from "./1724081695967-Hydra_2_0_3"; +export * from "./1724081984535-DowloadsRefactor"; diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 6f0e19051..120d27aca 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -77,54 +77,54 @@ export class HydraApi { baseURL: import.meta.env.MAIN_VITE_API_URL, }); - // this.instance.interceptors.request.use( - // (request) => { - // logger.log(" ---- REQUEST -----"); - // logger.log(request.method, request.url, request.params, request.data); - // return request; - // }, - // (error) => { - // logger.error("request error", error); - // return Promise.reject(error); - // } - // ); - - // this.instance.interceptors.response.use( - // (response) => { - // logger.log(" ---- RESPONSE -----"); - // logger.log( - // response.status, - // response.config.method, - // response.config.url, - // response.data - // ); - // return response; - // }, - // (error) => { - // logger.error(" ---- RESPONSE ERROR -----"); - - // const { config } = error; - - // logger.error( - // config.method, - // config.baseURL, - // config.url, - // config.headers, - // config.data - // ); - - // if (error.response) { - // logger.error("Response", error.response.status, error.response.data); - // } else if (error.request) { - // logger.error("Request", error.request); - // } else { - // logger.error("Error", error.message); - // } - - // logger.error(" ----- END RESPONSE ERROR -------"); - // return Promise.reject(error); - // } - // ); + this.instance.interceptors.request.use( + (request) => { + logger.log(" ---- REQUEST -----"); + logger.log(request.method, request.url, request.params, request.data); + return request; + }, + (error) => { + logger.error("request error", error); + return Promise.reject(error); + } + ); + + this.instance.interceptors.response.use( + (response) => { + logger.log(" ---- RESPONSE -----"); + logger.log( + response.status, + response.config.method, + response.config.url, + response.data + ); + return response; + }, + (error) => { + logger.error(" ---- RESPONSE ERROR -----"); + + const { config } = error; + + logger.error( + config.method, + config.baseURL, + config.url, + config.headers, + config.data + ); + + if (error.response) { + logger.error("Response", error.response.status, error.response.data); + } else if (error.request) { + logger.error("Request", error.request); + } else { + logger.error("Error", error.message); + } + + logger.error(" ----- END RESPONSE ERROR -------"); + return Promise.reject(error); + } + ); const userAuth = await userAuthRepository.findOne({ where: { id: 1 }, diff --git a/src/main/services/library-sync/create-game.ts b/src/main/services/library-sync/create-game.ts index b66a1897a..6699788ce 100644 --- a/src/main/services/library-sync/create-game.ts +++ b/src/main/services/library-sync/create-game.ts @@ -3,7 +3,7 @@ import { HydraApi } from "../hydra-api"; import { gameRepository } from "@main/repository"; export const createGame = async (game: Game) => { - HydraApi.post(`/games`, { + HydraApi.post(`/profile/games`, { objectId: game.objectID, playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds), shop: game.shop, diff --git a/src/main/services/library-sync/merge-with-remote-games.ts b/src/main/services/library-sync/merge-with-remote-games.ts index 2a6b5bb59..2b3f51b39 100644 --- a/src/main/services/library-sync/merge-with-remote-games.ts +++ b/src/main/services/library-sync/merge-with-remote-games.ts @@ -4,7 +4,7 @@ import { steamGamesWorker } from "@main/workers"; import { getSteamAppAsset } from "@main/helpers"; export const mergeWithRemoteGames = async () => { - return HydraApi.get("/games") + return HydraApi.get("/profile/games") .then(async (response) => { for (const game of response) { const localGame = await gameRepository.findOne({ diff --git a/src/main/services/library-sync/update-game-playtime.ts b/src/main/services/library-sync/update-game-playtime.ts index 39206a128..5cfc4103c 100644 --- a/src/main/services/library-sync/update-game-playtime.ts +++ b/src/main/services/library-sync/update-game-playtime.ts @@ -6,7 +6,7 @@ export const updateGamePlaytime = async ( deltaInMillis: number, lastTimePlayed: Date ) => { - HydraApi.put(`/games/${game.remoteId}`, { + HydraApi.put(`/profile/games/${game.remoteId}`, { playTimeDeltaInSeconds: Math.trunc(deltaInMillis / 1000), lastTimePlayed, }).catch(() => {}); diff --git a/src/main/services/library-sync/upload-games-batch.ts b/src/main/services/library-sync/upload-games-batch.ts index 88f023758..22dc595ed 100644 --- a/src/main/services/library-sync/upload-games-batch.ts +++ b/src/main/services/library-sync/upload-games-batch.ts @@ -14,7 +14,7 @@ export const uploadGamesBatch = async () => { for (const chunk of gamesChunks) { await HydraApi.post( - "/games/batch", + "/profile/games/batch", chunk.map((game) => { return { objectId: game.objectID, diff --git a/src/main/services/main-loop.ts b/src/main/services/main-loop.ts index ca72707fd..f2ec51ba7 100644 --- a/src/main/services/main-loop.ts +++ b/src/main/services/main-loop.ts @@ -10,6 +10,6 @@ export const startMainLoop = async () => { DownloadManager.watchDownloads(), ]); - await sleep(500); + await sleep(1000); } }; diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts index 0f7efa627..080f1efca 100644 --- a/src/main/services/process-watcher.ts +++ b/src/main/services/process-watcher.ts @@ -4,12 +4,16 @@ import { WindowManager } from "./window-manager"; import { createGame, updateGamePlaytime } from "./library-sync"; import { GameRunning } from "@types"; import { PythonInstance } from "./download"; +import { Game } from "@main/entity"; export const gamesPlaytime = new Map< number, - { lastTick: number; firstTick: number } + { lastTick: number; firstTick: number; lastSyncTick: number } >(); +const TICKS_TO_UPDATE_API = 120; +let currentTick = 1; + export const watchProcesses = async () => { const games = await gameRepository.find({ where: { @@ -30,48 +34,17 @@ export const watchProcesses = async () => { if (gameProcess) { if (gamesPlaytime.has(game.id)) { - const gamePlaytime = gamesPlaytime.get(game.id)!; - - const zero = gamePlaytime.lastTick; - const delta = performance.now() - zero; - - await gameRepository.update(game.id, { - playTimeInMilliseconds: game.playTimeInMilliseconds + delta, - lastTimePlayed: new Date(), - }); - - gamesPlaytime.set(game.id, { - ...gamePlaytime, - lastTick: performance.now(), - }); + onTickGame(game); } else { - if (game.remoteId) { - updateGamePlaytime(game, 0, new Date()); - } else { - createGame({ ...game, lastTimePlayed: new Date() }); - } - - gamesPlaytime.set(game.id, { - lastTick: performance.now(), - firstTick: performance.now(), - }); + onOpenGame(game); } } else if (gamesPlaytime.has(game.id)) { - const gamePlaytime = gamesPlaytime.get(game.id)!; - gamesPlaytime.delete(game.id); - - if (game.remoteId) { - updateGamePlaytime( - game, - performance.now() - gamePlaytime.firstTick, - game.lastTimePlayed! - ); - } else { - createGame(game); - } + onCloseGame(game); } } + currentTick++; + if (WindowManager.mainWindow) { const gamesRunning = Array.from(gamesPlaytime.entries()).map((entry) => { return { @@ -86,3 +59,68 @@ export const watchProcesses = async () => { ); } }; + +function onOpenGame(game: Game) { + const now = performance.now(); + + gamesPlaytime.set(game.id, { + lastTick: now, + firstTick: now, + lastSyncTick: now, + }); + + if (game.remoteId) { + updateGamePlaytime(game, 0, new Date()); + } else { + createGame({ ...game, lastTimePlayed: new Date() }); + } +} + +function onTickGame(game: Game) { + const now = performance.now(); + const gamePlaytime = gamesPlaytime.get(game.id)!; + + const delta = now - gamePlaytime.lastTick; + + gameRepository.update(game.id, { + playTimeInMilliseconds: game.playTimeInMilliseconds + delta, + lastTimePlayed: new Date(), + }); + + gamesPlaytime.set(game.id, { + ...gamePlaytime, + lastTick: now, + }); + + if (currentTick % TICKS_TO_UPDATE_API === 0) { + if (game.remoteId) { + updateGamePlaytime( + game, + now - gamePlaytime.lastSyncTick, + game.lastTimePlayed! + ); + } else { + createGame(game); + } + + gamesPlaytime.set(game.id, { + ...gamePlaytime, + lastSyncTick: now, + }); + } +} + +const onCloseGame = (game: Game) => { + const gamePlaytime = gamesPlaytime.get(game.id)!; + gamesPlaytime.delete(game.id); + + if (game.remoteId) { + updateGamePlaytime( + game, + performance.now() - gamePlaytime.firstTick, + game.lastTimePlayed! + ); + } else { + createGame(game); + } +}; diff --git a/src/renderer/src/components/sidebar/sidebar-profile.tsx b/src/renderer/src/components/sidebar/sidebar-profile.tsx index 81736e374..069831cb9 100644 --- a/src/renderer/src/components/sidebar/sidebar-profile.tsx +++ b/src/renderer/src/components/sidebar/sidebar-profile.tsx @@ -78,7 +78,7 @@ export function SidebarProfile() { )} - {userDetails && gameRunning && ( + {userDetails && gameRunning?.iconUrl && ( {gameRunning.title} { - listContainer.current?.addEventListener("scroll", handleScroll); - return () => - listContainer.current?.removeEventListener("scroll", handleScroll); + const container = listContainer.current; + container?.addEventListener("scroll", handleScroll); + return () => container?.removeEventListener("scroll", handleScroll); }, [isLoading]); const reloadList = () => { diff --git a/src/renderer/src/pages/user/user-content.tsx b/src/renderer/src/pages/user/user-content.tsx index b16c49bb5..c334389e2 100644 --- a/src/renderer/src/pages/user/user-content.tsx +++ b/src/renderer/src/pages/user/user-content.tsx @@ -1,4 +1,9 @@ -import { FriendRequestAction, UserGame, UserProfile } from "@types"; +import { + FriendRequestAction, + GameRunning, + UserGame, + UserProfile, +} from "@types"; import cn from "classnames"; import * as styles from "./user.css"; import { SPACING_UNIT, vars } from "@renderer/theme.css"; @@ -44,7 +49,6 @@ export function UserContent({ updateUserProfile, }: ProfileContentProps) { const { t, i18n } = useTranslation("user_profile"); - const { userDetails, profileBackground, @@ -64,6 +68,7 @@ export function UserContent({ useState(false); const [showSignOutModal, setShowSignOutModal] = useState(false); const [showUserBlockModal, setShowUserBlockModal] = useState(false); + const [currentGame, setCurrentGame] = useState(null); const { gameRunning } = useAppSelector((state) => state.gameRunning); @@ -113,6 +118,15 @@ export function UserContent({ const isMe = userDetails?.id == userProfile.id; + useEffect(() => { + if (isMe && gameRunning) { + setCurrentGame(gameRunning); + return; + } + + setCurrentGame(userProfile.currentGame); + }, [gameRunning, isMe, userProfile.currentGame]); + useEffect(() => { if (isMe) fetchFriendRequests(); }, [isMe, fetchFriendRequests]); @@ -284,10 +298,10 @@ export function UserContent({ position: "relative", }} > - {gameRunning && isMe && ( + {currentGame && ( {gameRunning.title} )} @@ -315,7 +329,7 @@ export function UserContent({

{userProfile.displayName}

- {isMe && gameRunning && ( + {currentGame && (
- - {gameRunning.title} + + {currentGame.title}
{t("playing_for", { amount: formatDiffInMillis( - gameRunning.sessionDurationInMillis, + currentGame.sessionDurationInMillis, new Date() ), })} diff --git a/src/renderer/src/pages/user/user-profile-settings-modal/user-block-list.tsx b/src/renderer/src/pages/user/user-profile-settings-modal/user-block-list.tsx index c062eabb7..0790b7256 100644 --- a/src/renderer/src/pages/user/user-profile-settings-modal/user-block-list.tsx +++ b/src/renderer/src/pages/user/user-profile-settings-modal/user-block-list.tsx @@ -51,9 +51,9 @@ export const UserEditProfileBlockList = () => { }; useEffect(() => { - listContainer.current?.addEventListener("scroll", handleScroll); - return () => - listContainer.current?.removeEventListener("scroll", handleScroll); + const container = listContainer.current; + container?.addEventListener("scroll", handleScroll); + return () => container?.removeEventListener("scroll", handleScroll); }, [isLoading]); const reloadList = () => { diff --git a/src/types/index.ts b/src/types/index.ts index 46ab5421b..3260d2743 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -141,9 +141,9 @@ export interface Game { export type LibraryGame = Omit; export interface GameRunning { - id: number; + id?: number; title: string; - iconUrl: string; + iconUrl: string | null; objectID: string; shop: GameShop; sessionDurationInMillis: number; @@ -318,6 +318,7 @@ export interface UserProfile { friends: UserFriend[]; totalFriends: number; relation: UserRelation | null; + currentGame: GameRunning | null; } export interface UpdateProfileProps {