diff --git a/.eslintignore b/.eslintignore index a6f34fea7..a9960b130 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,3 +2,4 @@ node_modules dist out .gitignore +migration.stub diff --git a/.prettierignore b/.prettierignore index 9b6e9df69..05d298a1e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,4 +5,3 @@ pnpm-lock.yaml LICENSE.md tsconfig.json tsconfig.*.json -src/main/migrations diff --git a/package.json b/package.json index aa77084eb..ff05b2ed2 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "build:mac": "electron-vite build && electron-builder --mac", "build:linux": "electron-vite build && electron-builder --linux", "prepare": "husky", - "typeorm:migration-create": "yarn typeorm migration:create" + "knex:migrate:make": "knex --knexfile src/main/knexfile.ts migrate:make --esm" }, "dependencies": { "@electron-toolkit/preload": "^3.0.0", @@ -43,7 +43,7 @@ "@vanilla-extract/recipes": "^0.5.2", "auto-launch": "^5.0.6", "axios": "^1.6.8", - "better-sqlite3": "^9.5.0", + "better-sqlite3": "^11.2.1", "check-disk-space": "^3.4.0", "classnames": "^2.5.1", "color": "^4.2.3", @@ -61,6 +61,7 @@ "iso-639-1": "3.1.2", "jsdom": "^24.0.0", "jsonwebtoken": "^9.0.2", + "knex": "^3.1.0", "lodash-es": "^4.17.21", "lottie-react": "^2.4.0", "parse-torrent": "^11.0.16", @@ -106,6 +107,7 @@ "prettier": "^3.2.4", "react": "^18.2.0", "react-dom": "^18.2.0", + "ts-node": "^10.9.2", "typescript": "^5.3.3", "vite": "^5.0.12", "vite-plugin-svgr": "^4.2.0" diff --git a/src/main/data-source.ts b/src/main/data-source.ts index 446ccbdc2..29c72f8c2 100644 --- a/src/main/data-source.ts +++ b/src/main/data-source.ts @@ -10,7 +10,6 @@ import { } from "@main/entity"; import { databasePath } from "./constants"; -import * as migrations from "./migrations"; export const dataSource = new DataSource({ type: "better-sqlite3", @@ -23,7 +22,6 @@ export const dataSource = new DataSource({ DownloadQueue, UserAuth, ], - synchronize: true, + synchronize: false, database: databasePath, - migrations, }); diff --git a/src/main/entity/repack.entity.ts b/src/main/entity/repack.entity.ts index ff3f16cb0..36de2a7c9 100644 --- a/src/main/entity/repack.entity.ts +++ b/src/main/entity/repack.entity.ts @@ -22,12 +22,6 @@ export class Repack { @Column("text", { unique: true }) magnet: string; - /** - * @deprecated Direct scraping capability has been removed - */ - @Column("int", { nullable: true }) - page: number; - @Column("text") repacker: string; diff --git a/src/main/hydra.dev.db b/src/main/hydra.dev.db new file mode 100644 index 000000000..d8c65f28e Binary files /dev/null and b/src/main/hydra.dev.db differ diff --git a/src/main/index.ts b/src/main/index.ts index 11eb479d1..24c367fd9 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -9,6 +9,7 @@ import { logger, PythonInstance, WindowManager } from "@main/services"; import { dataSource } from "@main/data-source"; import * as resources from "@locales"; import { userPreferencesRepository } from "@main/repository"; +import { knexClient, migrationConfig } from "./knex-client"; const { autoUpdater } = updater; @@ -52,6 +53,18 @@ if (process.defaultApp) { app.setAsDefaultProtocolClient(PROTOCOL); } +const runMigrations = async () => { + await knexClient.migrate.list(migrationConfig).then((result) => { + logger.log( + "Migrations to run:", + result[1].map((migration) => migration.name) + ); + }); + + await knexClient.migrate.latest(migrationConfig); + await knexClient.destroy(); +}; + // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. @@ -63,8 +76,15 @@ app.whenReady().then(async () => { return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString()); }); + await runMigrations() + .then(() => { + logger.log("Migrations executed successfully"); + }) + .catch((err) => { + logger.log("Migrations failed to run:", err); + }); + await dataSource.initialize(); - await dataSource.runMigrations(); await import("./main"); diff --git a/src/main/knex-client.ts b/src/main/knex-client.ts new file mode 100644 index 000000000..031760f63 --- /dev/null +++ b/src/main/knex-client.ts @@ -0,0 +1,29 @@ +import knex, { Knex } from "knex"; +import { databasePath } from "./constants"; +import { Hydra2_0_3 } from "./migrations/20240830143811_Hydra_2_0_3"; +import { RepackUris } from "./migrations/20240830143906_RepackUris"; + +export type HydraMigration = Knex.Migration & { name: string }; + +class MigrationSource implements Knex.MigrationSource { + getMigrations(): Promise { + return Promise.resolve([Hydra2_0_3, RepackUris]); + } + getMigrationName(migration: HydraMigration): string { + return migration.name; + } + getMigration(migration: HydraMigration): Promise { + return Promise.resolve(migration); + } +} + +export const knexClient = knex({ + client: "better-sqlite3", + connection: { + filename: databasePath, + }, +}); + +export const migrationConfig: Knex.MigratorConfig = { + migrationSource: new MigrationSource(), +}; diff --git a/src/main/knexfile.ts b/src/main/knexfile.ts new file mode 100644 index 000000000..df7972a9d --- /dev/null +++ b/src/main/knexfile.ts @@ -0,0 +1,10 @@ +const config = { + development: { + migrations: { + extension: "ts", + stub: "migrations/migration.stub", + }, + }, +}; + +export default config; diff --git a/src/main/migrations/1724081695967-Hydra_2_0_3.ts b/src/main/migrations/1724081695967-Hydra_2_0_3.ts deleted file mode 100644 index 5ab18acb1..000000000 --- a/src/main/migrations/1724081695967-Hydra_2_0_3.ts +++ /dev/null @@ -1,50 +0,0 @@ -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 deleted file mode 100644 index 3afc8444c..000000000 --- a/src/main/migrations/1724081984535-DowloadsRefactor.ts +++ /dev/null @@ -1,20 +0,0 @@ -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/20240830143811_Hydra_2_0_3.ts b/src/main/migrations/20240830143811_Hydra_2_0_3.ts new file mode 100644 index 000000000..6013f714e --- /dev/null +++ b/src/main/migrations/20240830143811_Hydra_2_0_3.ts @@ -0,0 +1,171 @@ +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"); + }, +}; diff --git a/src/main/migrations/20240830143906_RepackUris.ts b/src/main/migrations/20240830143906_RepackUris.ts new file mode 100644 index 000000000..0785d50df --- /dev/null +++ b/src/main/migrations/20240830143906_RepackUris.ts @@ -0,0 +1,58 @@ +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.createTable("temporary_repack", (table) => { + const timestamp = new Date().getTime(); + 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.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"); + table.text("uris").notNullable().defaultTo("[]"); + }); + await knex.raw( + `INSERT INTO "temporary_repack"("id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId") SELECT "id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId" FROM "repack"` + ); + await knex.schema.dropTable("repack"); + await knex.schema.renameTable("temporary_repack", "repack"); + }, + + down: async (knex: Knex) => { + await knex.schema.renameTable("repack", "temporary_repack"); + await knex.schema.createTable("repack", (table) => { + table.increments("id").primary(); + table.text("title").notNullable().unique(); + table.text("magnet").notNullable().unique(); + 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.raw( + `INSERT INTO "repack"("id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId") SELECT "id", "title", "magnet", "repacker", "fileSize", "uploadDate", "createdAt", "updatedAt", "downloadSourceId" FROM "temporary_repack"` + ); + await knex.schema.dropTable("temporary_repack"); + }, +}; diff --git a/src/main/migrations/index.ts b/src/main/migrations/index.ts deleted file mode 100644 index 5546bce00..000000000 --- a/src/main/migrations/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./1724081695967-Hydra_2_0_3"; -export * from "./1724081984535-DowloadsRefactor"; diff --git a/src/main/migrations/migration.stub b/src/main/migrations/migration.stub new file mode 100644 index 000000000..9cb0cbaba --- /dev/null +++ b/src/main/migrations/migration.stub @@ -0,0 +1,11 @@ +import type { HydraMigration } from "@main/knex-client"; +import type { Knex } from "knex"; + +export const MigrationName: HydraMigration = { + name: "MigrationName", + up: async (knex: Knex) => { + await knex.schema.createTable("table_name", (table) => {}); + }, + + down: async (knex: Knex) => {}, +}; diff --git a/src/main/services/how-long-to-beat.ts b/src/main/services/how-long-to-beat.ts index 67e96942f..c7164d09d 100644 --- a/src/main/services/how-long-to-beat.ts +++ b/src/main/services/how-long-to-beat.ts @@ -2,6 +2,7 @@ import axios from "axios"; import { requestWebPage } from "@main/helpers"; import { HowLongToBeatCategory } from "@types"; import { formatName } from "@shared"; +import { logger } from "./logger"; export interface HowLongToBeatResult { game_id: number; @@ -13,22 +14,27 @@ export interface HowLongToBeatSearchResponse { } export const searchHowLongToBeat = async (gameName: string) => { - const response = await axios.post( - "https://howlongtobeat.com/api/search", - { - searchType: "games", - searchTerms: formatName(gameName).split(" "), - searchPage: 1, - size: 100, - }, - { - headers: { - "User-Agent": - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", - Referer: "https://howlongtobeat.com/", + const response = await axios + .post( + "https://howlongtobeat.com/api/search", + { + searchType: "games", + searchTerms: formatName(gameName).split(" "), + searchPage: 1, + size: 100, }, - } - ); + { + headers: { + "User-Agent": + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", + Referer: "https://howlongtobeat.com/", + }, + } + ) + .catch((error) => { + logger.error("Error searching HowLongToBeat:", error?.response?.status); + return { data: { data: [] } }; + }); return response.data as HowLongToBeatSearchResponse; }; diff --git a/yarn.lock b/yarn.lock index 74b9a8a1a..9ffbf746f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2870,10 +2870,10 @@ bep53-range@^2.0.0: resolved "https://registry.npmjs.org/bep53-range/-/bep53-range-2.0.0.tgz" integrity sha512-sMm2sV5PRs0YOVk0LTKtjuIprVzxgTQUsrGX/7Yph2Rm4FO2Fqqtq7hNjsOB5xezM4v4+5rljCgK++UeQJZguA== -better-sqlite3@^9.5.0: - version "9.6.0" - resolved "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-9.6.0.tgz" - integrity sha512-yR5HATnqeYNVnkaUTf4bOP2dJSnyhP4puJN/QPRyx4YkBEEUxib422n2XzPqDEHjQQqazoYoADdAm5vE15+dAQ== +better-sqlite3@^11.2.1: + version "11.2.1" + resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-11.2.1.tgz#3c6b8a8e2e12444d380e811796b59c8aba012e03" + integrity sha512-Xbt1d68wQnUuFIEVsbt6V+RG30zwgbtCGQ4QOcXVrOH0FE4eHk64FWZ9NUfRHS4/x1PXqwz/+KOrnXD7f0WieA== dependencies: bindings "^1.5.0" prebuild-install "^7.1.1" @@ -3245,6 +3245,11 @@ color@^4.2.3: color-convert "^2.0.1" color-string "^1.9.0" +colorette@2.0.19: + version "2.0.19" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" + integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" @@ -3252,6 +3257,11 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +commander@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + commander@^5.0.0: version "5.1.0" resolved "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz" @@ -3462,7 +3472,7 @@ dayjs@^1.11.9: resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz" integrity sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg== -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: +debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -4101,6 +4111,11 @@ eslint@^8.56.0: strip-ansi "^6.0.1" text-table "^0.2.0" +esm@^3.2.25: + version "3.2.25" + resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" + integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== + espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz" @@ -4463,6 +4478,11 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + get-stdin@^9.0.0: version "9.0.0" resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz" @@ -4489,6 +4509,11 @@ get-symbol-description@^1.0.2: es-errors "^1.3.0" get-intrinsic "^1.2.4" +getopts@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.3.0.tgz#71e5593284807e03e2427449d4f6712a268666f4" + integrity sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA== + git-raw-commits@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-4.0.0.tgz" @@ -4909,6 +4934,11 @@ internal-slot@^1.0.7: hasown "^2.0.0" side-channel "^1.0.4" +interpret@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" + integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== + is-array-buffer@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz" @@ -5350,6 +5380,26 @@ keyv@^4.0.0, keyv@^4.5.3: dependencies: json-buffer "3.0.1" +knex@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/knex/-/knex-3.1.0.tgz#b6ddd5b5ad26a6315234a5b09ec38dc4a370bd8c" + integrity sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw== + dependencies: + colorette "2.0.19" + commander "^10.0.0" + debug "4.3.4" + escalade "^3.1.1" + esm "^3.2.25" + get-package-type "^0.1.0" + getopts "2.3.0" + interpret "^2.2.0" + lodash "^4.17.21" + pg-connection-string "2.6.2" + rechoir "^0.8.0" + resolve-from "^5.0.0" + tarn "^3.0.2" + tildify "2.0.0" + language-subtag-registry@^0.3.20: version "0.3.22" resolved "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz" @@ -5489,7 +5539,7 @@ lodash.upperfirst@^4.3.1: resolved "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz" integrity sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg== -lodash@^4.17.15: +lodash@^4.17.15, lodash@^4.17.21: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -6114,6 +6164,11 @@ pend@~1.2.0: resolved "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz" integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== +pg-connection-string@2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.2.tgz#713d82053de4e2bd166fab70cd4f26ad36aab475" + integrity sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA== + pg-int8@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" @@ -6457,6 +6512,13 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +rechoir@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" + integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== + dependencies: + resolve "^1.20.0" + redux-thunk@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz" @@ -6554,7 +6616,7 @@ resolve-from@^5.0.0: resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve@^1.22.1: +resolve@^1.20.0, resolve@^1.22.1: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -7083,6 +7145,11 @@ tar@^6.1.12: mkdirp "^1.0.3" yallist "^4.0.0" +tarn@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/tarn/-/tarn-3.0.2.tgz#73b6140fbb881b71559c4f8bfde3d9a4b3d27693" + integrity sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ== + temp-file@^3.4.0: version "3.4.0" resolved "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz" @@ -7120,6 +7187,11 @@ thenify-all@^1.0.0: resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== +tildify@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/tildify/-/tildify-2.0.0.tgz#f205f3674d677ce698b7067a99e949ce03b4754a" + integrity sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw== + tiny-typed-emitter@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz"