mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-21 10:03:56 +00:00
feat: adding download queue
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { DataSource } from "typeorm";
|
||||
import {
|
||||
DownloadQueue,
|
||||
DownloadSource,
|
||||
Game,
|
||||
GameShopCache,
|
||||
@@ -16,7 +17,14 @@ export const createDataSource = (
|
||||
) =>
|
||||
new DataSource({
|
||||
type: "better-sqlite3",
|
||||
entities: [Game, Repack, UserPreferences, GameShopCache, DownloadSource],
|
||||
entities: [
|
||||
Game,
|
||||
Repack,
|
||||
UserPreferences,
|
||||
GameShopCache,
|
||||
DownloadSource,
|
||||
DownloadQueue,
|
||||
],
|
||||
synchronize: true,
|
||||
database: databasePath,
|
||||
...options,
|
||||
|
||||
25
src/main/entity/download-queue.entity.ts
Normal file
25
src/main/entity/download-queue.entity.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
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;
|
||||
}
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
UpdateDateColumn,
|
||||
OneToMany,
|
||||
} from "typeorm";
|
||||
import { Repack } from "./repack.entity";
|
||||
import type { Repack } from "./repack.entity";
|
||||
|
||||
import { DownloadSourceStatus } from "@shared";
|
||||
|
||||
@Entity("download_source")
|
||||
@@ -26,7 +27,7 @@ export class DownloadSource {
|
||||
@Column("text", { default: DownloadSourceStatus.UpToDate })
|
||||
status: DownloadSourceStatus;
|
||||
|
||||
@OneToMany(() => Repack, (repack) => repack.downloadSource, { cascade: true })
|
||||
@OneToMany("Repack", "downloadSource", { cascade: true })
|
||||
repacks: Repack[];
|
||||
|
||||
@CreateDateColumn()
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Repack } from "./repack.entity";
|
||||
import type { GameShop } from "@types";
|
||||
import { Downloader } from "@shared";
|
||||
import type { Aria2Status } from "aria2";
|
||||
import type { DownloadQueue } from "./download-queue.entity";
|
||||
|
||||
@Entity("game")
|
||||
export class Game {
|
||||
@@ -66,10 +67,16 @@ export class Game {
|
||||
@Column("text", { nullable: true })
|
||||
uri: string | null;
|
||||
|
||||
@OneToOne(() => Repack, { nullable: true })
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@OneToOne("Repack", "game", { nullable: true })
|
||||
@JoinColumn()
|
||||
repack: Repack;
|
||||
|
||||
@OneToOne("DownloadQueue", "game")
|
||||
downloadQueue: DownloadQueue;
|
||||
|
||||
@Column("boolean", { default: false })
|
||||
isDeleted: boolean;
|
||||
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from "./repack.entity";
|
||||
export * from "./user-preferences.entity";
|
||||
export * from "./game-shop-cache.entity";
|
||||
export * from "./download-source.entity";
|
||||
export * from "./download-queue.entity";
|
||||
|
||||
@@ -19,6 +19,9 @@ export class Repack {
|
||||
@Column("text", { unique: true })
|
||||
magnet: string;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@Column("int", { nullable: true })
|
||||
page: number;
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ import { registerEvent } from "../register-event";
|
||||
|
||||
import type { GameShop } from "@types";
|
||||
import { getFileBase64, getSteamAppAsset } from "@main/helpers";
|
||||
import { stateManager } from "@main/state-manager";
|
||||
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
|
||||
const addGameToLibrary = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -27,9 +28,9 @@ const addGameToLibrary = async (
|
||||
)
|
||||
.then(async ({ affected }) => {
|
||||
if (!affected) {
|
||||
const steamGame = stateManager
|
||||
.getValue("steamGames")
|
||||
.find((game) => game.id === Number(objectID));
|
||||
const steamGame = await steamGamesWorker.run(Number(objectID), {
|
||||
name: "getById",
|
||||
});
|
||||
|
||||
const iconUrl = steamGame?.clientIcon
|
||||
? getSteamAppAsset("icon", objectID, steamGame.clientIcon)
|
||||
|
||||
@@ -6,8 +6,11 @@ const getLibrary = async () =>
|
||||
where: {
|
||||
isDeleted: false,
|
||||
},
|
||||
relations: {
|
||||
downloadQueue: true,
|
||||
},
|
||||
order: {
|
||||
updatedAt: "desc",
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
import { gameRepository } from "@main/repository";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
import { DownloadManager } from "@main/services";
|
||||
import { dataSource } from "@main/data-source";
|
||||
import { DownloadQueue, Game } from "@main/entity";
|
||||
|
||||
const cancelGameDownload = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
gameId: number
|
||||
) => {
|
||||
await DownloadManager.cancelDownload(gameId);
|
||||
await dataSource.transaction(async (transactionalEntityManager) => {
|
||||
await DownloadManager.cancelDownload(gameId);
|
||||
|
||||
await gameRepository.update(
|
||||
{
|
||||
id: gameId,
|
||||
},
|
||||
{
|
||||
status: "removed",
|
||||
bytesDownloaded: 0,
|
||||
progress: 0,
|
||||
}
|
||||
);
|
||||
await transactionalEntityManager.getRepository(DownloadQueue).delete({
|
||||
game: { id: gameId },
|
||||
});
|
||||
|
||||
await transactionalEntityManager.getRepository(Game).update(
|
||||
{
|
||||
id: gameId,
|
||||
},
|
||||
{
|
||||
status: "removed",
|
||||
bytesDownloaded: 0,
|
||||
progress: 0,
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
registerEvent("cancelGameDownload", cancelGameDownload);
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import { gameRepository } from "../../repository";
|
||||
|
||||
import { DownloadManager } from "@main/services";
|
||||
import { dataSource } from "@main/data-source";
|
||||
import { DownloadQueue, Game } from "@main/entity";
|
||||
|
||||
const pauseGameDownload = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
gameId: number
|
||||
) => {
|
||||
await DownloadManager.pauseDownload();
|
||||
await gameRepository.update({ id: gameId }, { status: "paused" });
|
||||
await dataSource.transaction(async (transactionalEntityManager) => {
|
||||
await DownloadManager.pauseDownload();
|
||||
|
||||
await transactionalEntityManager.getRepository(DownloadQueue).delete({
|
||||
game: { id: gameId },
|
||||
});
|
||||
|
||||
await transactionalEntityManager
|
||||
.getRepository(Game)
|
||||
.update({ id: gameId }, { status: "paused" });
|
||||
});
|
||||
};
|
||||
|
||||
registerEvent("pauseGameDownload", pauseGameDownload);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { gameRepository } from "../../repository";
|
||||
|
||||
import { DownloadManager } from "@main/services";
|
||||
import { dataSource } from "@main/data-source";
|
||||
import { Game } from "@main/entity";
|
||||
import { DownloadQueue, Game } from "@main/entity";
|
||||
|
||||
const resumeGameDownload = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -30,6 +30,14 @@ const resumeGameDownload = async (
|
||||
|
||||
await DownloadManager.resumeDownload(game);
|
||||
|
||||
await transactionalEntityManager
|
||||
.getRepository(DownloadQueue)
|
||||
.delete({ game: { id: gameId } });
|
||||
|
||||
await transactionalEntityManager
|
||||
.getRepository(DownloadQueue)
|
||||
.insert({ game: { id: gameId } });
|
||||
|
||||
await transactionalEntityManager
|
||||
.getRepository(Game)
|
||||
.update({ id: gameId }, { status: "active" });
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { gameRepository, repackRepository } from "@main/repository";
|
||||
import {
|
||||
downloadQueueRepository,
|
||||
gameRepository,
|
||||
repackRepository,
|
||||
} from "@main/repository";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
import type { StartGameDownloadPayload } from "@types";
|
||||
import { getFileBase64, getSteamAppAsset } from "@main/helpers";
|
||||
import { DownloadManager } from "@main/services";
|
||||
import { stateManager } from "@main/state-manager";
|
||||
|
||||
import { Not } from "typeorm";
|
||||
import { steamGamesWorker } from "@main/workers";
|
||||
|
||||
const startGameDownload = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -53,9 +58,9 @@ const startGameDownload = async (
|
||||
}
|
||||
);
|
||||
} else {
|
||||
const steamGame = stateManager
|
||||
.getValue("steamGames")
|
||||
.find((game) => game.id === Number(objectID));
|
||||
const steamGame = await steamGamesWorker.run(Number(objectID), {
|
||||
name: "getById",
|
||||
});
|
||||
|
||||
const iconUrl = steamGame?.clientIcon
|
||||
? getSteamAppAsset("icon", objectID, steamGame.clientIcon)
|
||||
@@ -89,6 +94,9 @@ const startGameDownload = async (
|
||||
},
|
||||
});
|
||||
|
||||
await downloadQueueRepository.delete({ game: { id: updatedGame!.id } });
|
||||
await downloadQueueRepository.insert({ game: { id: updatedGame!.id } });
|
||||
|
||||
await DownloadManager.startDownload(updatedGame!);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { DownloadManager, RepacksManager, startMainLoop } from "./services";
|
||||
import { gameRepository, userPreferencesRepository } from "./repository";
|
||||
import {
|
||||
downloadQueueRepository,
|
||||
userPreferencesRepository,
|
||||
} from "./repository";
|
||||
import { UserPreferences } from "./entity";
|
||||
import { RealDebridClient } from "./services/real-debrid";
|
||||
import { Not } from "typeorm";
|
||||
import { fetchDownloadSourcesAndUpdate } from "./helpers";
|
||||
|
||||
startMainLoop();
|
||||
@@ -15,15 +17,17 @@ const loadState = async (userPreferences: UserPreferences | null) => {
|
||||
if (userPreferences?.realDebridApiToken)
|
||||
RealDebridClient.authorize(userPreferences?.realDebridApiToken);
|
||||
|
||||
const game = await gameRepository.findOne({
|
||||
where: {
|
||||
status: "active",
|
||||
progress: Not(1),
|
||||
isDeleted: false,
|
||||
const [nextQueueItem] = await downloadQueueRepository.find({
|
||||
order: {
|
||||
id: "DESC",
|
||||
},
|
||||
relations: {
|
||||
game: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (game) DownloadManager.startDownload(game);
|
||||
if (nextQueueItem?.game.status === "active")
|
||||
DownloadManager.startDownload(nextQueueItem.game);
|
||||
|
||||
fetchDownloadSourcesAndUpdate();
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { dataSource } from "./data-source";
|
||||
import {
|
||||
DownloadQueue,
|
||||
DownloadSource,
|
||||
Game,
|
||||
GameShopCache,
|
||||
@@ -18,3 +19,5 @@ export const gameShopCacheRepository = dataSource.getRepository(GameShopCache);
|
||||
|
||||
export const downloadSourceRepository =
|
||||
dataSource.getRepository(DownloadSource);
|
||||
|
||||
export const downloadQueueRepository = dataSource.getRepository(DownloadQueue);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import Aria2, { StatusResponse } from "aria2";
|
||||
|
||||
import { gameRepository, userPreferencesRepository } from "@main/repository";
|
||||
import {
|
||||
downloadQueueRepository,
|
||||
gameRepository,
|
||||
userPreferencesRepository,
|
||||
} from "@main/repository";
|
||||
|
||||
import { WindowManager } from "./window-manager";
|
||||
import { RealDebridClient } from "./real-debrid";
|
||||
@@ -194,19 +198,6 @@ export class DownloadManager {
|
||||
where: { id: this.game.id, isDeleted: false },
|
||||
});
|
||||
|
||||
if (progress === 1 && this.game && !isDownloadingMetadata) {
|
||||
await this.publishNotification();
|
||||
|
||||
/*
|
||||
Only cancel bittorrent downloads to stop seeding
|
||||
*/
|
||||
if (status.bittorrent) {
|
||||
await this.cancelDownload(this.game.id);
|
||||
} else {
|
||||
this.clearCurrentDownload();
|
||||
}
|
||||
}
|
||||
|
||||
if (WindowManager.mainWindow && game) {
|
||||
if (!isNaN(progress))
|
||||
WindowManager.mainWindow.setProgressBar(progress === 1 ? -1 : progress);
|
||||
@@ -229,6 +220,32 @@ export class DownloadManager {
|
||||
JSON.parse(JSON.stringify(payload))
|
||||
);
|
||||
}
|
||||
|
||||
if (progress === 1 && this.game && !isDownloadingMetadata) {
|
||||
await this.publishNotification();
|
||||
|
||||
await downloadQueueRepository.delete({ game: this.game });
|
||||
|
||||
/*
|
||||
Only cancel bittorrent downloads to stop seeding
|
||||
*/
|
||||
if (status.bittorrent) {
|
||||
await this.cancelDownload(this.game.id);
|
||||
} else {
|
||||
this.clearCurrentDownload();
|
||||
}
|
||||
|
||||
const [nextQueueItem] = await downloadQueueRepository.find({
|
||||
order: {
|
||||
id: "DESC",
|
||||
},
|
||||
relations: {
|
||||
game: true,
|
||||
},
|
||||
});
|
||||
|
||||
this.resumeDownload(nextQueueItem!.game);
|
||||
}
|
||||
}
|
||||
|
||||
private static clearCurrentDownload() {
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import type { Repack } from "@main/entity";
|
||||
import type { SteamGame } from "@types";
|
||||
|
||||
interface State {
|
||||
repacks: Repack[];
|
||||
steamGames: SteamGame[];
|
||||
}
|
||||
|
||||
const initialState: State = {
|
||||
repacks: [],
|
||||
steamGames: [],
|
||||
};
|
||||
|
||||
export class StateManager {
|
||||
private state = initialState;
|
||||
|
||||
public setValue<T extends keyof State>(key: T, value: State[T]) {
|
||||
this.state = { ...this.state, [key]: value };
|
||||
}
|
||||
|
||||
public getValue<T extends keyof State>(key: T) {
|
||||
return this.state[key];
|
||||
}
|
||||
|
||||
public clearValue<T extends keyof State>(key: T) {
|
||||
this.state = { ...this.state, [key]: initialState[key] };
|
||||
}
|
||||
}
|
||||
|
||||
export const stateManager = new StateManager();
|
||||
@@ -7,7 +7,7 @@ import { formatName } from "@shared";
|
||||
import { workerData } from "node:worker_threads";
|
||||
|
||||
const steamGamesIndex = new flexSearch.Index({
|
||||
tokenize: "forward",
|
||||
tokenize: "reverse",
|
||||
});
|
||||
|
||||
const { steamGamesPath } = workerData;
|
||||
|
||||
Reference in New Issue
Block a user