mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 13:56:16 +00:00
Compare commits
27 Commits
release/v3
...
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",
|
||||
"no_downloads_title": "Such empty",
|
||||
"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": {
|
||||
"downloads_path": "Downloads path",
|
||||
@@ -253,8 +256,9 @@
|
||||
"must_be_valid_url": "The source must be a valid URL",
|
||||
"blocked_users": "Blocked users",
|
||||
"user_unblocked": "User has been unblocked",
|
||||
"enable_achievement_notifications": "When an achievement is unlocked",
|
||||
"launch_minimized": "Launch Hydra minimized"
|
||||
"enable_achievement_notifications": "When an achievement in unlocked",
|
||||
"launch_minimized": "Launch Hydra minimized",
|
||||
"seed_after_download_completes": "Seed after download completes"
|
||||
},
|
||||
"notifications": {
|
||||
"download_complete": "Download complete",
|
||||
|
||||
@@ -194,7 +194,10 @@
|
||||
"queued": "Na fila",
|
||||
"no_downloads_title": "Nada por aqui…",
|
||||
"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": {
|
||||
"downloads_path": "Diretório dos downloads",
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
UserAuth,
|
||||
GameAchievement,
|
||||
UserSubscription,
|
||||
SeedList,
|
||||
} from "@main/entity";
|
||||
|
||||
import { databasePath } from "./constants";
|
||||
@@ -25,6 +26,7 @@ export const dataSource = new DataSource({
|
||||
DownloadSource,
|
||||
DownloadQueue,
|
||||
GameAchievement,
|
||||
SeedList,
|
||||
],
|
||||
synchronize: false,
|
||||
database: databasePath,
|
||||
|
||||
@@ -54,6 +54,9 @@ export class Game {
|
||||
@Column("int", { default: Downloader.Torrent })
|
||||
downloader: Downloader;
|
||||
|
||||
@Column("boolean", { default: false })
|
||||
shouldSeed: boolean;
|
||||
|
||||
/**
|
||||
* Progress is a float between 0 and 1
|
||||
*/
|
||||
|
||||
@@ -8,3 +8,4 @@ export * from "./game.entity";
|
||||
export * from "./game-achievements.entity";
|
||||
export * from "./download-source.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 })
|
||||
startMinimized: boolean;
|
||||
|
||||
@Column("boolean", { default: true })
|
||||
seedAfterDownloadCompletes: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@ import { CreateUserSubscription } from "./migrations/20241015235142_create_user_
|
||||
import { AddBackgroundImageUrl } from "./migrations/20241016100249_add_background_image_url";
|
||||
import { AddWinePrefixToGame } from "./migrations/20241019081648_add_wine_prefix_to_game";
|
||||
import { AddStartMinimizedColumn } from "./migrations/20241030171454_add_start_minimized_column";
|
||||
import { 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 };
|
||||
|
||||
class MigrationSource implements Knex.MigrationSource<HydraMigration> {
|
||||
@@ -28,6 +31,9 @@ class MigrationSource implements Knex.MigrationSource<HydraMigration> {
|
||||
AddBackgroundImageUrl,
|
||||
AddWinePrefixToGame,
|
||||
AddStartMinimizedColumn,
|
||||
AddSeedAfterDownloadCompletesColumn,
|
||||
AddSeedListTable,
|
||||
AddShouldSeedColumn,
|
||||
]);
|
||||
}
|
||||
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,
|
||||
GameAchievement,
|
||||
UserSubscription,
|
||||
SeedList,
|
||||
} from "@main/entity";
|
||||
|
||||
export const gameRepository = dataSource.getRepository(Game);
|
||||
@@ -27,6 +28,8 @@ export const downloadQueueRepository = dataSource.getRepository(DownloadQueue);
|
||||
|
||||
export const userAuthRepository = dataSource.getRepository(UserAuth);
|
||||
|
||||
export const seedListRepository = dataSource.getRepository(SeedList);
|
||||
|
||||
export const userSubscriptionRepository =
|
||||
dataSource.getRepository(UserSubscription);
|
||||
|
||||
|
||||
@@ -2,7 +2,11 @@ import { Game } from "@main/entity";
|
||||
import { Downloader } from "@shared";
|
||||
import { PythonInstance } from "./python-instance";
|
||||
import { WindowManager } from "../window-manager";
|
||||
import { downloadQueueRepository, gameRepository } from "@main/repository";
|
||||
import {
|
||||
downloadQueueRepository,
|
||||
gameRepository,
|
||||
userPreferencesRepository,
|
||||
} from "@main/repository";
|
||||
import { publishDownloadCompleteNotification } from "../notifications";
|
||||
import { RealDebridDownloader } from "./real-debrid-downloader";
|
||||
import type { DownloadProgress } from "@types";
|
||||
@@ -50,6 +54,19 @@ export class DownloadManager {
|
||||
|
||||
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({
|
||||
order: {
|
||||
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() {
|
||||
if (this.currentDownloader === Downloader.Torrent) {
|
||||
await PythonInstance.pauseDownload();
|
||||
|
||||
@@ -16,8 +16,9 @@ import {
|
||||
StartDownloadPayload,
|
||||
PauseDownloadPayload,
|
||||
LibtorrentStatus,
|
||||
LibtorrentPayload,
|
||||
ProcessPayload,
|
||||
LibtorrentSeedingPayload,
|
||||
LibtorrentDownloadingPayload,
|
||||
} from "./types";
|
||||
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() {
|
||||
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 === null) return null;
|
||||
|
||||
try {
|
||||
const {
|
||||
progress,
|
||||
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> = {
|
||||
if (response.data) {
|
||||
try {
|
||||
const {
|
||||
progress,
|
||||
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,
|
||||
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,
|
||||
status: "active",
|
||||
};
|
||||
|
||||
await gameRepository.update(
|
||||
{ id: gameId },
|
||||
{
|
||||
...update,
|
||||
folderName,
|
||||
}
|
||||
);
|
||||
gameId,
|
||||
} as DownloadProgress;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
@@ -20,7 +20,7 @@ export enum LibtorrentStatus {
|
||||
Seeding = 5,
|
||||
}
|
||||
|
||||
export interface LibtorrentPayload {
|
||||
export interface LibtorrentDownloadingPayload {
|
||||
progress: number;
|
||||
numPeers: number;
|
||||
numSeeds: number;
|
||||
@@ -32,7 +32,22 @@ export interface LibtorrentPayload {
|
||||
gameId: number;
|
||||
}
|
||||
|
||||
export interface LibtorrentSeedingPayload {
|
||||
progress: number;
|
||||
numPeers: number;
|
||||
numSeeds: number;
|
||||
uploadSpeed: number;
|
||||
fileSize: number;
|
||||
folderName: string;
|
||||
status: LibtorrentStatus;
|
||||
gameId: number;
|
||||
}
|
||||
|
||||
export interface ProcessPayload {
|
||||
exe: string;
|
||||
pid: number;
|
||||
}
|
||||
|
||||
export interface SeedPayload {
|
||||
should_seed: boolean;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export const startMainLoop = async () => {
|
||||
watchProcesses(),
|
||||
DownloadManager.watchDownloads(),
|
||||
AchievementWatcherManager.watchAchievements(),
|
||||
DownloadManager.watchSeedingList(),
|
||||
]);
|
||||
|
||||
await sleep(1500);
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
import type { CatalogueCategory } from "@shared";
|
||||
import type { AxiosProgressEvent } from "axios";
|
||||
import { GameAchievement } from "@main/entity";
|
||||
import { LibtorrentSeedingPayload } from "@main/services/download/types";
|
||||
|
||||
contextBridge.exposeInMainWorld("electron", {
|
||||
/* Torrenting */
|
||||
@@ -26,6 +27,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.invoke("pauseGameDownload", gameId),
|
||||
resumeGameDownload: (gameId: number) =>
|
||||
ipcRenderer.invoke("resumeGameDownload", gameId),
|
||||
startSeeding: (gameId: number, magnet: string, savePath: string) =>
|
||||
ipcRenderer.invoke("startSeeding", gameId, magnet, savePath),
|
||||
onDownloadProgress: (cb: (value: DownloadProgress) => void) => {
|
||||
const listener = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
@@ -34,6 +37,14 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ipcRenderer.on("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 */
|
||||
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: (
|
||||
cb: (value: DownloadProgress) => void
|
||||
) => () => Electron.IpcRenderer;
|
||||
onSeedingList: (
|
||||
cb: (value: LibtorrentSeedingPayload[]) => void
|
||||
) => () => Electron.IpcRenderer;
|
||||
|
||||
/* Catalogue */
|
||||
searchGames: (query: string) => Promise<CatalogueEntry[]>;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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 {
|
||||
@@ -21,6 +22,7 @@ export interface DownloadGroupProps {
|
||||
title: string;
|
||||
openDeleteGameModal: (gameId: number) => void;
|
||||
openGameInstaller: (gameId: number) => void;
|
||||
seedingList: SeedingList[];
|
||||
}
|
||||
|
||||
export function DownloadGroup({
|
||||
@@ -28,6 +30,7 @@ export function DownloadGroup({
|
||||
title,
|
||||
openDeleteGameModal,
|
||||
openGameInstaller,
|
||||
seedingList = [],
|
||||
}: DownloadGroupProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -46,6 +49,17 @@ export function DownloadGroup({
|
||||
isGameDeleting,
|
||||
} = 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 isGameDownloading = lastPacket?.game.id === game.id;
|
||||
|
||||
@@ -60,6 +74,7 @@ export function DownloadGroup({
|
||||
const getGameInfo = (game: LibraryGame) => {
|
||||
const isGameDownloading = lastPacket?.game.id === game.id;
|
||||
const finalDownloadSize = getFinalDownloadSize(game);
|
||||
const seed = seedingMap.get(game.id);
|
||||
|
||||
if (isGameDeleting(game.id)) {
|
||||
return <p>{t("deleting")}</p>;
|
||||
@@ -98,7 +113,18 @@ export function DownloadGroup({
|
||||
}
|
||||
|
||||
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") {
|
||||
@@ -127,8 +153,8 @@ export function DownloadGroup({
|
||||
|
||||
const getGameActions = (game: LibraryGame) => {
|
||||
const isGameDownloading = lastPacket?.game.id === game.id;
|
||||
|
||||
const deleting = isGameDeleting(game.id);
|
||||
const seed = seedingMap.get(game.id);
|
||||
|
||||
if (game.progress === 1) {
|
||||
return (
|
||||
@@ -144,6 +170,14 @@ export function DownloadGroup({
|
||||
<Button onClick={() => openDeleteGameModal(game.id)} theme="outline">
|
||||
{t("delete")}
|
||||
</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 { useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal";
|
||||
import * as styles from "./downloads.css";
|
||||
import { DeleteGameModal } from "./delete-game-modal";
|
||||
import { DownloadGroup } from "./download-group";
|
||||
import type { LibraryGame } from "@types";
|
||||
import type { LibraryGame, SeedingList } from "@types";
|
||||
import { orderBy } from "lodash-es";
|
||||
import { ArrowDownIcon } from "@primer/octicons-react";
|
||||
|
||||
@@ -30,6 +30,12 @@ export default function Downloads() {
|
||||
|
||||
const { lastPacket } = useDownload();
|
||||
|
||||
const [seedingList, setSeedingList] = useState<SeedingList[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
window.electron.onSeedingList((value) => setSeedingList(value));
|
||||
}, []);
|
||||
|
||||
const handleOpenGameInstaller = (gameId: number) =>
|
||||
window.electron.openGameInstaller(gameId).then((isBinaryInPath) => {
|
||||
if (!isBinaryInPath) setShowBinaryNotFoundModal(true);
|
||||
@@ -122,6 +128,7 @@ export default function Downloads() {
|
||||
library={group.library}
|
||||
openDeleteGameModal={handleOpenDeleteGameModal}
|
||||
openGameInstaller={handleOpenGameInstaller}
|
||||
seedingList={seedingList}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,7 @@ export function SettingsBehavior() {
|
||||
preferQuitInsteadOfHiding: false,
|
||||
runAtStartup: false,
|
||||
startMinimized: false,
|
||||
seedAfterDownloadCompletes: true,
|
||||
});
|
||||
|
||||
const { t } = useTranslation("settings");
|
||||
@@ -28,6 +29,7 @@ export function SettingsBehavior() {
|
||||
preferQuitInsteadOfHiding: userPreferences.preferQuitInsteadOfHiding,
|
||||
runAtStartup: userPreferences.runAtStartup,
|
||||
startMinimized: userPreferences.startMinimized,
|
||||
seedAfterDownloadCompletes: userPreferences.seedAfterDownloadCompletes,
|
||||
});
|
||||
}
|
||||
}, [userPreferences]);
|
||||
@@ -86,6 +88,16 @@ export function SettingsBehavior() {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CheckboxField
|
||||
label={t("seed_after_download_completes")}
|
||||
checked={form.seedAfterDownloadCompletes}
|
||||
onChange={() =>
|
||||
handleChange({
|
||||
seedAfterDownloadCompletes: !form.seedAfterDownloadCompletes,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ export type GameStatus =
|
||||
| "paused"
|
||||
| "error"
|
||||
| "complete"
|
||||
| "removed";
|
||||
| "removed"
|
||||
| "seeding";
|
||||
|
||||
export type GameShop = "steam" | "epic";
|
||||
|
||||
@@ -124,6 +125,7 @@ export interface Game {
|
||||
objectID: string;
|
||||
shop: GameShop;
|
||||
downloadQueue: DownloadQueue | null;
|
||||
shouldSeed: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -151,6 +153,16 @@ export interface DownloadProgress {
|
||||
game: LibraryGame;
|
||||
}
|
||||
|
||||
export interface SeedingList {
|
||||
progress: number;
|
||||
numPeers: number;
|
||||
numSeeds: number;
|
||||
uploadSpeed: number;
|
||||
gameId: number;
|
||||
folderName: string;
|
||||
fileSize: number;
|
||||
}
|
||||
|
||||
export interface UserPreferences {
|
||||
downloadsPath: string | null;
|
||||
language: string;
|
||||
@@ -161,6 +173,7 @@ export interface UserPreferences {
|
||||
preferQuitInsteadOfHiding: boolean;
|
||||
runAtStartup: boolean;
|
||||
startMinimized: boolean;
|
||||
seedAfterDownloadCompletes: boolean;
|
||||
}
|
||||
|
||||
export interface Steam250Game {
|
||||
|
||||
@@ -50,6 +50,21 @@ class Handler(BaseHTTPRequestHandler):
|
||||
|
||||
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":
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
@@ -107,6 +122,8 @@ class Handler(BaseHTTPRequestHandler):
|
||||
elif data['action'] == 'kill-torrent':
|
||||
torrent_downloader.abort_session()
|
||||
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.end_headers()
|
||||
|
||||
@@ -106,7 +106,7 @@ class TorrentDownloader:
|
||||
params = {'url': magnet, 'save_path': save_path, 'trackers': self.trackers}
|
||||
torrent_handle = self.session.add_torrent(params)
|
||||
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()
|
||||
|
||||
self.downloading_game_id = game_id
|
||||
@@ -151,6 +151,7 @@ class TorrentDownloader:
|
||||
'gameId': self.downloading_game_id,
|
||||
'progress': status.progress,
|
||||
'downloadSpeed': status.download_rate,
|
||||
'uploadSpeed': status.upload_rate,
|
||||
'numPeers': status.num_peers,
|
||||
'numSeeds': status.num_seeds,
|
||||
'status': status.state,
|
||||
@@ -158,8 +159,34 @@ class TorrentDownloader:
|
||||
}
|
||||
|
||||
if status.progress == 1:
|
||||
torrent_handle.pause()
|
||||
self.session.remove_torrent(torrent_handle)
|
||||
self.downloading_game_id = -1
|
||||
|
||||
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