mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-24 03:11:03 +00:00
Merge branch 'main' into fix/linux-game-tracking
This commit is contained in:
@@ -1,23 +1,21 @@
|
||||
import type { HowLongToBeatCategory } from "@types";
|
||||
import { getHowLongToBeatGame, searchHowLongToBeat } from "@main/services";
|
||||
import type { GameShop, HowLongToBeatCategory } from "@types";
|
||||
|
||||
import { registerEvent } from "../register-event";
|
||||
import { formatName } from "@shared";
|
||||
import { HydraApi } from "@main/services";
|
||||
|
||||
const getHowLongToBeat = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
title: string
|
||||
objectId: string,
|
||||
shop: GameShop
|
||||
): Promise<HowLongToBeatCategory[] | null> => {
|
||||
const response = await searchHowLongToBeat(title);
|
||||
|
||||
const game = response.data.find((game) => {
|
||||
return formatName(game.game_name) === formatName(title);
|
||||
const params = new URLSearchParams({
|
||||
objectId,
|
||||
shop,
|
||||
});
|
||||
|
||||
if (!game) return null;
|
||||
const howLongToBeat = await getHowLongToBeatGame(String(game.game_id));
|
||||
|
||||
return howLongToBeat;
|
||||
return HydraApi.get(`/games/how-long-to-beat?${params.toString()}`, null, {
|
||||
needsAuth: false,
|
||||
});
|
||||
};
|
||||
|
||||
registerEvent("getHowLongToBeat", getHowLongToBeat);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { appVersion, defaultDownloadsPath } from "@main/constants";
|
||||
import { appVersion, defaultDownloadsPath, isStaging } from "@main/constants";
|
||||
import { ipcMain } from "electron";
|
||||
|
||||
import "./catalogue/get-catalogue";
|
||||
@@ -72,5 +72,6 @@ import "./misc/show-item-in-folder";
|
||||
|
||||
ipcMain.handle("ping", () => "pong");
|
||||
ipcMain.handle("getVersion", () => appVersion);
|
||||
ipcMain.handle("isStaging", () => isStaging);
|
||||
ipcMain.handle("isPortableVersion", () => isPortableVersion());
|
||||
ipcMain.handle("getDefaultDownloadsPath", () => defaultDownloadsPath);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { registerEvent } from "../register-event";
|
||||
import parseTorrent from "parse-torrent";
|
||||
import type { StartGameDownloadPayload } from "@types";
|
||||
import { DownloadManager, HydraApi, logger } from "@main/services";
|
||||
|
||||
@@ -9,7 +8,6 @@ import { createGame } from "@main/services/library-sync";
|
||||
import { steamUrlBuilder } from "@shared";
|
||||
import { dataSource } from "@main/data-source";
|
||||
import { DownloadQueue, Game } from "@main/entity";
|
||||
import { HydraAnalytics } from "@main/services/hydra-analytics";
|
||||
|
||||
const startGameDownload = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
@@ -91,17 +89,6 @@ const startGameDownload = async (
|
||||
logger.error("Failed to create game download", err);
|
||||
});
|
||||
|
||||
if (uri.startsWith("magnet:")) {
|
||||
try {
|
||||
const { infoHash } = await parseTorrent(payload.uri);
|
||||
if (infoHash) {
|
||||
HydraAnalytics.postDownload(infoHash).catch(() => {});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Failed to parse torrent", err);
|
||||
}
|
||||
}
|
||||
|
||||
await DownloadManager.cancelDownload(updatedGame!.id);
|
||||
await DownloadManager.startDownload(updatedGame!);
|
||||
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import axios from "axios";
|
||||
import { requestWebPage } from "@main/helpers";
|
||||
import type {
|
||||
HowLongToBeatCategory,
|
||||
HowLongToBeatSearchResponse,
|
||||
} from "@types";
|
||||
import { formatName } from "@shared";
|
||||
import { logger } from "./logger";
|
||||
import UserAgent from "user-agents";
|
||||
|
||||
const state = {
|
||||
apiKey: null as string | null,
|
||||
};
|
||||
|
||||
const getHowLongToBeatSearchApiKey = async () => {
|
||||
const userAgent = new UserAgent();
|
||||
|
||||
const document = await requestWebPage("https://howlongtobeat.com/");
|
||||
const scripts = Array.from(document.querySelectorAll("script"));
|
||||
|
||||
const appScript = scripts.find((script) =>
|
||||
script.src.startsWith("/_next/static/chunks/pages/_app")
|
||||
);
|
||||
|
||||
if (!appScript) return null;
|
||||
|
||||
const response = await axios.get(
|
||||
`https://howlongtobeat.com${appScript.src}`,
|
||||
{
|
||||
headers: {
|
||||
"User-Agent": userAgent.toString(),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const results = /fetch\("\/api\/search\/"\.concat\("(.*?)"\)/gm.exec(
|
||||
response.data
|
||||
);
|
||||
|
||||
if (!results) return null;
|
||||
|
||||
return results[1];
|
||||
};
|
||||
|
||||
export const searchHowLongToBeat = async (gameName: string) => {
|
||||
state.apiKey = state.apiKey ?? (await getHowLongToBeatSearchApiKey());
|
||||
if (!state.apiKey) return { data: [] };
|
||||
|
||||
const userAgent = new UserAgent();
|
||||
|
||||
const response = await axios
|
||||
.post(
|
||||
`https://howlongtobeat.com/api/search/${state.apiKey}`,
|
||||
{
|
||||
searchType: "games",
|
||||
searchTerms: formatName(gameName).split(" "),
|
||||
searchPage: 1,
|
||||
size: 20,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"User-Agent": userAgent.toString(),
|
||||
Referer: "https://howlongtobeat.com/",
|
||||
},
|
||||
}
|
||||
)
|
||||
.catch((error) => {
|
||||
logger.error("Error searching HowLongToBeat:", error?.response?.status);
|
||||
return { data: { data: [] } };
|
||||
});
|
||||
|
||||
return response.data as HowLongToBeatSearchResponse;
|
||||
};
|
||||
|
||||
const parseListItems = ($lis: Element[]) => {
|
||||
return $lis.map(($li) => {
|
||||
const title = $li.querySelector("h4")?.textContent;
|
||||
const [, accuracyClassName] = Array.from(($li as HTMLElement).classList);
|
||||
|
||||
const accuracy = accuracyClassName.split("time_").at(1);
|
||||
|
||||
return {
|
||||
title: title ?? "",
|
||||
duration: $li.querySelector("h5")?.textContent ?? "",
|
||||
accuracy: accuracy ?? "",
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const getHowLongToBeatGame = async (
|
||||
id: string
|
||||
): Promise<HowLongToBeatCategory[]> => {
|
||||
const document = await requestWebPage(`https://howlongtobeat.com/game/${id}`);
|
||||
|
||||
const $ul = document.querySelector(".shadow_shadow ul");
|
||||
if (!$ul) return [];
|
||||
|
||||
const $lis = Array.from($ul.children);
|
||||
|
||||
const [$firstLi] = $lis;
|
||||
|
||||
if ($firstLi.tagName === "DIV") {
|
||||
const $pcData = $lis.find(($li) => $li.textContent?.includes("PC"));
|
||||
return parseListItems(Array.from($pcData?.querySelectorAll("li") ?? []));
|
||||
}
|
||||
|
||||
return parseListItems($lis);
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
import { userSubscriptionRepository } from "@main/repository";
|
||||
import axios from "axios";
|
||||
import { appVersion } from "@main/constants";
|
||||
|
||||
export class HydraAnalytics {
|
||||
private static instance = axios.create({
|
||||
baseURL: import.meta.env.MAIN_VITE_ANALYTICS_API_URL,
|
||||
headers: { "User-Agent": `Hydra Launcher v${appVersion}` },
|
||||
});
|
||||
|
||||
private static async hasActiveSubscription() {
|
||||
const userSubscription = await userSubscriptionRepository.findOne({
|
||||
where: { id: 1 },
|
||||
});
|
||||
|
||||
return (
|
||||
userSubscription?.expiresAt && userSubscription.expiresAt > new Date()
|
||||
);
|
||||
}
|
||||
|
||||
static async postDownload(hash: string) {
|
||||
const hasSubscription = await this.hasActiveSubscription();
|
||||
|
||||
return this.instance
|
||||
.post("/track", {
|
||||
event: "download",
|
||||
attributes: {
|
||||
hash,
|
||||
hasSubscription,
|
||||
},
|
||||
})
|
||||
.then((response) => response.data);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { UserNotLoggedInError, SubscriptionRequiredError } from "@shared";
|
||||
import { omit } from "lodash-es";
|
||||
import { appVersion } from "@main/constants";
|
||||
import { getUserData } from "./user/get-user-data";
|
||||
import { isFuture, isToday } from "date-fns";
|
||||
|
||||
interface HydraApiOptions {
|
||||
needsAuth?: boolean;
|
||||
@@ -45,10 +46,8 @@ export class HydraApi {
|
||||
}
|
||||
|
||||
private static hasActiveSubscription() {
|
||||
return (
|
||||
this.userAuth.subscription?.expiresAt &&
|
||||
this.userAuth.subscription.expiresAt > new Date()
|
||||
);
|
||||
const expiresAt = this.userAuth.subscription?.expiresAt;
|
||||
return expiresAt && (isFuture(expiresAt) || isToday(expiresAt));
|
||||
}
|
||||
|
||||
static async handleExternalAuth(uri: string) {
|
||||
|
||||
@@ -4,7 +4,6 @@ export * from "./steam-250";
|
||||
export * from "./steam-grid";
|
||||
export * from "./window-manager";
|
||||
export * from "./download";
|
||||
export * from "./how-long-to-beat";
|
||||
export * from "./process-watcher";
|
||||
export * from "./main-loop";
|
||||
export * from "./hydra-api";
|
||||
|
||||
Reference in New Issue
Block a user