Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Feature/torbox integration #1348

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions python_rpc/http_downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ def __init__(self):
)
)

def start_download(self, url: str, save_path: str, header: str):
def start_download(self, url: str, save_path: str, header: str, out: str = None):
if self.download:
self.aria2.resume([self.download])
else:
downloads = self.aria2.add(url, options={"header": header, "dir": save_path})
downloads = self.aria2.add(url, options={"header": header, "dir": save_path, "out": out})

self.download = downloads[0]

def pause_download(self):
Expand Down
16 changes: 8 additions & 8 deletions python_rpc/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@
torrent_downloader = TorrentDownloader(torrent_session)
downloads[initial_download['game_id']] = torrent_downloader
try:
torrent_downloader.start_download(initial_download['url'], initial_download['save_path'], "")
torrent_downloader.start_download(initial_download['url'], initial_download['save_path'])
except Exception as e:
print("Error starting torrent download", e)
else:
http_downloader = HttpDownloader()
downloads[initial_download['game_id']] = http_downloader
try:
http_downloader.start_download(initial_download['url'], initial_download['save_path'], initial_download.get('header'))
http_downloader.start_download(initial_download['url'], initial_download['save_path'], initial_download.get('header'), initial_download.get("out"))
except Exception as e:
print("Error starting http download", e)

Expand All @@ -45,7 +45,7 @@
torrent_downloader = TorrentDownloader(torrent_session, lt.torrent_flags.upload_mode)
downloads[seed['game_id']] = torrent_downloader
try:
torrent_downloader.start_download(seed['url'], seed['save_path'], "")
torrent_downloader.start_download(seed['url'], seed['save_path'])
except Exception as e:
print("Error starting seeding", e)

Expand Down Expand Up @@ -140,18 +140,18 @@ def action():

if url.startswith('magnet'):
if existing_downloader and isinstance(existing_downloader, TorrentDownloader):
existing_downloader.start_download(url, data['save_path'], "")
existing_downloader.start_download(url, data['save_path'])
else:
torrent_downloader = TorrentDownloader(torrent_session)
downloads[game_id] = torrent_downloader
torrent_downloader.start_download(url, data['save_path'], "")
torrent_downloader.start_download(url, data['save_path'])
else:
if existing_downloader and isinstance(existing_downloader, HttpDownloader):
existing_downloader.start_download(url, data['save_path'], data.get('header'))
existing_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'))
else:
http_downloader = HttpDownloader()
downloads[game_id] = http_downloader
http_downloader.start_download(url, data['save_path'], data.get('header'))
http_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'))

downloading_game_id = game_id

Expand All @@ -167,7 +167,7 @@ def action():
elif action == 'resume_seeding':
torrent_downloader = TorrentDownloader(torrent_session, lt.torrent_flags.upload_mode)
downloads[game_id] = torrent_downloader
torrent_downloader.start_download(data['url'], data['save_path'], "")
torrent_downloader.start_download(data['url'], data['save_path'])
elif action == 'pause_seeding':
downloader = downloads.get(game_id)
if downloader:
Expand Down
2 changes: 1 addition & 1 deletion python_rpc/torrent_downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def __init__(self, torrent_session, flags = lt.torrent_flags.auto_managed):
"http://bvarf.tracker.sh:2086/announce",
]

def start_download(self, magnet: str, save_path: str, header: str):
def start_download(self, magnet: str, save_path: str):
params = {'url': magnet, 'save_path': save_path, 'trackers': self.trackers, 'flags': self.flags}
self.torrent_handle = self.session.add_torrent(params)
self.torrent_handle.resume()
Expand Down
3 changes: 2 additions & 1 deletion src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,8 @@
"launch_minimized": "Launch Hydra minimized",
"disable_nsfw_alert": "Disable NSFW alert",
"seed_after_download_complete": "Seed after download complete",
"show_hidden_achievement_description": "Show hidden achievements description before unlocking them"
"show_hidden_achievement_description": "Show hidden achievements description before unlocking them",
"debrid_services": "Debrid Services"
},
"notifications": {
"download_complete": "Download complete",
Expand Down
3 changes: 2 additions & 1 deletion src/locales/pt-BR/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,8 @@
"launch_minimized": "Iniciar o Hydra minimizado",
"disable_nsfw_alert": "Desativar alerta de conteúdo inapropriado",
"seed_after_download_complete": "Semear após a conclusão do download",
"show_hidden_achievement_description": "Mostrar descrição de conquistas ocultas antes de debloqueá-las"
"show_hidden_achievement_description": "Mostrar descrição de conquistas ocultas antes de debloqueá-las",
"debrid_services": "Serviços Debrid"
},
"notifications": {
"download_complete": "Download concluído",
Expand Down
3 changes: 3 additions & 0 deletions src/main/entity/user-preferences.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export class UserPreferences {
@Column("text", { nullable: true })
realDebridApiToken: string | null;

@Column("text", { nullable: true })
torBoxApiToken: string | null;

@Column("boolean", { default: false })
downloadNotificationsEnabled: boolean;

Expand Down
4 changes: 0 additions & 4 deletions src/main/events/auth/sign-out.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { registerEvent } from "../register-event";
import { DownloadManager, HydraApi, gamesPlaytime } from "@main/services";
import { dataSource } from "@main/data-source";
import { DownloadQueue, Game, UserAuth, UserSubscription } from "@main/entity";
import { PythonRPC } from "@main/services/python-rpc";

const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
const databaseOperations = dataSource
Expand All @@ -27,9 +26,6 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
/* Cancels any ongoing downloads */
DownloadManager.cancelDownload();

/* Disconnects libtorrent */
PythonRPC.kill();

HydraApi.handleSignOut();

await Promise.all([
Expand Down
12 changes: 10 additions & 2 deletions src/main/events/cloud-save/upload-save-game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ const bundleBackup = async (
return tarLocation;
};

const uploadSaveGame = async (
_event: Electron.IpcMainInvokeEvent,
export const createBackup = async (
objectId: string,
shop: GameShop,
downloadOptionTitle: string | null
Expand Down Expand Up @@ -108,4 +107,13 @@ const uploadSaveGame = async (
});
};

const uploadSaveGame = async (
_event: Electron.IpcMainInvokeEvent,
objectId: string,
shop: GameShop,
downloadOptionTitle: string | null
) => {
return createBackup(objectId, shop, downloadOptionTitle);
};

registerEvent("uploadSaveGame", uploadSaveGame);
2 changes: 2 additions & 0 deletions src/main/knex-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { AddShouldSeedColumn } from "./migrations/20241108200154_add_should_seed
import { AddSeedAfterDownloadColumn } from "./migrations/20241108201806_add_seed_after_download";
import { AddHiddenAchievementDescriptionColumn } from "./migrations/20241216140633_add_hidden_achievement_description_column ";
import { AddLaunchOptionsColumnToGame } from "./migrations/20241226044022_add_launch_options_column_to_game";
import { AddTorBoxApiToken } from "./migrations/20250111182229_add_torbox_api_token_column";

export type HydraMigration = Knex.Migration & { name: string };

Expand All @@ -39,6 +40,7 @@ class MigrationSource implements Knex.MigrationSource<HydraMigration> {
AddSeedAfterDownloadColumn,
AddHiddenAchievementDescriptionColumn,
AddLaunchOptionsColumnToGame,
AddTorBoxApiToken,
]);
}
getMigrationName(migration: HydraMigration): string {
Expand Down
7 changes: 6 additions & 1 deletion src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,19 @@ import { uploadGamesBatch } from "./services/library-sync";
import { Aria2 } from "./services/aria2";
import { Downloader } from "@shared";
import { IsNull, Not } from "typeorm";
import { TorBoxClient } from "./services/download/torbox";

const loadState = async (userPreferences: UserPreferences | null) => {
import("./events");

Aria2.spawn();

if (userPreferences?.realDebridApiToken) {
RealDebridClient.authorize(userPreferences?.realDebridApiToken);
RealDebridClient.authorize(userPreferences.realDebridApiToken);
}

if (userPreferences?.torBoxApiToken) {
TorBoxClient.authorize(userPreferences?.torBoxApiToken);
}

Ludusavi.addManifestToLudusaviConfig();
Expand Down
17 changes: 17 additions & 0 deletions src/main/migrations/20250111182229_add_torbox_api_token_column.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { HydraMigration } from "@main/knex-client";
import type { Knex } from "knex";

export const AddTorBoxApiToken: HydraMigration = {
name: "AddTorBoxApiToken",
up: (knex: Knex) => {
return knex.schema.alterTable("user_preferences", (table) => {
return table.string("torBoxApiToken").nullable();
});
},

down: async (knex: Knex) => {
return knex.schema.alterTable("user_preferences", (table) => {
return table.dropColumn("torBoxApiToken");
});
},
};
21 changes: 21 additions & 0 deletions src/main/services/download/download-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity
import { RealDebridClient } from "./real-debrid";
import path from "path";
import { logger } from "../logger";
import { TorBoxClient } from "./torbox";
import axios from "axios";

export class DownloadManager {
private static downloadingGameId: number | null = null;
Expand All @@ -29,6 +31,7 @@ export class DownloadManager {
game?.status === "active"
? await this.getDownloadPayload(game).catch(() => undefined)
: undefined,

initialSeeding?.map((game) => ({
game_id: game.id,
url: game.uri!,
Expand Down Expand Up @@ -260,11 +263,16 @@ export class DownloadManager {
case Downloader.PixelDrain: {
const id = game.uri!.split("/").pop();

const name = await axios
.get(`https://pixeldrain.com/api/file/${id}/info`)
.then((res) => res.data.name as string);

return {
action: "start",
game_id: game.id,
url: `https://pixeldrain.com/api/file/${id}?download`,
save_path: game.downloadPath!,
out: name,
};
}
case Downloader.Qiwi: {
Expand Down Expand Up @@ -304,6 +312,19 @@ export class DownloadManager {
save_path: game.downloadPath!,
};
}
case Downloader.TorBox: {
const { name, url } = await TorBoxClient.getDownloadInfo(game.uri!);
console.log(url, name);

if (!url) return;
return {
action: "start",
game_id: game.id,
url,
save_path: game.downloadPath!,
out: name,
};
}
}
}

Expand Down
39 changes: 21 additions & 18 deletions src/main/services/download/torbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,23 @@ import type {
TorBoxAddTorrentRequest,
TorBoxRequestLinkRequest,
} from "@types";
import { logger } from "../logger";

export class TorBoxClient {
private static instance: AxiosInstance;
private static readonly baseURL = "https://api.torbox.app/v1/api";
public static apiToken: string;
private static apiToken: string;

static authorize(apiToken: string) {
this.apiToken = apiToken;
this.instance = axios.create({
baseURL: this.baseURL,
headers: {
Authorization: `Bearer ${apiToken}`,
},
});
this.apiToken = apiToken;
}

static async addMagnet(magnet: string) {
private static async addMagnet(magnet: string) {
const form = new FormData();
form.append("magnet", magnet);

Expand All @@ -32,6 +31,10 @@ export class TorBoxClient {
form
);

if (!response.data.success) {
throw new Error(response.data.detail);
}

return response.data.data;
}

Expand All @@ -55,22 +58,16 @@ export class TorBoxClient {
}

static async requestLink(id: number) {
const searchParams = new URLSearchParams({});

searchParams.set("token", this.apiToken);
searchParams.set("torrent_id", id.toString());
searchParams.set("zip_link", "true");
const searchParams = new URLSearchParams({
token: this.apiToken,
torrent_id: id.toString(),
zip_link: "true",
});

const response = await this.instance.get<TorBoxRequestLinkRequest>(
"/torrents/requestdl?" + searchParams.toString()
);

if (response.status !== 200) {
logger.error(response.data.error);
logger.error(response.data.detail);
return null;
}

return response.data.data;
}

Expand All @@ -81,17 +78,23 @@ export class TorBoxClient {
return response.data.data;
}

static async getTorrentId(magnetUri: string) {
private static async getTorrentIdAndName(magnetUri: string) {
const userTorrents = await this.getAllTorrentsFromUser();

const { infoHash } = await parseTorrent(magnetUri);
const userTorrent = userTorrents.find(
(userTorrent) => userTorrent.hash === infoHash
);

if (userTorrent) return userTorrent.id;
if (userTorrent) return { id: userTorrent.id, name: userTorrent.name };

const torrent = await this.addMagnet(magnetUri);
return torrent.torrent_id;
return { id: torrent.torrent_id, name: torrent.name };
}

static async getDownloadInfo(uri: string) {
const { id, name } = await this.getTorrentIdAndName(uri);
const url = await this.requestLink(id);
return { url, name: `${name}.zip` };
}
}
3 changes: 2 additions & 1 deletion src/main/services/hosters/datanodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ export class DatanodesApi {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
},
maxRedirects: 0, validateStatus: (status: number) => status === 302 || status < 400,
maxRedirects: 0,
validateStatus: (status: number) => status === 302 || status < 400,
}
);

Expand Down
5 changes: 5 additions & 0 deletions src/main/services/process-watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Game } from "@main/entity";
import axios from "axios";
import { exec } from "child_process";
import { ProcessPayload } from "./download/types";
import { createBackup } from "@main/events/cloud-save/upload-save-game";

const commands = {
findWineDir: `lsof -c wine 2>/dev/null | grep '/drive_c/windows$' | head -n 1 | awk '{for(i=9;i<=NF;i++) printf "%s ", $i; print ""}'`,
Expand Down Expand Up @@ -269,6 +270,10 @@ const onCloseGame = (game: Game) => {
gamesPlaytime.delete(game.id);

if (game.remoteId) {
// create backup
// todo: check for hydra cloud?
createBackup(game.objectID, game.shop, "");

updateGamePlaytime(
game,
performance.now() - gamePlaytime.lastSyncTick,
Expand Down
Binary file added src/renderer/src/assets/icons/torbox.webp
Binary file not shown.
1 change: 1 addition & 0 deletions src/renderer/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const DOWNLOADER_NAME = {
[Downloader.PixelDrain]: "PixelDrain",
[Downloader.Qiwi]: "Qiwi",
[Downloader.Datanodes]: "Datanodes",
[Downloader.TorBox]: "TorBox",
};

export const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
Loading
Loading