diff --git a/src/main/events/download-sources/delete-all-download-sources.ts b/src/main/events/download-sources/delete-all-download-sources.ts index ec333e20..cdb781b7 100644 --- a/src/main/events/download-sources/delete-all-download-sources.ts +++ b/src/main/events/download-sources/delete-all-download-sources.ts @@ -1,10 +1,15 @@ import { registerEvent } from "../register-event"; import { downloadSourcesSublevel, repacksSublevel } from "@main/level"; +import { invalidateDownloadSourcesCache, invalidateIdCaches } from "./helpers"; const deleteAllDownloadSources = async ( _event: Electron.IpcMainInvokeEvent ) => { await Promise.all([repacksSublevel.clear(), downloadSourcesSublevel.clear()]); + + // Invalidate caches after clearing all sources + invalidateDownloadSourcesCache(); + invalidateIdCaches(); }; registerEvent("deleteAllDownloadSources", deleteAllDownloadSources); diff --git a/src/main/events/download-sources/delete-download-source.ts b/src/main/events/download-sources/delete-download-source.ts index d6fc4fda..72de8746 100644 --- a/src/main/events/download-sources/delete-download-source.ts +++ b/src/main/events/download-sources/delete-download-source.ts @@ -1,5 +1,6 @@ import { registerEvent } from "../register-event"; import { downloadSourcesSublevel, repacksSublevel } from "@main/level"; +import { invalidateDownloadSourcesCache, invalidateIdCaches } from "./helpers"; const deleteDownloadSource = async ( _event: Electron.IpcMainInvokeEvent, @@ -20,6 +21,10 @@ const deleteDownloadSource = async ( await batch.write(); await downloadSourcesSublevel.del(`${id}`); + + // Invalidate caches after deletion + invalidateDownloadSourcesCache(); + invalidateIdCaches(); }; registerEvent("deleteDownloadSource", deleteDownloadSource); diff --git a/src/main/events/download-sources/helpers.ts b/src/main/events/download-sources/helpers.ts index b0c6841d..95fdebe3 100644 --- a/src/main/events/download-sources/helpers.ts +++ b/src/main/events/download-sources/helpers.ts @@ -163,19 +163,52 @@ const addNewDownloads = async ( for (const download of downloads) { const formattedTitle = formatRepackName(download.title); - const [firstLetter] = formattedTitle; - const games = steamGames[firstLetter] || []; + let gamesInSteam: FormattedSteamGame[] = []; - const gamesInSteam = games.filter((game) => - formattedTitle.startsWith(game.formattedName) - ); + // Only try to match if we have a valid formatted title + if (formattedTitle && formattedTitle.length > 0) { + const [firstLetter] = formattedTitle; + const games = steamGames[firstLetter] || []; - if (gamesInSteam.length === 0) continue; + // Try exact prefix match first + gamesInSteam = games.filter((game) => + formattedTitle.startsWith(game.formattedName) + ); + // 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 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( + (game) => + formattedTitle.includes(game.formattedName) || + game.formattedName.includes(formattedTitle) + ); + if (matches.length > 0) { + gamesInSteam = matches; + break; + } + } + } + } + + // Add matched game IDs to source tracking for (const game of gamesInSteam) { objectIdsOnSource.add(String(game.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)), @@ -254,3 +287,26 @@ export const importDownloadSourceToLocal = async ( objectIds, }; }; + +export const updateDownloadSourcePreservingTimestamp = async ( + existingSource: DownloadSource, + url: string +) => { + const response = await axios.get>(url); + + const updatedSource = { + ...existingSource, + name: response.data.name, + etag: response.headers["etag"] || null, + status: DownloadSourceStatus.UpToDate, + downloadCount: response.data.downloads.length, + updatedAt: new Date(), + // Preserve the original createdAt timestamp + }; + + await downloadSourcesSublevel.put(`${existingSource.id}`, updatedSource); + + invalidateDownloadSourcesCache(); + + return updatedSource; +}; diff --git a/src/main/events/download-sources/sync-download-sources.ts b/src/main/events/download-sources/sync-download-sources.ts index 19ab8079..4e3837ba 100644 --- a/src/main/events/download-sources/sync-download-sources.ts +++ b/src/main/events/download-sources/sync-download-sources.ts @@ -3,11 +3,7 @@ import axios, { AxiosError } from "axios"; import { z } from "zod"; import { downloadSourcesSublevel, repacksSublevel } from "@main/level"; import { DownloadSourceStatus } from "@shared"; -import { - checkUrlExists, - invalidateDownloadSourcesCache, - invalidateIdCaches, -} from "./helpers"; +import { invalidateDownloadSourcesCache } from "./helpers"; const downloadSourceSchema = z.object({ name: z.string().max(255), @@ -157,60 +153,6 @@ const addNewDownloads = async ( } }; -const deleteDownloadSource = async (id: number) => { - const repacksToDelete: string[] = []; - - for await (const [key, repack] of repacksSublevel.iterator()) { - if (repack.downloadSourceId === id) { - repacksToDelete.push(key); - } - } - - const batch = repacksSublevel.batch(); - for (const key of repacksToDelete) { - batch.del(key); - } - await batch.write(); - - await downloadSourcesSublevel.del(`${id}`); - - invalidateDownloadSourcesCache(); - invalidateIdCaches(); -}; - -const importDownloadSource = async (url: string) => { - const urlExists = await checkUrlExists(url); - if (urlExists) { - return; - } - - const response = await axios.get>(url); - - const steamGames = await getSteamGames(); - - const now = new Date(); - - const nextId = await getNextId(downloadSourcesSublevel); - - const downloadSource = { - id: nextId, - url, - name: response.data.name, - etag: response.headers["etag"] || null, - status: DownloadSourceStatus.UpToDate, - downloadCount: response.data.downloads.length, - objectIds: [], - createdAt: now, - updatedAt: now, - }; - - await downloadSourcesSublevel.put(`${downloadSource.id}`, downloadSource); - - invalidateDownloadSourcesCache(); - - await addNewDownloads(downloadSource, response.data.downloads, steamGames); -}; - const syncDownloadSources = async ( _event: Electron.IpcMainInvokeEvent ): Promise => { @@ -249,57 +191,66 @@ const syncDownloadSources = async ( existingRepacks.push(repack); } - if (downloadSources.some((source) => !source.fingerprint)) { - await Promise.all( - downloadSources.map(async (source) => { - await deleteDownloadSource(source.id); - await importDownloadSource(source.url); - }) - ); - } else { - for (const downloadSource of downloadSources) { - const headers: Record = {}; + // Handle sources with missing fingerprints individually, don't delete all sources + const sourcesWithFingerprints = downloadSources.filter( + (source) => source.fingerprint + ); + const sourcesWithoutFingerprints = downloadSources.filter( + (source) => !source.fingerprint + ); - if (downloadSource.etag) { - headers["If-None-Match"] = downloadSource.etag; - } + // For sources without fingerprints, just continue with normal sync + // They will get fingerprints updated later by updateMissingFingerprints + const allSourcesToSync = [ + ...sourcesWithFingerprints, + ...sourcesWithoutFingerprints, + ]; - try { - const response = await axios.get(downloadSource.url, { - headers, - }); + for (const downloadSource of allSourcesToSync) { + const headers: Record = {}; - const source = downloadSourceSchema.parse(response.data); - const steamGames = await getSteamGames(); + if (downloadSource.etag) { + headers["If-None-Match"] = downloadSource.etag; + } - const repacks = source.downloads.filter( - (download) => - !existingRepacks.some((repack) => repack.title === download.title) - ); + try { + const response = await axios.get(downloadSource.url, { + headers, + }); - await downloadSourcesSublevel.put(`${downloadSource.id}`, { - ...downloadSource, - etag: response.headers["etag"] || null, - downloadCount: source.downloads.length, - status: DownloadSourceStatus.UpToDate, - }); + const source = downloadSourceSchema.parse(response.data); + const steamGames = await getSteamGames(); - await addNewDownloads(downloadSource, repacks, steamGames); + const repacks = source.downloads.filter( + (download) => + !existingRepacks.some((repack) => repack.title === download.title) + ); - newRepacksCount += repacks.length; - } catch (err: unknown) { - const isNotModified = (err as AxiosError).response?.status === 304; + await downloadSourcesSublevel.put(`${downloadSource.id}`, { + ...downloadSource, + etag: response.headers["etag"] || null, + downloadCount: source.downloads.length, + status: DownloadSourceStatus.UpToDate, + }); - await downloadSourcesSublevel.put(`${downloadSource.id}`, { - ...downloadSource, - status: isNotModified - ? DownloadSourceStatus.UpToDate - : DownloadSourceStatus.Errored, - }); - } + await addNewDownloads(downloadSource, repacks, steamGames); + + newRepacksCount += repacks.length; + } catch (err: unknown) { + const isNotModified = (err as AxiosError).response?.status === 304; + + await downloadSourcesSublevel.put(`${downloadSource.id}`, { + ...downloadSource, + status: isNotModified + ? DownloadSourceStatus.UpToDate + : DownloadSourceStatus.Errored, + }); } } + // Invalidate cache after all sync operations complete + invalidateDownloadSourcesCache(); + return newRepacksCount; } catch (err) { return -1;