mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 13:34:54 +03:00
fix: migrating hltb to api
This commit is contained in:
parent
7f600a0cbf
commit
0fc6d69851
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -48,6 +48,7 @@ jobs:
|
||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
|
||||
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
|
||||
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
|
||||
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.RENDERER_VITE_EXTERNAL_RESOURCES_URL }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Windows
|
||||
@ -59,6 +60,7 @@ jobs:
|
||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
|
||||
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
|
||||
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
|
||||
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.RENDERER_VITE_EXTERNAL_RESOURCES_URL }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create artifact
|
||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -47,6 +47,7 @@ jobs:
|
||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
|
||||
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
|
||||
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
|
||||
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.RENDERER_VITE_EXTERNAL_RESOURCES_URL }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
@ -57,6 +58,7 @@ jobs:
|
||||
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
|
||||
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
|
||||
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
|
||||
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.RENDERER_VITE_EXTERNAL_RESOURCES_URL }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Create artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
|
@ -1,23 +1,27 @@
|
||||
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));
|
||||
const response = await HydraApi.get(
|
||||
`/games/how-long-to-beat?${params.toString()}`,
|
||||
null,
|
||||
{
|
||||
needsAuth: false,
|
||||
}
|
||||
);
|
||||
|
||||
return howLongToBeat;
|
||||
return response;
|
||||
};
|
||||
|
||||
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,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);
|
||||
};
|
@ -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";
|
||||
|
@ -15,7 +15,6 @@ import type {
|
||||
import type { CatalogueCategory } from "@shared";
|
||||
import type { AxiosProgressEvent } from "axios";
|
||||
import { GameAchievement } from "@main/entity";
|
||||
import { isStaging } from "@main/constants";
|
||||
|
||||
contextBridge.exposeInMainWorld("electron", {
|
||||
/* Torrenting */
|
||||
@ -43,8 +42,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
getGameShopDetails: (objectId: string, shop: GameShop, language: string) =>
|
||||
ipcRenderer.invoke("getGameShopDetails", objectId, shop, language),
|
||||
getRandomGame: () => ipcRenderer.invoke("getRandomGame"),
|
||||
getHowLongToBeat: (title: string) =>
|
||||
ipcRenderer.invoke("getHowLongToBeat", title),
|
||||
getHowLongToBeat: (objectId: string, shop: GameShop) =>
|
||||
ipcRenderer.invoke("getHowLongToBeat", objectId, shop),
|
||||
getGames: (take?: number, skip?: number) =>
|
||||
ipcRenderer.invoke("getGames", take, skip),
|
||||
searchGameRepacks: (query: string) =>
|
||||
@ -199,7 +198,7 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
ping: () => ipcRenderer.invoke("ping"),
|
||||
getVersion: () => ipcRenderer.invoke("getVersion"),
|
||||
getDefaultDownloadsPath: () => ipcRenderer.invoke("getDefaultDownloadsPath"),
|
||||
isStaging,
|
||||
isStaging: () => ipcRenderer.invoke("isStaging"),
|
||||
isPortableVersion: () => ipcRenderer.invoke("isPortableVersion"),
|
||||
openExternal: (src: string) => ipcRenderer.invoke("openExternal", src),
|
||||
openCheckout: () => ipcRenderer.invoke("openCheckout"),
|
||||
|
@ -8,10 +8,6 @@
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self' 'unsafe-inline' *;"
|
||||
/>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="%RENDERER_VITE_EXTERNAL_RESOURCES_URL%"
|
||||
></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
@ -62,25 +62,6 @@ export function App() {
|
||||
clearUserDetails,
|
||||
} = useUserDetails();
|
||||
|
||||
useEffect(() => {
|
||||
if (userDetails) {
|
||||
const $existingScript = document.getElementById("user-details");
|
||||
|
||||
const content = `window.userDetails = ${JSON.stringify(userDetails)};`;
|
||||
|
||||
if ($existingScript) {
|
||||
$existingScript.textContent = content;
|
||||
} else {
|
||||
const $script = document.createElement("script");
|
||||
$script.id = "user-details";
|
||||
$script.type = "text/javascript";
|
||||
$script.textContent = content;
|
||||
|
||||
document.head.appendChild($script);
|
||||
}
|
||||
}
|
||||
}, [userDetails]);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const navigate = useNavigate();
|
||||
@ -133,12 +114,33 @@ export function App() {
|
||||
dispatch(setProfileBackground(profileBackground));
|
||||
}
|
||||
|
||||
fetchUserDetails().then((response) => {
|
||||
if (response) {
|
||||
updateUserDetails(response);
|
||||
syncFriendRequests();
|
||||
}
|
||||
});
|
||||
fetchUserDetails()
|
||||
.then((response) => {
|
||||
if (response) {
|
||||
updateUserDetails(response);
|
||||
syncFriendRequests();
|
||||
|
||||
const $existingScript = document.getElementById("user-details");
|
||||
|
||||
const content = `window.userDetails = ${JSON.stringify(response)};`;
|
||||
|
||||
if ($existingScript) {
|
||||
$existingScript.textContent = content;
|
||||
} else {
|
||||
const $script = document.createElement("script");
|
||||
$script.id = "user-details";
|
||||
$script.type = "text/javascript";
|
||||
$script.textContent = content;
|
||||
|
||||
document.head.appendChild($script);
|
||||
}
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
const $script = document.createElement("script");
|
||||
$script.src = import.meta.env.RENDERER_VITE_EXTERNAL_RESOURCES_URL;
|
||||
document.head.appendChild($script);
|
||||
});
|
||||
}, [fetchUserDetails, syncFriendRequests, updateUserDetails, dispatch]);
|
||||
|
||||
const onSignIn = useCallback(() => {
|
||||
|
@ -181,6 +181,7 @@ export function GameDetailsContextProvider({
|
||||
shop,
|
||||
i18n.language,
|
||||
userDetails,
|
||||
userPreferences,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
6
src/renderer/src/declaration.d.ts
vendored
6
src/renderer/src/declaration.d.ts
vendored
@ -60,7 +60,8 @@ declare global {
|
||||
) => Promise<ShopDetails | null>;
|
||||
getRandomGame: () => Promise<Steam250Game>;
|
||||
getHowLongToBeat: (
|
||||
title: string
|
||||
objectId: string,
|
||||
shop: GameShop
|
||||
) => Promise<HowLongToBeatCategory[] | null>;
|
||||
getGames: (take?: number, skip?: number) => Promise<CatalogueEntry[]>;
|
||||
searchGameRepacks: (query: string) => Promise<GameRepack[]>;
|
||||
@ -162,6 +163,7 @@ declare global {
|
||||
openExternal: (src: string) => Promise<void>;
|
||||
openCheckout: () => Promise<void>;
|
||||
getVersion: () => Promise<string>;
|
||||
isStaging: () => Promise<boolean>;
|
||||
ping: () => string;
|
||||
getDefaultDownloadsPath: () => Promise<string>;
|
||||
isPortableVersion: () => Promise<boolean>;
|
||||
@ -231,8 +233,6 @@ declare global {
|
||||
|
||||
/* Notifications */
|
||||
publishNewRepacksNotification: (newRepacksCount: number) => Promise<void>;
|
||||
|
||||
isStaging: boolean;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
|
@ -97,8 +97,10 @@ export function Sidebar() {
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
const howLongToBeat =
|
||||
await window.electron.getHowLongToBeat(gameTitle);
|
||||
const howLongToBeat = await window.electron.getHowLongToBeat(
|
||||
objectId,
|
||||
shop
|
||||
);
|
||||
|
||||
if (howLongToBeat) {
|
||||
howLongToBeatEntriesTable.add({
|
||||
|
@ -45,22 +45,25 @@ export function ProfileContent() {
|
||||
return userProfile?.relation?.status === "ACCEPTED";
|
||||
}, [userProfile]);
|
||||
|
||||
const buildUserGameDetailsPath = (game: UserGame) => {
|
||||
if (!userProfile?.hasActiveSubscription || game.achievementCount === 0) {
|
||||
return buildGameDetailsPath({
|
||||
...game,
|
||||
objectId: game.objectId,
|
||||
});
|
||||
}
|
||||
const buildUserGameDetailsPath = useCallback(
|
||||
(game: UserGame) => {
|
||||
if (!userProfile?.hasActiveSubscription || game.achievementCount === 0) {
|
||||
return buildGameDetailsPath({
|
||||
...game,
|
||||
objectId: game.objectId,
|
||||
});
|
||||
}
|
||||
|
||||
const userParams = userProfile
|
||||
? {
|
||||
userId: userProfile.id,
|
||||
}
|
||||
: undefined;
|
||||
const userParams = userProfile
|
||||
? {
|
||||
userId: userProfile.id,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return buildGameAchievementPath({ ...game }, userParams);
|
||||
};
|
||||
return buildGameAchievementPath({ ...game }, userParams);
|
||||
},
|
||||
[userProfile]
|
||||
);
|
||||
|
||||
const formatPlayTime = useCallback(
|
||||
(playTimeInSeconds = 0) => {
|
||||
@ -259,6 +262,7 @@ export function ProfileContent() {
|
||||
userStats,
|
||||
numberFormatter,
|
||||
t,
|
||||
buildUserGameDetailsPath,
|
||||
formatPlayTime,
|
||||
navigate,
|
||||
]);
|
||||
|
8
src/renderer/src/vite-env.d.ts
vendored
8
src/renderer/src/vite-env.d.ts
vendored
@ -1,2 +1,10 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite-plugin-svgr/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly RENDERER_VITE_EXTERNAL_RESOURCES_URL: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user