Skip to content
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
2 changes: 1 addition & 1 deletion electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,4 @@ publish:
owner: hydralauncher
repo: hydra
electronDownload:
mirror: https://npmmirror.com/mirrors/electron/
mirror: https://github.com/electron/electron/releases/download/v
60 changes: 37 additions & 23 deletions python_rpc/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
from http_downloader import HttpDownloader
from profile_image_processor import ProfileImageProcessor
from http_multi_link_downloader import HttpMultiLinkDownloader
import libtorrent as lt

try:
import libtorrent as lt
except Exception as e:
lt = None
print("Warning: libtorrent is not available, torrent features disabled:", e)

app = Flask(__name__)

Expand All @@ -19,7 +24,10 @@
# This can be streamed down from Node
downloading_game_id = -1

torrent_session = lt.session({'listen_interfaces': '0.0.0.0:{port}'.format(port=torrent_port)})
if lt is not None:
torrent_session = lt.session({'listen_interfaces': '0.0.0.0:{port}'.format(port=torrent_port)})
else:
torrent_session = None

if start_download_payload:
initial_download = json.loads(urllib.parse.unquote(start_download_payload))
Expand All @@ -34,12 +42,15 @@
except Exception as e:
print("Error starting multi-link download", e)
elif initial_download['url'].startswith('magnet'):
torrent_downloader = TorrentDownloader(torrent_session)
downloads[initial_download['game_id']] = torrent_downloader
try:
torrent_downloader.start_download(initial_download['url'], initial_download['save_path'])
except Exception as e:
print("Error starting torrent download", e)
if lt is None:
print("Warning: libtorrent is not available, skipping initial torrent download for", initial_download['game_id'])
else:
torrent_downloader = TorrentDownloader(torrent_session)
downloads[initial_download['game_id']] = torrent_downloader
try:
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
Expand All @@ -49,14 +60,17 @@
print("Error starting http download", e)

if start_seeding_payload:
initial_seeding = json.loads(urllib.parse.unquote(start_seeding_payload))
for seed in initial_seeding:
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'])
except Exception as e:
print("Error starting seeding", e)
if lt is None:
print("Warning: libtorrent is not available, skipping initial seeding payload")
else:
initial_seeding = json.loads(urllib.parse.unquote(start_seeding_payload))
for seed in initial_seeding:
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'])
except Exception as e:
print("Error starting seeding", e)

def validate_rpc_password():
"""Middleware to validate RPC password."""
Expand Down Expand Up @@ -153,11 +167,8 @@ def profile_image():
data = request.get_json()
image_path = data.get('image_path')

# use webp as default value for target_extension
target_extension = data.get('target_extension') or 'webp'

try:
processed_image_path, mime_type = ProfileImageProcessor.process_image(image_path, target_extension)
processed_image_path, mime_type = ProfileImageProcessor.process_image(image_path)
return jsonify({'imagePath': processed_image_path, 'mimeType': mime_type}), 200
except Exception as e:
return jsonify({"error": str(e)}), 400
Expand Down Expand Up @@ -217,9 +228,12 @@ def action():
if downloader:
downloader.cancel_download()
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'])
if lt is None:
print("Warning: libtorrent is not available, cannot resume seeding for", game_id)
else:
torrent_downloader = TorrentDownloader(torrent_session, lt.torrent_flags.upload_mode)
downloads[game_id] = torrent_downloader
torrent_downloader.start_download(data['url'], data['save_path'])
elif action == 'pause_seeding':
downloader = downloads.get(game_id)
if downloader:
Expand Down
8 changes: 4 additions & 4 deletions python_rpc/profile_image_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
class ProfileImageProcessor:

@staticmethod
def get_parsed_image_data(image_path, target_extension):
def get_parsed_image_data(image_path):
Image.MAX_IMAGE_PIXELS = 933120000

image = Image.open(image_path)
Expand All @@ -16,7 +16,7 @@ def get_parsed_image_data(image_path, target_extension):
return image_path, mime_type
else:
new_uuid = str(uuid.uuid4())
new_image_path = os.path.join(tempfile.gettempdir(), new_uuid) + "." + target_extension
new_image_path = os.path.join(tempfile.gettempdir(), new_uuid) + ".webp"
image.save(new_image_path)

new_image = Image.open(new_image_path)
Expand All @@ -26,5 +26,5 @@ def get_parsed_image_data(image_path, target_extension):


@staticmethod
def process_image(image_path, target_extension):
return ProfileImageProcessor.get_parsed_image_data(image_path, target_extension)
def process_image(image_path):
return ProfileImageProcessor.get_parsed_image_data(image_path)
17 changes: 15 additions & 2 deletions python_rpc/torrent_downloader.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import libtorrent as lt
try:
import libtorrent as lt
except Exception as e:
lt = None
print("Warning: libtorrent is not available, torrent features disabled:", e)


class TorrentDownloader:
def __init__(self, torrent_session, flags = lt.torrent_flags.auto_managed):
def __init__(self, torrent_session, flags=None):
if lt is None:
# Torrent functionality is disabled when libtorrent is missing.
# The caller should avoid using TorrentDownloader in this case.
raise RuntimeError("libtorrent is not available; TorrentDownloader cannot be used")

if flags is None:
flags = lt.torrent_flags.auto_managed

self.torrent_handle = None
self.session = torrent_session
self.flags = flags
Expand Down
22 changes: 21 additions & 1 deletion src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,16 @@
"launch_options": "Launch Options",
"launch_options_description": "Advanced users may choose to enter modifications to their launch options (experimental feature)",
"launch_options_placeholder": "No parameter specified",
"collections_section_title": "Collections & tags",
"collections_section_description": "Organize this game with custom tags and a play status",
"collections_tags_label": "Tags",
"collections_tags_placeholder": "Add tags separated by commas (e.g. To beat later, Co-op, Horror)",
"play_status": "Status",
"play_status_none": "No status",
"play_status_not_installed": "Not installed",
"play_status_installed": "Installed",
"play_status_completed": "Completed",
"play_status_abandoned": "Abandoned",
"no_download_option_info": "No information available",
"backup_deletion_failed": "Failed to delete backup",
"max_number_of_artifacts_reached": "Maximum number of backups reached for this game",
Expand Down Expand Up @@ -467,6 +477,14 @@
"found_download_option_zero": "No download option found",
"found_download_option_one": "Found {{countFormatted}} download option",
"found_download_option_other": "Found {{countFormatted}} download options",
"export_download_sources": "Export sources",
"import_download_sources": "Import sources",
"download_sources_json_filter": "Download sources JSON",
"download_sources_export_success": "Exported {{countFormatted}} sources",
"download_sources_export_failed": "Failed to export download sources",
"download_sources_import_success": "Imported {{countFormatted}} sources",
"download_sources_import_skipped": "Skipped {{countFormatted}} sources",
"download_sources_import_failed": "Failed to import download sources",
"import": "Import",
"importing": "Importing...",
"public": "Public",
Expand Down Expand Up @@ -730,7 +748,9 @@
"manual_playtime_tooltip": "This playtime has been manually updated",
"all_games": "All Games",
"recently_played": "Recently Played",
"favorites": "Favorites"
"favorites": "Favorites",
"play_status_all": "All statuses",
"collections_tags_filter_placeholder": "Filter by tag"
},
"achievement": {
"achievement_unlocked": "Achievement unlocked",
Expand Down
25 changes: 24 additions & 1 deletion src/locales/ru/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
},
"header": {
"search": "Поиск",
"search_library": "Поиск по библиотеке",
"home": "Главная",
"catalogue": "Каталог",
"downloads": "Загрузки",
Expand Down Expand Up @@ -285,6 +286,16 @@
"launch_options": "Параметры запуска",
"launch_options_description": "Опытные пользователи могут внести изменения в параметры запуска",
"launch_options_placeholder": "Параметр не указан ",
"collections_section_title": "Коллекции и теги",
"collections_section_description": "Организуйте игру с помощью пользовательских тегов и статуса прохождения",
"collections_tags_label": "Теги",
"collections_tags_placeholder": "Добавьте теги через запятую (например: Пройти позже, Кооператив, Хорроры)",
"play_status": "Статус",
"play_status_none": "Без статуса",
"play_status_not_installed": "Не установлена",
"play_status_installed": "Установлена",
"play_status_completed": "Пройдена",
"play_status_abandoned": "Брошена",
"no_download_option_info": "Информация недоступна",
"backup_deletion_failed": "Не удалось удалить резервную копию",
"max_number_of_artifacts_reached": "Достигнуто максимальное количество резервных копий для этой игры",
Expand Down Expand Up @@ -353,6 +364,8 @@
"caption": "Субтитры",
"audio": "Аудио",
"filter_by_source": "Фильтр по источнику",
"sort_by_size": "Сортировать по размеру",
"sort_by_date": "Сортировать по дате",
"no_repacks_found": "Источники для этой игры не найдены",
"show": "Показать",
"hide": "Скрыть",
Expand Down Expand Up @@ -465,6 +478,14 @@
"found_download_option_zero": "Не найдено вариантов загрузки",
"found_download_option_one": "Найден {{countFormatted}} вариант загрузки",
"found_download_option_other": "Найдено {{countFormatted}} вариантов загрузки",
"export_download_sources": "Экспортировать источники",
"import_download_sources": "Импортировать источники",
"download_sources_json_filter": "JSON источников загрузки",
"download_sources_export_success": "Экспортировано источников: {{countFormatted}}",
"download_sources_export_failed": "Не удалось экспортировать источники загрузки",
"download_sources_import_success": "Импортировано источников: {{countFormatted}}",
"download_sources_import_skipped": "Пропущено источников: {{countFormatted}}",
"download_sources_import_failed": "Не удалось импортировать источники загрузки",
"import": "Импортировать",
"importing": "Импортируется...",
"public": "Публичный",
Expand Down Expand Up @@ -755,6 +776,8 @@
"manual_playtime_tooltip": "Время игры было обновлено вручную",
"all_games": "Все игры",
"recently_played": "Недавно сыгранные",
"favorites": "Избранное"
"favorites": "Избранное",
"play_status_all": "Все статусы",
"collections_tags_filter_placeholder": "Фильтр по тегу"
}
}
78 changes: 39 additions & 39 deletions src/main/constants.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,59 @@
import { app } from "electron";
import path from "node:path";
import { SystemPath } from "./services/system-path";
import { SystemPath } from "@main/services/system-path";

export const defaultDownloadsPath = SystemPath.getPath("downloads");
const apiUrl = import.meta.env.MAIN_VITE_API_URL ?? "";

export const isStaging = import.meta.env.MAIN_VITE_API_URL.includes("staging");
export const isStaging = () => apiUrl.includes("staging");

export const windowsStartMenuPath = path.join(
SystemPath.getPath("appData"),
"Microsoft",
"Windows",
"Start Menu",
"Programs"
);
export const defaultDownloadsPath = () => SystemPath.getPath("downloads");

export const publicProfilePath = "C:/Users/Public";
export const windowsStartMenuPath = () =>
path.join(
SystemPath.getPath("appData"),
"Microsoft",
"Windows",
"Start Menu",
"Programs"
);

export const levelDatabasePath = path.join(
SystemPath.getPath("userData"),
`hydra-db${isStaging ? "-staging" : ""}`
);
export const publicProfilePath = () => "C:/Users/Public";

export const commonRedistPath = path.join(
SystemPath.getPath("userData"),
"CommonRedist"
);
export const levelDatabasePath = () =>
path.join(
SystemPath.getPath("userData"),
`hydra-db${isStaging() ? "-staging" : ""}`
);

export const logsPath = path.join(
SystemPath.getPath("userData"),
`logs${isStaging ? "-staging" : ""}`
);
export const commonRedistPath = () =>
path.join(SystemPath.getPath("userData"), "CommonRedist");

export const achievementSoundPath = app.isPackaged
? path.join(process.resourcesPath, "achievement.wav")
: path.join(__dirname, "..", "..", "resources", "achievement.wav");
export const logsPath = () =>
path.join(SystemPath.getPath("userData"), `logs${isStaging() ? "-staging" : ""}`);

export const backupsPath = path.join(SystemPath.getPath("userData"), "Backups");
export const achievementSoundPath = () =>
app.isPackaged
? path.join(process.resourcesPath, "achievement.wav")
: path.join(__dirname, "..", "..", "resources", "achievement.wav");

export const appVersion = app.getVersion() + (isStaging ? "-staging" : "");
export const backupsPath = () =>
path.join(SystemPath.getPath("userData"), "Backups");

export const ASSETS_PATH = path.join(SystemPath.getPath("userData"), "Assets");
export const appVersion = () =>
app.getVersion() + (isStaging() ? "-staging" : "");

export const THEMES_PATH = path.join(SystemPath.getPath("userData"), "themes");
export const ASSETS_PATH = () =>
path.join(SystemPath.getPath("userData"), "Assets");

export const THEMES_PATH = () =>
path.join(SystemPath.getPath("userData"), "themes");

export const MAIN_LOOP_INTERVAL = 2000;

export const DEFAULT_ACHIEVEMENT_SOUND_VOLUME = 0.15;

export const DECKY_PLUGINS_LOCATION = path.join(
SystemPath.getPath("home"),
"homebrew",
"plugins"
);
export const DECKY_PLUGINS_LOCATION = () =>
path.join(SystemPath.getPath("home"), "homebrew", "plugins");

export const HYDRA_DECKY_PLUGIN_LOCATION = path.join(
DECKY_PLUGINS_LOCATION,
"Hydra"
);
export const HYDRA_DECKY_PLUGIN_LOCATION = () =>
path.join(DECKY_PLUGINS_LOCATION(), "Hydra");
6 changes: 3 additions & 3 deletions src/main/events/cloud-save/download-game-artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ const restoreLudusaviBackup = (
addWinePrefixToWindowsPath(userProfilePath, winePrefixPath)
)
.replace(
publicProfilePath,
addWinePrefixToWindowsPath(publicProfilePath, winePrefixPath)
publicProfilePath(),
addWinePrefixToWindowsPath(publicProfilePath(), winePrefixPath)
);

logger.info(`Moving ${sourcePath} to ${destinationPath}`);
Expand Down Expand Up @@ -112,7 +112,7 @@ const downloadGameArtifact = async (
}>(`/profile/games/artifacts/${gameArtifactId}/download`);

const zipLocation = path.join(SystemPath.getPath("userData"), objectKey);
const backupPath = path.join(backupsPath, `${shop}-${objectId}`);
const backupPath = path.join(backupsPath(), `${shop}-${objectId}`);

if (fs.existsSync(backupPath)) {
fs.rmSync(backupPath, {
Expand Down
Loading
Loading