mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 13:56:16 +00:00
Compare commits
27 Commits
4584783f44
...
feature/se
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f22896303d | ||
|
|
a9085ec2ed | ||
|
|
452532e18b | ||
|
|
b55bc935f7 | ||
|
|
e8fc71b36b | ||
|
|
6295637b48 | ||
|
|
e1a5a95ceb | ||
|
|
e5dff9babe | ||
|
|
5d59a15bbf | ||
|
|
f555890b4c | ||
|
|
d4b1569892 | ||
|
|
e0f7f34d14 | ||
|
|
cd4715e00d | ||
|
|
5daad057e7 | ||
|
|
3a3c0a2b35 | ||
|
|
64b1795ddd | ||
|
|
86d3f7ac81 | ||
|
|
1458314df6 | ||
|
|
1ad501c64e | ||
|
|
92ec056ba8 | ||
|
|
bd8974c7cb | ||
|
|
83b7fb83ab | ||
|
|
dc4dda7e17 | ||
|
|
1ebf8acb9b | ||
|
|
0955af1e69 | ||
|
|
fd7f2403da | ||
|
|
e331b9b246 |
@@ -198,7 +198,10 @@
|
|||||||
"queued": "Queued",
|
"queued": "Queued",
|
||||||
"no_downloads_title": "Such empty",
|
"no_downloads_title": "Such empty",
|
||||||
"no_downloads_description": "You haven't downloaded anything with Hydra yet, but it's never too late to start.",
|
"no_downloads_description": "You haven't downloaded anything with Hydra yet, but it's never too late to start.",
|
||||||
"checking_files": "Checking files…"
|
"checking_files": "Checking files…",
|
||||||
|
"seeding": "Seeding",
|
||||||
|
"stop_seed": "Stop seed",
|
||||||
|
"resume_seed": "Resume seed"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "Downloads path",
|
"downloads_path": "Downloads path",
|
||||||
@@ -253,8 +256,9 @@
|
|||||||
"must_be_valid_url": "The source must be a valid URL",
|
"must_be_valid_url": "The source must be a valid URL",
|
||||||
"blocked_users": "Blocked users",
|
"blocked_users": "Blocked users",
|
||||||
"user_unblocked": "User has been unblocked",
|
"user_unblocked": "User has been unblocked",
|
||||||
"enable_achievement_notifications": "When an achievement is unlocked",
|
"enable_achievement_notifications": "When an achievement in unlocked",
|
||||||
"launch_minimized": "Launch Hydra minimized"
|
"launch_minimized": "Launch Hydra minimized",
|
||||||
|
"seed_after_download_completes": "Seed after download completes"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download complete",
|
"download_complete": "Download complete",
|
||||||
|
|||||||
@@ -194,7 +194,10 @@
|
|||||||
"queued": "Na fila",
|
"queued": "Na fila",
|
||||||
"no_downloads_title": "Nada por aqui…",
|
"no_downloads_title": "Nada por aqui…",
|
||||||
"no_downloads_description": "Você ainda não baixou nada pelo Hydra, mas nunca é tarde para começar.",
|
"no_downloads_description": "Você ainda não baixou nada pelo Hydra, mas nunca é tarde para começar.",
|
||||||
"checking_files": "Verificando arquivos…"
|
"checking_files": "Verificando arquivos…",
|
||||||
|
"seeding": "Semeando",
|
||||||
|
"stop_seed": "Parar seed",
|
||||||
|
"resume_seed": "Retomar seed"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "Diretório dos downloads",
|
"downloads_path": "Diretório dos downloads",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
UserAuth,
|
UserAuth,
|
||||||
GameAchievement,
|
GameAchievement,
|
||||||
UserSubscription,
|
UserSubscription,
|
||||||
|
SeedList,
|
||||||
} from "@main/entity";
|
} from "@main/entity";
|
||||||
|
|
||||||
import { databasePath } from "./constants";
|
import { databasePath } from "./constants";
|
||||||
@@ -25,6 +26,7 @@ export const dataSource = new DataSource({
|
|||||||
DownloadSource,
|
DownloadSource,
|
||||||
DownloadQueue,
|
DownloadQueue,
|
||||||
GameAchievement,
|
GameAchievement,
|
||||||
|
SeedList,
|
||||||
],
|
],
|
||||||
synchronize: false,
|
synchronize: false,
|
||||||
database: databasePath,
|
database: databasePath,
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ export class Game {
|
|||||||
@Column("int", { default: Downloader.Torrent })
|
@Column("int", { default: Downloader.Torrent })
|
||||||
downloader: Downloader;
|
downloader: Downloader;
|
||||||
|
|
||||||
|
@Column("boolean", { default: false })
|
||||||
|
shouldSeed: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Progress is a float between 0 and 1
|
* Progress is a float between 0 and 1
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ export * from "./game.entity";
|
|||||||
export * from "./game-achievements.entity";
|
export * from "./game-achievements.entity";
|
||||||
export * from "./download-source.entity";
|
export * from "./download-source.entity";
|
||||||
export * from "./download-queue.entity";
|
export * from "./download-queue.entity";
|
||||||
|
export * from "./seed-list.entity";
|
||||||
|
|||||||
25
src/main/entity/seed-list.entity.ts
Normal file
25
src/main/entity/seed-list.entity.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
Column,
|
||||||
|
} from "typeorm";
|
||||||
|
|
||||||
|
@Entity("seed_list")
|
||||||
|
export class SeedList {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column("text")
|
||||||
|
downloadUri: string;
|
||||||
|
|
||||||
|
@Column("boolean", { default: false })
|
||||||
|
shouldSeed: boolean;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
@@ -38,6 +38,9 @@ export class UserPreferences {
|
|||||||
@Column("boolean", { default: false })
|
@Column("boolean", { default: false })
|
||||||
startMinimized: boolean;
|
startMinimized: boolean;
|
||||||
|
|
||||||
|
@Column("boolean", { default: true })
|
||||||
|
seedAfterDownloadCompletes: boolean;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import { CreateUserSubscription } from "./migrations/20241015235142_create_user_
|
|||||||
import { AddBackgroundImageUrl } from "./migrations/20241016100249_add_background_image_url";
|
import { AddBackgroundImageUrl } from "./migrations/20241016100249_add_background_image_url";
|
||||||
import { AddWinePrefixToGame } from "./migrations/20241019081648_add_wine_prefix_to_game";
|
import { AddWinePrefixToGame } from "./migrations/20241019081648_add_wine_prefix_to_game";
|
||||||
import { AddStartMinimizedColumn } from "./migrations/20241030171454_add_start_minimized_column";
|
import { AddStartMinimizedColumn } from "./migrations/20241030171454_add_start_minimized_column";
|
||||||
|
import { AddSeedAfterDownloadCompletesColumn } from "./migrations/20241101012727_add_seed_after_download_completes_column";
|
||||||
|
import { AddSeedListTable } from "./migrations/20241103231555_add_seed_list_table";
|
||||||
|
import { AddShouldSeedColumn } from "./migrations/20241107211345_add_should_seed_colum";
|
||||||
export type HydraMigration = Knex.Migration & { name: string };
|
export type HydraMigration = Knex.Migration & { name: string };
|
||||||
|
|
||||||
class MigrationSource implements Knex.MigrationSource<HydraMigration> {
|
class MigrationSource implements Knex.MigrationSource<HydraMigration> {
|
||||||
@@ -28,6 +31,9 @@ class MigrationSource implements Knex.MigrationSource<HydraMigration> {
|
|||||||
AddBackgroundImageUrl,
|
AddBackgroundImageUrl,
|
||||||
AddWinePrefixToGame,
|
AddWinePrefixToGame,
|
||||||
AddStartMinimizedColumn,
|
AddStartMinimizedColumn,
|
||||||
|
AddSeedAfterDownloadCompletesColumn,
|
||||||
|
AddSeedListTable,
|
||||||
|
AddShouldSeedColumn,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
getMigrationName(migration: HydraMigration): string {
|
getMigrationName(migration: HydraMigration): string {
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { HydraMigration } from "@main/knex-client";
|
||||||
|
import type { Knex } from "knex";
|
||||||
|
|
||||||
|
export const AddSeedAfterDownloadCompletesColumn: HydraMigration = {
|
||||||
|
name: "AddSeedAfterDownloadCompletesColumn",
|
||||||
|
up: (knex: Knex) => {
|
||||||
|
return knex.schema.alterTable("user_preferences", (table) => {
|
||||||
|
return table
|
||||||
|
.boolean("seedAfterDownloadCompletes")
|
||||||
|
.notNullable()
|
||||||
|
.defaultTo(1);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (knex: Knex) => {
|
||||||
|
return knex.schema.alterTable("user_preferences", (table) => {
|
||||||
|
return table.dropColumn("seedAfterDownloadCompletes");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
19
src/main/migrations/20241103231555_add_seed_list_table.ts
Normal file
19
src/main/migrations/20241103231555_add_seed_list_table.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { HydraMigration } from "@main/knex-client";
|
||||||
|
import type { Knex } from "knex";
|
||||||
|
|
||||||
|
export const AddSeedListTable: HydraMigration = {
|
||||||
|
name: "AddSeedListTable",
|
||||||
|
up: (knex: Knex) => {
|
||||||
|
return knex.schema.createTable("seed_list", async (table) => {
|
||||||
|
table.increments("id").primary();
|
||||||
|
table.text("downloadUri").notNullable();
|
||||||
|
table.boolean("shouldSeed").defaultTo(false);
|
||||||
|
table.datetime("createdAt").notNullable().defaultTo(knex.fn.now());
|
||||||
|
table.datetime("updatedAt").notNullable().defaultTo(knex.fn.now());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (knex: Knex) => {
|
||||||
|
return knex.schema.dropTable("seed_list");
|
||||||
|
},
|
||||||
|
};
|
||||||
17
src/main/migrations/20241107211345_add_should_seed_colum.ts
Normal file
17
src/main/migrations/20241107211345_add_should_seed_colum.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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(false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (knex: Knex) => {
|
||||||
|
return knex.schema.alterTable("game", (table) => {
|
||||||
|
return table.dropColumn("shouldSeed");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
UserAuth,
|
UserAuth,
|
||||||
GameAchievement,
|
GameAchievement,
|
||||||
UserSubscription,
|
UserSubscription,
|
||||||
|
SeedList,
|
||||||
} from "@main/entity";
|
} from "@main/entity";
|
||||||
|
|
||||||
export const gameRepository = dataSource.getRepository(Game);
|
export const gameRepository = dataSource.getRepository(Game);
|
||||||
@@ -27,6 +28,8 @@ export const downloadQueueRepository = dataSource.getRepository(DownloadQueue);
|
|||||||
|
|
||||||
export const userAuthRepository = dataSource.getRepository(UserAuth);
|
export const userAuthRepository = dataSource.getRepository(UserAuth);
|
||||||
|
|
||||||
|
export const seedListRepository = dataSource.getRepository(SeedList);
|
||||||
|
|
||||||
export const userSubscriptionRepository =
|
export const userSubscriptionRepository =
|
||||||
dataSource.getRepository(UserSubscription);
|
dataSource.getRepository(UserSubscription);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import { Game } from "@main/entity";
|
|||||||
import { Downloader } from "@shared";
|
import { Downloader } from "@shared";
|
||||||
import { PythonInstance } from "./python-instance";
|
import { PythonInstance } from "./python-instance";
|
||||||
import { WindowManager } from "../window-manager";
|
import { WindowManager } from "../window-manager";
|
||||||
import { downloadQueueRepository, gameRepository } from "@main/repository";
|
import {
|
||||||
|
downloadQueueRepository,
|
||||||
|
gameRepository,
|
||||||
|
userPreferencesRepository,
|
||||||
|
} from "@main/repository";
|
||||||
import { publishDownloadCompleteNotification } from "../notifications";
|
import { publishDownloadCompleteNotification } from "../notifications";
|
||||||
import { RealDebridDownloader } from "./real-debrid-downloader";
|
import { RealDebridDownloader } from "./real-debrid-downloader";
|
||||||
import type { DownloadProgress } from "@types";
|
import type { DownloadProgress } from "@types";
|
||||||
@@ -50,6 +54,19 @@ export class DownloadManager {
|
|||||||
|
|
||||||
await downloadQueueRepository.delete({ game });
|
await downloadQueueRepository.delete({ game });
|
||||||
|
|
||||||
|
const userPreferences = await userPreferencesRepository.findOne({
|
||||||
|
where: { id: 1 },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
userPreferences?.seedAfterDownloadCompletes &&
|
||||||
|
this.currentDownloader === Downloader.Torrent
|
||||||
|
) {
|
||||||
|
if (!game.shouldSeed) {
|
||||||
|
await gameRepository.update(game.id, { shouldSeed: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [nextQueueItem] = await downloadQueueRepository.find({
|
const [nextQueueItem] = await downloadQueueRepository.find({
|
||||||
order: {
|
order: {
|
||||||
id: "DESC",
|
id: "DESC",
|
||||||
@@ -66,6 +83,21 @@ export class DownloadManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async watchSeedingList() {
|
||||||
|
const shouldSeedGames = await gameRepository.findOne({
|
||||||
|
where: { shouldSeed: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (shouldSeedGames) {
|
||||||
|
const seedingList = await PythonInstance.getSeedingList();
|
||||||
|
|
||||||
|
WindowManager.mainWindow?.webContents.send(
|
||||||
|
"on-seeding-list",
|
||||||
|
JSON.parse(JSON.stringify(seedingList))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static async pauseDownload() {
|
static async pauseDownload() {
|
||||||
if (this.currentDownloader === Downloader.Torrent) {
|
if (this.currentDownloader === Downloader.Torrent) {
|
||||||
await PythonInstance.pauseDownload();
|
await PythonInstance.pauseDownload();
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ import {
|
|||||||
StartDownloadPayload,
|
StartDownloadPayload,
|
||||||
PauseDownloadPayload,
|
PauseDownloadPayload,
|
||||||
LibtorrentStatus,
|
LibtorrentStatus,
|
||||||
LibtorrentPayload,
|
|
||||||
ProcessPayload,
|
ProcessPayload,
|
||||||
|
LibtorrentSeedingPayload,
|
||||||
|
LibtorrentDownloadingPayload,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { pythonInstanceLogger as logger } from "../logger";
|
import { pythonInstanceLogger as logger } from "../logger";
|
||||||
|
|
||||||
@@ -60,67 +61,87 @@ export class PythonInstance {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async getSeedingList() {
|
||||||
|
const response = await this.rpc.get<LibtorrentSeedingPayload[] | null>(
|
||||||
|
"/seed-list"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data && response.data.length > 0) {
|
||||||
|
for (const seed of response.data) {
|
||||||
|
await gameRepository.update({ id: seed.gameId }, { status: "seeding" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
public static async getStatus() {
|
public static async getStatus() {
|
||||||
if (this.downloadingGameId === -1) return null;
|
const response = await this.rpc.get<LibtorrentDownloadingPayload | null>(
|
||||||
|
"/status"
|
||||||
|
);
|
||||||
|
|
||||||
const response = await this.rpc.get<LibtorrentPayload | null>("/status");
|
if (response.data) {
|
||||||
|
try {
|
||||||
if (response.data === null) return null;
|
const {
|
||||||
|
progress,
|
||||||
try {
|
numPeers,
|
||||||
const {
|
numSeeds,
|
||||||
progress,
|
downloadSpeed,
|
||||||
numPeers,
|
|
||||||
numSeeds,
|
|
||||||
downloadSpeed,
|
|
||||||
bytesDownloaded,
|
|
||||||
fileSize,
|
|
||||||
folderName,
|
|
||||||
status,
|
|
||||||
gameId,
|
|
||||||
} = response.data;
|
|
||||||
|
|
||||||
this.downloadingGameId = gameId;
|
|
||||||
|
|
||||||
const isDownloadingMetadata =
|
|
||||||
status === LibtorrentStatus.DownloadingMetadata;
|
|
||||||
|
|
||||||
const isCheckingFiles = status === LibtorrentStatus.CheckingFiles;
|
|
||||||
|
|
||||||
if (!isDownloadingMetadata && !isCheckingFiles) {
|
|
||||||
const update: QueryDeepPartialEntity<Game> = {
|
|
||||||
bytesDownloaded,
|
bytesDownloaded,
|
||||||
fileSize,
|
fileSize,
|
||||||
|
folderName,
|
||||||
|
status,
|
||||||
|
gameId,
|
||||||
|
} = response.data;
|
||||||
|
|
||||||
|
this.downloadingGameId = gameId;
|
||||||
|
|
||||||
|
const isDownloadingMetadata =
|
||||||
|
status === LibtorrentStatus.DownloadingMetadata;
|
||||||
|
|
||||||
|
const isCheckingFiles = status === LibtorrentStatus.CheckingFiles;
|
||||||
|
|
||||||
|
if (!isDownloadingMetadata && !isCheckingFiles) {
|
||||||
|
const update: QueryDeepPartialEntity<Game> = {
|
||||||
|
bytesDownloaded,
|
||||||
|
fileSize,
|
||||||
|
progress,
|
||||||
|
status: "active",
|
||||||
|
};
|
||||||
|
|
||||||
|
await gameRepository.update(
|
||||||
|
{ id: gameId },
|
||||||
|
{
|
||||||
|
...update,
|
||||||
|
folderName,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
progress === 1 &&
|
||||||
|
!isCheckingFiles &&
|
||||||
|
status !== LibtorrentStatus.Seeding
|
||||||
|
) {
|
||||||
|
this.downloadingGameId = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
numPeers,
|
||||||
|
numSeeds,
|
||||||
|
downloadSpeed,
|
||||||
|
timeRemaining: calculateETA(fileSize, bytesDownloaded, downloadSpeed),
|
||||||
|
isDownloadingMetadata,
|
||||||
|
isCheckingFiles,
|
||||||
progress,
|
progress,
|
||||||
status: "active",
|
gameId,
|
||||||
};
|
} as DownloadProgress;
|
||||||
|
} catch (err) {
|
||||||
await gameRepository.update(
|
return null;
|
||||||
{ id: gameId },
|
|
||||||
{
|
|
||||||
...update,
|
|
||||||
folderName,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progress === 1 && !isCheckingFiles) {
|
|
||||||
this.downloadingGameId = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
numPeers,
|
|
||||||
numSeeds,
|
|
||||||
downloadSpeed,
|
|
||||||
timeRemaining: calculateETA(fileSize, bytesDownloaded, downloadSpeed),
|
|
||||||
isDownloadingMetadata,
|
|
||||||
isCheckingFiles,
|
|
||||||
progress,
|
|
||||||
gameId,
|
|
||||||
} as DownloadProgress;
|
|
||||||
} catch (err) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async pauseDownload() {
|
static async pauseDownload() {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export enum LibtorrentStatus {
|
|||||||
Seeding = 5,
|
Seeding = 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LibtorrentPayload {
|
export interface LibtorrentDownloadingPayload {
|
||||||
progress: number;
|
progress: number;
|
||||||
numPeers: number;
|
numPeers: number;
|
||||||
numSeeds: number;
|
numSeeds: number;
|
||||||
@@ -32,7 +32,22 @@ export interface LibtorrentPayload {
|
|||||||
gameId: number;
|
gameId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LibtorrentSeedingPayload {
|
||||||
|
progress: number;
|
||||||
|
numPeers: number;
|
||||||
|
numSeeds: number;
|
||||||
|
uploadSpeed: number;
|
||||||
|
fileSize: number;
|
||||||
|
folderName: string;
|
||||||
|
status: LibtorrentStatus;
|
||||||
|
gameId: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProcessPayload {
|
export interface ProcessPayload {
|
||||||
exe: string;
|
exe: string;
|
||||||
pid: number;
|
pid: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SeedPayload {
|
||||||
|
should_seed: boolean;
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const startMainLoop = async () => {
|
|||||||
watchProcesses(),
|
watchProcesses(),
|
||||||
DownloadManager.watchDownloads(),
|
DownloadManager.watchDownloads(),
|
||||||
AchievementWatcherManager.watchAchievements(),
|
AchievementWatcherManager.watchAchievements(),
|
||||||
|
DownloadManager.watchSeedingList(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await sleep(1500);
|
await sleep(1500);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import type {
|
|||||||
import type { CatalogueCategory } from "@shared";
|
import type { CatalogueCategory } from "@shared";
|
||||||
import type { AxiosProgressEvent } from "axios";
|
import type { AxiosProgressEvent } from "axios";
|
||||||
import { GameAchievement } from "@main/entity";
|
import { GameAchievement } from "@main/entity";
|
||||||
|
import { LibtorrentSeedingPayload } from "@main/services/download/types";
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("electron", {
|
contextBridge.exposeInMainWorld("electron", {
|
||||||
/* Torrenting */
|
/* Torrenting */
|
||||||
@@ -26,6 +27,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("pauseGameDownload", gameId),
|
ipcRenderer.invoke("pauseGameDownload", gameId),
|
||||||
resumeGameDownload: (gameId: number) =>
|
resumeGameDownload: (gameId: number) =>
|
||||||
ipcRenderer.invoke("resumeGameDownload", gameId),
|
ipcRenderer.invoke("resumeGameDownload", gameId),
|
||||||
|
startSeeding: (gameId: number, magnet: string, savePath: string) =>
|
||||||
|
ipcRenderer.invoke("startSeeding", gameId, magnet, savePath),
|
||||||
onDownloadProgress: (cb: (value: DownloadProgress) => void) => {
|
onDownloadProgress: (cb: (value: DownloadProgress) => void) => {
|
||||||
const listener = (
|
const listener = (
|
||||||
_event: Electron.IpcRendererEvent,
|
_event: Electron.IpcRendererEvent,
|
||||||
@@ -34,6 +37,14 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.on("on-download-progress", listener);
|
ipcRenderer.on("on-download-progress", listener);
|
||||||
return () => ipcRenderer.removeListener("on-download-progress", listener);
|
return () => ipcRenderer.removeListener("on-download-progress", listener);
|
||||||
},
|
},
|
||||||
|
onSeedingList: (cb: (value: LibtorrentSeedingPayload[]) => void) => {
|
||||||
|
const listener = (
|
||||||
|
_event: Electron.IpcRendererEvent,
|
||||||
|
value: LibtorrentSeedingPayload[]
|
||||||
|
) => cb(value);
|
||||||
|
ipcRenderer.on("on-seeding-list", listener);
|
||||||
|
return () => ipcRenderer.removeListener("on-seeding-list", listener);
|
||||||
|
},
|
||||||
|
|
||||||
/* Catalogue */
|
/* Catalogue */
|
||||||
searchGames: (query: string) => ipcRenderer.invoke("searchGames", query),
|
searchGames: (query: string) => ipcRenderer.invoke("searchGames", query),
|
||||||
|
|||||||
3
src/renderer/src/declaration.d.ts
vendored
3
src/renderer/src/declaration.d.ts
vendored
@@ -49,6 +49,9 @@ declare global {
|
|||||||
onDownloadProgress: (
|
onDownloadProgress: (
|
||||||
cb: (value: DownloadProgress) => void
|
cb: (value: DownloadProgress) => void
|
||||||
) => () => Electron.IpcRenderer;
|
) => () => Electron.IpcRenderer;
|
||||||
|
onSeedingList: (
|
||||||
|
cb: (value: LibtorrentSeedingPayload[]) => void
|
||||||
|
) => () => Electron.IpcRenderer;
|
||||||
|
|
||||||
/* Catalogue */
|
/* Catalogue */
|
||||||
searchGames: (query: string) => Promise<CatalogueEntry[]>;
|
searchGames: (query: string) => Promise<CatalogueEntry[]>;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import type { LibraryGame } from "@types";
|
import type { LibraryGame, SeedingList } from "@types";
|
||||||
|
|
||||||
import { Badge, Button } from "@renderer/components";
|
import { Badge, Button } from "@renderer/components";
|
||||||
import {
|
import {
|
||||||
@@ -21,6 +22,7 @@ export interface DownloadGroupProps {
|
|||||||
title: string;
|
title: string;
|
||||||
openDeleteGameModal: (gameId: number) => void;
|
openDeleteGameModal: (gameId: number) => void;
|
||||||
openGameInstaller: (gameId: number) => void;
|
openGameInstaller: (gameId: number) => void;
|
||||||
|
seedingList: SeedingList[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DownloadGroup({
|
export function DownloadGroup({
|
||||||
@@ -28,6 +30,7 @@ export function DownloadGroup({
|
|||||||
title,
|
title,
|
||||||
openDeleteGameModal,
|
openDeleteGameModal,
|
||||||
openGameInstaller,
|
openGameInstaller,
|
||||||
|
seedingList = [],
|
||||||
}: DownloadGroupProps) {
|
}: DownloadGroupProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -46,6 +49,17 @@ export function DownloadGroup({
|
|||||||
isGameDeleting,
|
isGameDeleting,
|
||||||
} = useDownload();
|
} = useDownload();
|
||||||
|
|
||||||
|
const seedingMap = useMemo(() => {
|
||||||
|
if (!Array.isArray(seedingList) || seedingList.length === 0) {
|
||||||
|
return new Map<number, SeedingList>();
|
||||||
|
}
|
||||||
|
const map = new Map<number, SeedingList>();
|
||||||
|
seedingList.forEach((seed) => {
|
||||||
|
map.set(seed.gameId, seed);
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}, [seedingList]);
|
||||||
|
|
||||||
const getFinalDownloadSize = (game: LibraryGame) => {
|
const getFinalDownloadSize = (game: LibraryGame) => {
|
||||||
const isGameDownloading = lastPacket?.game.id === game.id;
|
const isGameDownloading = lastPacket?.game.id === game.id;
|
||||||
|
|
||||||
@@ -60,6 +74,7 @@ export function DownloadGroup({
|
|||||||
const getGameInfo = (game: LibraryGame) => {
|
const getGameInfo = (game: LibraryGame) => {
|
||||||
const isGameDownloading = lastPacket?.game.id === game.id;
|
const isGameDownloading = lastPacket?.game.id === game.id;
|
||||||
const finalDownloadSize = getFinalDownloadSize(game);
|
const finalDownloadSize = getFinalDownloadSize(game);
|
||||||
|
const seed = seedingMap.get(game.id);
|
||||||
|
|
||||||
if (isGameDeleting(game.id)) {
|
if (isGameDeleting(game.id)) {
|
||||||
return <p>{t("deleting")}</p>;
|
return <p>{t("deleting")}</p>;
|
||||||
@@ -98,7 +113,18 @@ export function DownloadGroup({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (game.progress === 1) {
|
if (game.progress === 1) {
|
||||||
return <p>{t("completed")}</p>;
|
return (
|
||||||
|
<>
|
||||||
|
{seed ? (
|
||||||
|
<>
|
||||||
|
<p>{t("seeding")}</p>
|
||||||
|
<p>{formatBytes(seed.uploadSpeed ?? 0)}/s</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p>{t("completed")}</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (game.status === "paused") {
|
if (game.status === "paused") {
|
||||||
@@ -127,8 +153,8 @@ export function DownloadGroup({
|
|||||||
|
|
||||||
const getGameActions = (game: LibraryGame) => {
|
const getGameActions = (game: LibraryGame) => {
|
||||||
const isGameDownloading = lastPacket?.game.id === game.id;
|
const isGameDownloading = lastPacket?.game.id === game.id;
|
||||||
|
|
||||||
const deleting = isGameDeleting(game.id);
|
const deleting = isGameDeleting(game.id);
|
||||||
|
const seed = seedingMap.get(game.id);
|
||||||
|
|
||||||
if (game.progress === 1) {
|
if (game.progress === 1) {
|
||||||
return (
|
return (
|
||||||
@@ -144,6 +170,14 @@ export function DownloadGroup({
|
|||||||
<Button onClick={() => openDeleteGameModal(game.id)} theme="outline">
|
<Button onClick={() => openDeleteGameModal(game.id)} theme="outline">
|
||||||
{t("delete")}
|
{t("delete")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{seed && game.shouldSeed && (
|
||||||
|
<Button theme="outline">{t("stop_seed")}</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{seed && !game.shouldSeed && (
|
||||||
|
<Button theme="outline">{t("resume_seed")}</Button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
import { useDownload, useLibrary } from "@renderer/hooks";
|
import { useDownload, useLibrary } from "@renderer/hooks";
|
||||||
|
|
||||||
import { useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
|
import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
|
||||||
import * as styles from "./downloads.css";
|
import * as styles from "./downloads.css";
|
||||||
import { DeleteGameModal } from "./delete-game-modal";
|
import { DeleteGameModal } from "./delete-game-modal";
|
||||||
import { DownloadGroup } from "./download-group";
|
import { DownloadGroup } from "./download-group";
|
||||||
import type { LibraryGame } from "@types";
|
import type { LibraryGame, SeedingList } from "@types";
|
||||||
import { orderBy } from "lodash-es";
|
import { orderBy } from "lodash-es";
|
||||||
import { ArrowDownIcon } from "@primer/octicons-react";
|
import { ArrowDownIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
@@ -30,6 +30,12 @@ export default function Downloads() {
|
|||||||
|
|
||||||
const { lastPacket } = useDownload();
|
const { lastPacket } = useDownload();
|
||||||
|
|
||||||
|
const [seedingList, setSeedingList] = useState<SeedingList[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.electron.onSeedingList((value) => setSeedingList(value));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleOpenGameInstaller = (gameId: number) =>
|
const handleOpenGameInstaller = (gameId: number) =>
|
||||||
window.electron.openGameInstaller(gameId).then((isBinaryInPath) => {
|
window.electron.openGameInstaller(gameId).then((isBinaryInPath) => {
|
||||||
if (!isBinaryInPath) setShowBinaryNotFoundModal(true);
|
if (!isBinaryInPath) setShowBinaryNotFoundModal(true);
|
||||||
@@ -122,6 +128,7 @@ export default function Downloads() {
|
|||||||
library={group.library}
|
library={group.library}
|
||||||
openDeleteGameModal={handleOpenDeleteGameModal}
|
openDeleteGameModal={handleOpenDeleteGameModal}
|
||||||
openGameInstaller={handleOpenGameInstaller}
|
openGameInstaller={handleOpenGameInstaller}
|
||||||
|
seedingList={seedingList}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export function SettingsBehavior() {
|
|||||||
preferQuitInsteadOfHiding: false,
|
preferQuitInsteadOfHiding: false,
|
||||||
runAtStartup: false,
|
runAtStartup: false,
|
||||||
startMinimized: false,
|
startMinimized: false,
|
||||||
|
seedAfterDownloadCompletes: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
@@ -28,6 +29,7 @@ export function SettingsBehavior() {
|
|||||||
preferQuitInsteadOfHiding: userPreferences.preferQuitInsteadOfHiding,
|
preferQuitInsteadOfHiding: userPreferences.preferQuitInsteadOfHiding,
|
||||||
runAtStartup: userPreferences.runAtStartup,
|
runAtStartup: userPreferences.runAtStartup,
|
||||||
startMinimized: userPreferences.startMinimized,
|
startMinimized: userPreferences.startMinimized,
|
||||||
|
seedAfterDownloadCompletes: userPreferences.seedAfterDownloadCompletes,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [userPreferences]);
|
}, [userPreferences]);
|
||||||
@@ -86,6 +88,16 @@ export function SettingsBehavior() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<CheckboxField
|
||||||
|
label={t("seed_after_download_completes")}
|
||||||
|
checked={form.seedAfterDownloadCompletes}
|
||||||
|
onChange={() =>
|
||||||
|
handleChange({
|
||||||
|
seedAfterDownloadCompletes: !form.seedAfterDownloadCompletes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ export type GameStatus =
|
|||||||
| "paused"
|
| "paused"
|
||||||
| "error"
|
| "error"
|
||||||
| "complete"
|
| "complete"
|
||||||
| "removed";
|
| "removed"
|
||||||
|
| "seeding";
|
||||||
|
|
||||||
export type GameShop = "steam" | "epic";
|
export type GameShop = "steam" | "epic";
|
||||||
|
|
||||||
@@ -124,6 +125,7 @@ export interface Game {
|
|||||||
objectID: string;
|
objectID: string;
|
||||||
shop: GameShop;
|
shop: GameShop;
|
||||||
downloadQueue: DownloadQueue | null;
|
downloadQueue: DownloadQueue | null;
|
||||||
|
shouldSeed: boolean;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
@@ -151,6 +153,16 @@ export interface DownloadProgress {
|
|||||||
game: LibraryGame;
|
game: LibraryGame;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SeedingList {
|
||||||
|
progress: number;
|
||||||
|
numPeers: number;
|
||||||
|
numSeeds: number;
|
||||||
|
uploadSpeed: number;
|
||||||
|
gameId: number;
|
||||||
|
folderName: string;
|
||||||
|
fileSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UserPreferences {
|
export interface UserPreferences {
|
||||||
downloadsPath: string | null;
|
downloadsPath: string | null;
|
||||||
language: string;
|
language: string;
|
||||||
@@ -161,6 +173,7 @@ export interface UserPreferences {
|
|||||||
preferQuitInsteadOfHiding: boolean;
|
preferQuitInsteadOfHiding: boolean;
|
||||||
runAtStartup: boolean;
|
runAtStartup: boolean;
|
||||||
startMinimized: boolean;
|
startMinimized: boolean;
|
||||||
|
seedAfterDownloadCompletes: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Steam250Game {
|
export interface Steam250Game {
|
||||||
|
|||||||
@@ -50,6 +50,21 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
self.wfile.write(json.dumps(status).encode('utf-8'))
|
self.wfile.write(json.dumps(status).encode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
elif self.path == "/seed-list":
|
||||||
|
if self.headers.get(self.rpc_password_header) != rpc_password:
|
||||||
|
self.send_response(401)
|
||||||
|
self.end_headers()
|
||||||
|
return
|
||||||
|
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-type", "application/json")
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
seed_list = torrent_downloader.get_seed_list()
|
||||||
|
|
||||||
|
self.wfile.write(json.dumps(seed_list).encode('utf-8'))
|
||||||
|
|
||||||
elif self.path == "/healthcheck":
|
elif self.path == "/healthcheck":
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
@@ -107,6 +122,8 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
elif data['action'] == 'kill-torrent':
|
elif data['action'] == 'kill-torrent':
|
||||||
torrent_downloader.abort_session()
|
torrent_downloader.abort_session()
|
||||||
torrent_downloader = None
|
torrent_downloader = None
|
||||||
|
elif data['action'] == 'start-seeding':
|
||||||
|
torrent_downloader.start_seeding(data['game_id'], data['magnet'], data['save_path'])
|
||||||
|
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ class TorrentDownloader:
|
|||||||
params = {'url': magnet, 'save_path': save_path, 'trackers': self.trackers}
|
params = {'url': magnet, 'save_path': save_path, 'trackers': self.trackers}
|
||||||
torrent_handle = self.session.add_torrent(params)
|
torrent_handle = self.session.add_torrent(params)
|
||||||
self.torrent_handles[game_id] = torrent_handle
|
self.torrent_handles[game_id] = torrent_handle
|
||||||
torrent_handle.set_flags(lt.torrent_flags.auto_managed)
|
torrent_handle.set_flags(lt.torrent_flags.auto_managed, lt.torrent_flags.seed_mode)
|
||||||
torrent_handle.resume()
|
torrent_handle.resume()
|
||||||
|
|
||||||
self.downloading_game_id = game_id
|
self.downloading_game_id = game_id
|
||||||
@@ -151,6 +151,7 @@ class TorrentDownloader:
|
|||||||
'gameId': self.downloading_game_id,
|
'gameId': self.downloading_game_id,
|
||||||
'progress': status.progress,
|
'progress': status.progress,
|
||||||
'downloadSpeed': status.download_rate,
|
'downloadSpeed': status.download_rate,
|
||||||
|
'uploadSpeed': status.upload_rate,
|
||||||
'numPeers': status.num_peers,
|
'numPeers': status.num_peers,
|
||||||
'numSeeds': status.num_seeds,
|
'numSeeds': status.num_seeds,
|
||||||
'status': status.state,
|
'status': status.state,
|
||||||
@@ -158,8 +159,34 @@ class TorrentDownloader:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if status.progress == 1:
|
if status.progress == 1:
|
||||||
torrent_handle.pause()
|
|
||||||
self.session.remove_torrent(torrent_handle)
|
|
||||||
self.downloading_game_id = -1
|
self.downloading_game_id = -1
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
def get_seed_list(self):
|
||||||
|
response = []
|
||||||
|
|
||||||
|
for game_id, torrent_handle in self.torrent_handles.items():
|
||||||
|
if game_id == self.downloading_game_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
status = torrent_handle.status()
|
||||||
|
info = torrent_handle.torrent_file()
|
||||||
|
|
||||||
|
torrent_info = {
|
||||||
|
'folderName': info.name() if info else "",
|
||||||
|
'fileSize': info.total_size() if info else 0,
|
||||||
|
'gameId': game_id,
|
||||||
|
'progress': status.progress,
|
||||||
|
'downloadSpeed': status.download_rate,
|
||||||
|
'uploadSpeed': status.upload_rate,
|
||||||
|
'numPeers': status.num_peers,
|
||||||
|
'numSeeds': status.num_seeds,
|
||||||
|
'status': status.state,
|
||||||
|
'bytesDownloaded': status.progress * info.total_size() if info else status.all_time_download,
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.state == 5:
|
||||||
|
response.append(torrent_info)
|
||||||
|
|
||||||
|
return response
|
||||||
Reference in New Issue
Block a user