From 0fc6d69851db91e0c08b04413923a7a9a63b8421 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Mon, 2 Dec 2024 17:58:13 +0000 Subject: [PATCH] fix: migrating hltb to api --- .github/workflows/build.yml | 2 + .github/workflows/release.yml | 2 + .../events/catalogue/get-how-long-to-beat.ts | 26 +++-- src/main/events/index.ts | 3 +- src/main/services/how-long-to-beat.ts | 108 ------------------ src/main/services/index.ts | 1 - src/preload/index.ts | 7 +- src/renderer/index.html | 4 - src/renderer/src/app.tsx | 52 +++++---- .../game-details/game-details.context.tsx | 1 + src/renderer/src/declaration.d.ts | 6 +- .../pages/game-details/sidebar/sidebar.tsx | 6 +- .../profile-content/profile-content.tsx | 32 +++--- src/renderer/src/vite-env.d.ts | 8 ++ 14 files changed, 85 insertions(+), 173 deletions(-) delete mode 100644 src/main/services/how-long-to-beat.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0f650fc5..0811967b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ff97c937..b91cc743 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/src/main/events/catalogue/get-how-long-to-beat.ts b/src/main/events/catalogue/get-how-long-to-beat.ts index 01966afc..fab5677f 100644 --- a/src/main/events/catalogue/get-how-long-to-beat.ts +++ b/src/main/events/catalogue/get-how-long-to-beat.ts @@ -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 => { - 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); diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 932b80e4..eff62531 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -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); diff --git a/src/main/services/how-long-to-beat.ts b/src/main/services/how-long-to-beat.ts deleted file mode 100644 index 1e5f3279..00000000 --- a/src/main/services/how-long-to-beat.ts +++ /dev/null @@ -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 => { - 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); -}; diff --git a/src/main/services/index.ts b/src/main/services/index.ts index 27abf579..498159c9 100644 --- a/src/main/services/index.ts +++ b/src/main/services/index.ts @@ -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"; diff --git a/src/preload/index.ts b/src/preload/index.ts index 17f4b3e9..3f2f677a 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -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"), diff --git a/src/renderer/index.html b/src/renderer/index.html index c3ce2e83..6290dfa4 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -8,10 +8,6 @@ http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' *;" /> -
diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index e40d2d27..2406baab 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -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(() => { diff --git a/src/renderer/src/context/game-details/game-details.context.tsx b/src/renderer/src/context/game-details/game-details.context.tsx index 398d6c27..70da4524 100644 --- a/src/renderer/src/context/game-details/game-details.context.tsx +++ b/src/renderer/src/context/game-details/game-details.context.tsx @@ -181,6 +181,7 @@ export function GameDetailsContextProvider({ shop, i18n.language, userDetails, + userPreferences, ]); useEffect(() => { diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index ee04db0b..2ffcfab7 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -60,7 +60,8 @@ declare global { ) => Promise; getRandomGame: () => Promise; getHowLongToBeat: ( - title: string + objectId: string, + shop: GameShop ) => Promise; getGames: (take?: number, skip?: number) => Promise; searchGameRepacks: (query: string) => Promise; @@ -162,6 +163,7 @@ declare global { openExternal: (src: string) => Promise; openCheckout: () => Promise; getVersion: () => Promise; + isStaging: () => Promise; ping: () => string; getDefaultDownloadsPath: () => Promise; isPortableVersion: () => Promise; @@ -231,8 +233,6 @@ declare global { /* Notifications */ publishNewRepacksNotification: (newRepacksCount: number) => Promise; - - isStaging: boolean; } interface Window { diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx index c4ad640b..86f24f5a 100644 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx @@ -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({ diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx index d6b55490..26975647 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -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, ]); diff --git a/src/renderer/src/vite-env.d.ts b/src/renderer/src/vite-env.d.ts index b1f45c78..41b3f064 100644 --- a/src/renderer/src/vite-env.d.ts +++ b/src/renderer/src/vite-env.d.ts @@ -1,2 +1,10 @@ /// /// + +interface ImportMetaEnv { + readonly RENDERER_VITE_EXTERNAL_RESOURCES_URL: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +}