From 136a44473f94b2c7038aaa295750107034b72b78 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Tue, 14 Oct 2025 19:26:39 +0100 Subject: [PATCH] feat: adding o1 cache --- src/locales/en/translation.json | 1 + src/locales/pt-BR/translation.json | 8 ++ src/locales/pt-PT/translation.json | 8 ++ src/main/events/download-sources/helpers.ts | 136 ++++++++++++++---- .../download-sources/sync-download-sources.ts | 135 +++++++++++++---- .../sidebar-adding-custom-game-modal.scss | 5 +- .../profile/profile-content/sort-options.tsx | 2 +- 7 files changed, 232 insertions(+), 63 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index c91a1296..066fd8a8 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -599,6 +599,7 @@ "activity": "Recent Activity", "library": "Library", "pinned": "Pinned", + "sort_by": "Sort by:", "achievements_earned": "Achievements earned", "played_recently": "Played recently", "playtime": "Playtime", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index ef34ee6f..c578a91c 100755 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -592,10 +592,18 @@ "user_profile": { "amount_hours": "{{amount}} horas", "amount_minutes": "{{amount}} minutos", + "amount_hours_short": "{{amount}}h", + "amount_minutes_short": "{{amount}}m", "last_time_played": "Última sessão {{period}}", "activity": "Atividades recentes", "library": "Biblioteca", + "pinned": "Fixados", + "sort_by": "Ordenar por:", + "achievements_earned": "Conquistas obtidas", + "played_recently": "Jogados recentemente", + "playtime": "Tempo de jogo", "total_play_time": "Tempo total de jogo", + "manual_playtime_tooltip": "Este tempo de jogo foi atualizado manualmente", "no_recent_activity_title": "Hmmm… nada por aqui", "no_recent_activity_description": "Parece que você não jogou nada recentemente. Que tal começar agora?", "display_name": "Nome de exibição", diff --git a/src/locales/pt-PT/translation.json b/src/locales/pt-PT/translation.json index 7c244b6e..962504d4 100644 --- a/src/locales/pt-PT/translation.json +++ b/src/locales/pt-PT/translation.json @@ -377,10 +377,18 @@ "user_profile": { "amount_hours": "{{amount}} horas", "amount_minutes": "{{amount}} minutos", + "amount_hours_short": "{{amount}}h", + "amount_minutes_short": "{{amount}}m", "last_time_played": "Última sessão {{period}}", "activity": "Atividade recente", "library": "Biblioteca", + "pinned": "Fixados", + "sort_by": "Ordenar por:", + "achievements_earned": "Conquistas obtidas", + "played_recently": "Jogados recentemente", + "playtime": "Tempo de jogo", "total_play_time": "Tempo total de jogo", + "manual_playtime_tooltip": "Este tempo de jogo foi atualizado manualmente", "no_recent_activity_title": "Hmmm… não há nada por aqui", "no_recent_activity_description": "Parece que não jogaste nada recentemente. Que tal começar agora?", "display_name": "Nome de apresentação", diff --git a/src/main/events/download-sources/helpers.ts b/src/main/events/download-sources/helpers.ts index 7f346fc0..7f49499d 100644 --- a/src/main/events/download-sources/helpers.ts +++ b/src/main/events/download-sources/helpers.ts @@ -2,6 +2,7 @@ import axios from "axios"; import { z } from "zod"; import { downloadSourcesSublevel, repacksSublevel } from "@main/level"; import { DownloadSourceStatus } from "@shared"; +import crypto from "crypto"; const downloadSourceSchema = z.object({ name: z.string().max(255), @@ -15,6 +16,46 @@ const downloadSourceSchema = z.object({ ), }); +// Pre-computed title-to-Steam-ID mapping +type TitleHashMapping = Record; +let titleHashMappingCache: TitleHashMapping | null = null; +let titleHashMappingCacheTime = 0; +const TITLE_HASH_MAPPING_TTL = 86400000; // 24 hours + +const getTitleHashMapping = async (): Promise => { + const now = Date.now(); + if ( + titleHashMappingCache && + now - titleHashMappingCacheTime < TITLE_HASH_MAPPING_TTL + ) { + return titleHashMappingCache; + } + + try { + const response = await axios.get( + "https://cdn.losbroxas.org/results_a4c50f70c2.json", + { + timeout: 10000, + } + ); + + titleHashMappingCache = response.data; + titleHashMappingCacheTime = now; + console.log( + `✅ Loaded title hash mapping with ${Object.keys(response.data).length} entries` + ); + return response.data; + } catch (error) { + console.error("Failed to fetch title hash mapping:", error); + // Return empty mapping on error - will fall back to fuzzy matching + return {}; + } +}; + +const hashTitle = (title: string): string => { + return crypto.createHash("sha256").update(title).digest("hex"); +}; + type SteamGamesByLetter = Record; type FormattedSteamGame = { id: string; name: string; formattedName: string }; type FormattedSteamGamesByLetter = Record; @@ -161,57 +202,89 @@ const addNewDownloads = async ( const batch = repacksSublevel.batch(); + // Fetch the pre-computed hash mapping + const titleHashMapping = await getTitleHashMapping(); + let hashMatchCount = 0; + let fuzzyMatchCount = 0; + let noMatchCount = 0; + for (const download of downloads) { - const formattedTitle = formatRepackName(download.title); - let gamesInSteam: FormattedSteamGame[] = []; + let objectIds: string[] = []; + let usedHashMatch = false; - // Only try to match if we have a valid formatted title - if (formattedTitle && formattedTitle.length > 0) { - const [firstLetter] = formattedTitle; - const games = steamGames[firstLetter] || []; + // FIRST: Try hash-based lookup (fast and accurate) + const titleHash = hashTitle(download.title); + const steamIdsFromHash = titleHashMapping[titleHash]; - // Try exact prefix match first - gamesInSteam = games.filter((game) => - formattedTitle.startsWith(game.formattedName) - ); + if (steamIdsFromHash && steamIdsFromHash.length > 0) { + // Found in hash mapping - trust it completely + hashMatchCount++; + usedHashMatch = true; - // If no exact prefix match, try contains match (more lenient) - if (gamesInSteam.length === 0) { - gamesInSteam = games.filter( - (game) => - formattedTitle.includes(game.formattedName) || - game.formattedName.includes(formattedTitle) + // Use the Steam IDs directly as strings (trust the hash mapping) + objectIds = steamIdsFromHash.map(String); + } + + // FALLBACK: Use fuzzy matching ONLY if hash lookup found nothing + if (!usedHashMatch) { + let gamesInSteam: FormattedSteamGame[] = []; + const formattedTitle = formatRepackName(download.title); + + if (formattedTitle && formattedTitle.length > 0) { + const [firstLetter] = formattedTitle; + const games = steamGames[firstLetter] || []; + + // Try exact prefix match first + gamesInSteam = games.filter((game) => + formattedTitle.startsWith(game.formattedName) ); - } - // If still no match, try checking all letters (not just first letter) - // This helps with repacks that use abbreviations or alternate naming - if (gamesInSteam.length === 0) { - for (const letter of Object.keys(steamGames)) { - const letterGames = steamGames[letter] || []; - const matches = letterGames.filter( + // If no exact prefix match, try contains match (more lenient) + if (gamesInSteam.length === 0) { + gamesInSteam = games.filter( (game) => formattedTitle.includes(game.formattedName) || game.formattedName.includes(formattedTitle) ); - if (matches.length > 0) { - gamesInSteam = matches; - break; + } + + // If still no match, try checking all letters (not just first letter) + if (gamesInSteam.length === 0) { + for (const letter of Object.keys(steamGames)) { + const letterGames = steamGames[letter] || []; + const matches = letterGames.filter( + (game) => + formattedTitle.includes(game.formattedName) || + game.formattedName.includes(formattedTitle) + ); + if (matches.length > 0) { + gamesInSteam = matches; + break; + } } } + + if (gamesInSteam.length > 0) { + fuzzyMatchCount++; + objectIds = gamesInSteam.map((game) => String(game.id)); + } else { + noMatchCount++; + } + } else { + noMatchCount++; } } // Add matched game IDs to source tracking - for (const game of gamesInSteam) { - objectIdsOnSource.add(String(game.id)); + for (const id of objectIds) { + objectIdsOnSource.add(id); } // Create the repack even if no games matched // This ensures all repacks from sources are imported const repack = { id: nextRepackId++, - objectIds: gamesInSteam.map((game) => String(game.id)), + objectIds: objectIds, title: download.title, uris: download.uris, fileSize: download.fileSize, @@ -227,6 +300,11 @@ const addNewDownloads = async ( await batch.write(); + // Log matching statistics + console.log( + `📊 Matching stats for ${downloadSource.name}: Hash=${hashMatchCount}, Fuzzy=${fuzzyMatchCount}, None=${noMatchCount}` + ); + const existingSource = await downloadSourcesSublevel.get( `${downloadSource.id}` ); diff --git a/src/main/events/download-sources/sync-download-sources.ts b/src/main/events/download-sources/sync-download-sources.ts index b567ca63..75a50459 100644 --- a/src/main/events/download-sources/sync-download-sources.ts +++ b/src/main/events/download-sources/sync-download-sources.ts @@ -4,6 +4,7 @@ import { z } from "zod"; import { downloadSourcesSublevel, repacksSublevel } from "@main/level"; import { DownloadSourceStatus } from "@shared"; import { invalidateDownloadSourcesCache, invalidateIdCaches } from "./helpers"; +import crypto from "crypto"; const downloadSourceSchema = z.object({ name: z.string().max(255), @@ -17,6 +18,45 @@ const downloadSourceSchema = z.object({ ), }); +// Pre-computed title-to-Steam-ID mapping (shared with helpers.ts) +type TitleHashMapping = Record; +let titleHashMappingCache: TitleHashMapping | null = null; +let titleHashMappingCacheTime = 0; +const TITLE_HASH_MAPPING_TTL = 86400000; // 24 hours + +const getTitleHashMapping = async (): Promise => { + const now = Date.now(); + if ( + titleHashMappingCache && + now - titleHashMappingCacheTime < TITLE_HASH_MAPPING_TTL + ) { + return titleHashMappingCache; + } + + try { + const response = await axios.get( + "https://cdn.losbroxas.org/results_a4c50f70c2.json", + { + timeout: 10000, + } + ); + + titleHashMappingCache = response.data; + titleHashMappingCacheTime = now; + console.log( + `✅ Loaded title hash mapping with ${Object.keys(response.data).length} entries` + ); + return response.data; + } catch (error) { + console.error("Failed to fetch title hash mapping:", error); + return {}; + } +}; + +const hashTitle = (title: string): string => { + return crypto.createHash("sha256").update(title).digest("hex"); +}; + type SteamGamesByLetter = Record; type FormattedSteamGame = { id: string; name: string; formattedName: string }; type FormattedSteamGamesByLetter = Record; @@ -109,57 +149,89 @@ const addNewDownloads = async ( const batch = repacksSublevel.batch(); + // Fetch the pre-computed hash mapping + const titleHashMapping = await getTitleHashMapping(); + let hashMatchCount = 0; + let fuzzyMatchCount = 0; + let noMatchCount = 0; + for (const download of downloads) { - const formattedTitle = formatRepackName(download.title); - let gamesInSteam: FormattedSteamGame[] = []; + let objectIds: string[] = []; + let usedHashMatch = false; - // Only try to match if we have a valid formatted title - if (formattedTitle && formattedTitle.length > 0) { - const [firstLetter] = formattedTitle; - const games = steamGames[firstLetter] || []; + // FIRST: Try hash-based lookup (fast and accurate) + const titleHash = hashTitle(download.title); + const steamIdsFromHash = titleHashMapping[titleHash]; - // Try exact prefix match first - gamesInSteam = games.filter((game) => - formattedTitle.startsWith(game.formattedName) - ); + if (steamIdsFromHash && steamIdsFromHash.length > 0) { + // Found in hash mapping - trust it completely + hashMatchCount++; + usedHashMatch = true; - // If no exact prefix match, try contains match (more lenient) - if (gamesInSteam.length === 0) { - gamesInSteam = games.filter( - (game) => - formattedTitle.includes(game.formattedName) || - game.formattedName.includes(formattedTitle) + // Use the Steam IDs directly as strings (trust the hash mapping) + objectIds = steamIdsFromHash.map(String); + } + + // FALLBACK: Use fuzzy matching ONLY if hash lookup found nothing + if (!usedHashMatch) { + let gamesInSteam: FormattedSteamGame[] = []; + const formattedTitle = formatRepackName(download.title); + + if (formattedTitle && formattedTitle.length > 0) { + const [firstLetter] = formattedTitle; + const games = steamGames[firstLetter] || []; + + // Try exact prefix match first + gamesInSteam = games.filter((game) => + formattedTitle.startsWith(game.formattedName) ); - } - // If still no match, try checking all letters (not just first letter) - // This helps with repacks that use abbreviations or alternate naming - if (gamesInSteam.length === 0) { - for (const letter of Object.keys(steamGames)) { - const letterGames = steamGames[letter] || []; - const matches = letterGames.filter( + // If no exact prefix match, try contains match (more lenient) + if (gamesInSteam.length === 0) { + gamesInSteam = games.filter( (game) => formattedTitle.includes(game.formattedName) || game.formattedName.includes(formattedTitle) ); - if (matches.length > 0) { - gamesInSteam = matches; - break; + } + + // If still no match, try checking all letters (not just first letter) + if (gamesInSteam.length === 0) { + for (const letter of Object.keys(steamGames)) { + const letterGames = steamGames[letter] || []; + const matches = letterGames.filter( + (game) => + formattedTitle.includes(game.formattedName) || + game.formattedName.includes(formattedTitle) + ); + if (matches.length > 0) { + gamesInSteam = matches; + break; + } } } + + if (gamesInSteam.length > 0) { + fuzzyMatchCount++; + objectIds = gamesInSteam.map((game) => String(game.id)); + } else { + noMatchCount++; + } + } else { + noMatchCount++; } } // Add matched game IDs to source tracking - for (const game of gamesInSteam) { - objectIdsOnSource.add(String(game.id)); + for (const id of objectIds) { + objectIdsOnSource.add(id); } // Create the repack even if no games matched // This ensures all repacks from sources are imported const repack = { id: nextRepackId++, - objectIds: gamesInSteam.map((game) => String(game.id)), + objectIds: objectIds, title: download.title, uris: download.uris, fileSize: download.fileSize, @@ -175,6 +247,11 @@ const addNewDownloads = async ( await batch.write(); + // Log matching statistics + console.log( + `📊 Matching stats for ${downloadSource.name}: Hash=${hashMatchCount}, Fuzzy=${fuzzyMatchCount}, None=${noMatchCount}` + ); + const existingSource = await downloadSourcesSublevel.get( `${downloadSource.id}` ); diff --git a/src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.scss b/src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.scss index 942384fe..1a8ca315 100644 --- a/src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.scss +++ b/src/renderer/src/components/sidebar/sidebar-adding-custom-game-modal.scss @@ -5,10 +5,7 @@ display: flex; flex-direction: column; gap: calc(globals.$spacing-unit * 3); - width: 100%; - max-width: 500px; - margin: 0 auto; - text-align: center; + min-width: 500px; } &__form { diff --git a/src/renderer/src/pages/profile/profile-content/sort-options.tsx b/src/renderer/src/pages/profile/profile-content/sort-options.tsx index 53da8e40..607e54b9 100644 --- a/src/renderer/src/pages/profile/profile-content/sort-options.tsx +++ b/src/renderer/src/pages/profile/profile-content/sort-options.tsx @@ -14,7 +14,7 @@ export function SortOptions({ sortBy, onSortChange }: SortOptionsProps) { return (
- Sort by: + {t("sort_by")}