mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-31 06:41:03 +00:00
feat: improving download source import code
This commit is contained in:
@@ -16,6 +16,8 @@ const downloadSourceSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
type SteamGamesByLetter = Record<string, { id: string; name: string }[]>;
|
type SteamGamesByLetter = Record<string, { id: string; name: string }[]>;
|
||||||
|
type FormattedSteamGame = { id: string; name: string; formattedName: string };
|
||||||
|
type FormattedSteamGamesByLetter = Record<string, FormattedSteamGame[]>;
|
||||||
|
|
||||||
const formatName = (name: string) => {
|
const formatName = (name: string) => {
|
||||||
return name
|
return name
|
||||||
@@ -29,8 +31,48 @@ const formatRepackName = (name: string) => {
|
|||||||
return formatName(name.replace("[DL]", ""));
|
return formatName(name.replace("[DL]", ""));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface DownloadSource {
|
||||||
|
id: number;
|
||||||
|
url: string;
|
||||||
|
name: string;
|
||||||
|
etag: string | null;
|
||||||
|
status: number;
|
||||||
|
downloadCount: number;
|
||||||
|
objectIds: string[];
|
||||||
|
fingerprint?: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
let downloadSourcesCache: Map<string, DownloadSource> | null = null;
|
||||||
|
let downloadSourcesCacheTime = 0;
|
||||||
|
const CACHE_TTL = 5000;
|
||||||
|
|
||||||
|
const getDownloadSourcesMap = async (): Promise<
|
||||||
|
Map<string, DownloadSource>
|
||||||
|
> => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (downloadSourcesCache && now - downloadSourcesCacheTime < CACHE_TTL) {
|
||||||
|
return downloadSourcesCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
const map = new Map();
|
||||||
|
for await (const [key, source] of downloadSourcesSublevel.iterator()) {
|
||||||
|
map.set(key, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadSourcesCache = map;
|
||||||
|
downloadSourcesCacheTime = now;
|
||||||
|
return map;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const invalidateDownloadSourcesCache = () => {
|
||||||
|
downloadSourcesCache = null;
|
||||||
|
};
|
||||||
|
|
||||||
export const checkUrlExists = async (url: string): Promise<boolean> => {
|
export const checkUrlExists = async (url: string): Promise<boolean> => {
|
||||||
for await (const [, source] of downloadSourcesSublevel.iterator()) {
|
const sources = await getDownloadSourcesMap();
|
||||||
|
for (const source of sources.values()) {
|
||||||
if (source.url === url) {
|
if (source.url === url) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -38,12 +80,31 @@ export const checkUrlExists = async (url: string): Promise<boolean> => {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSteamGames = async () => {
|
let steamGamesCache: FormattedSteamGamesByLetter | null = null;
|
||||||
|
let steamGamesCacheTime = 0;
|
||||||
|
const STEAM_GAMES_CACHE_TTL = 300000;
|
||||||
|
|
||||||
|
const getSteamGames = async (): Promise<FormattedSteamGamesByLetter> => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (steamGamesCache && now - steamGamesCacheTime < STEAM_GAMES_CACHE_TTL) {
|
||||||
|
return steamGamesCache;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await axios.get<SteamGamesByLetter>(
|
const response = await axios.get<SteamGamesByLetter>(
|
||||||
`${import.meta.env.MAIN_VITE_EXTERNAL_RESOURCES_URL}/steam-games-by-letter.json`
|
`${import.meta.env.MAIN_VITE_EXTERNAL_RESOURCES_URL}/steam-games-by-letter.json`
|
||||||
);
|
);
|
||||||
|
|
||||||
return response.data;
|
const formattedData: FormattedSteamGamesByLetter = {};
|
||||||
|
for (const [letter, games] of Object.entries(response.data)) {
|
||||||
|
formattedData[letter] = games.map((game) => ({
|
||||||
|
...game,
|
||||||
|
formattedName: formatName(game.name),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
steamGamesCache = formattedData;
|
||||||
|
steamGamesCacheTime = now;
|
||||||
|
return formattedData;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SublevelIterator = AsyncIterable<[string, { id: number }]>;
|
type SublevelIterator = AsyncIterable<[string, { id: number }]>;
|
||||||
@@ -52,33 +113,61 @@ interface SublevelWithId {
|
|||||||
iterator: () => SublevelIterator;
|
iterator: () => SublevelIterator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let maxRepackId: number | null = null;
|
||||||
|
let maxDownloadSourceId: number | null = null;
|
||||||
|
|
||||||
const getNextId = async (sublevel: SublevelWithId): Promise<number> => {
|
const getNextId = async (sublevel: SublevelWithId): Promise<number> => {
|
||||||
|
const isRepackSublevel = sublevel === repacksSublevel;
|
||||||
|
const isDownloadSourceSublevel = sublevel === downloadSourcesSublevel;
|
||||||
|
|
||||||
|
if (isRepackSublevel && maxRepackId !== null) {
|
||||||
|
return ++maxRepackId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDownloadSourceSublevel && maxDownloadSourceId !== null) {
|
||||||
|
return ++maxDownloadSourceId;
|
||||||
|
}
|
||||||
|
|
||||||
let maxId = 0;
|
let maxId = 0;
|
||||||
for await (const [, value] of sublevel.iterator()) {
|
for await (const [, value] of sublevel.iterator()) {
|
||||||
if (value.id > maxId) {
|
if (value.id > maxId) {
|
||||||
maxId = value.id;
|
maxId = value.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isRepackSublevel) {
|
||||||
|
maxRepackId = maxId;
|
||||||
|
} else if (isDownloadSourceSublevel) {
|
||||||
|
maxDownloadSourceId = maxId;
|
||||||
|
}
|
||||||
|
|
||||||
return maxId + 1;
|
return maxId + 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const invalidateIdCaches = () => {
|
||||||
|
maxRepackId = null;
|
||||||
|
maxDownloadSourceId = null;
|
||||||
|
};
|
||||||
|
|
||||||
const addNewDownloads = async (
|
const addNewDownloads = async (
|
||||||
downloadSource: { id: number; name: string },
|
downloadSource: { id: number; name: string },
|
||||||
downloads: z.infer<typeof downloadSourceSchema>["downloads"],
|
downloads: z.infer<typeof downloadSourceSchema>["downloads"],
|
||||||
steamGames: SteamGamesByLetter
|
steamGames: FormattedSteamGamesByLetter
|
||||||
) => {
|
) => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const objectIdsOnSource = new Set<string>();
|
const objectIdsOnSource = new Set<string>();
|
||||||
|
|
||||||
let nextRepackId = await getNextId(repacksSublevel);
|
let nextRepackId = await getNextId(repacksSublevel);
|
||||||
|
|
||||||
|
const batch = repacksSublevel.batch();
|
||||||
|
|
||||||
for (const download of downloads) {
|
for (const download of downloads) {
|
||||||
const formattedTitle = formatRepackName(download.title);
|
const formattedTitle = formatRepackName(download.title);
|
||||||
const [firstLetter] = formattedTitle;
|
const [firstLetter] = formattedTitle;
|
||||||
const games = steamGames[firstLetter] || [];
|
const games = steamGames[firstLetter] || [];
|
||||||
|
|
||||||
const gamesInSteam = games.filter((game) =>
|
const gamesInSteam = games.filter((game) =>
|
||||||
formattedTitle.startsWith(formatName(game.name))
|
formattedTitle.startsWith(game.formattedName)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (gamesInSteam.length === 0) continue;
|
if (gamesInSteam.length === 0) continue;
|
||||||
@@ -100,9 +189,11 @@ const addNewDownloads = async (
|
|||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
};
|
};
|
||||||
|
|
||||||
await repacksSublevel.put(`${repack.id}`, repack);
|
batch.put(`${repack.id}`, repack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await batch.write();
|
||||||
|
|
||||||
const existingSource = await downloadSourcesSublevel.get(
|
const existingSource = await downloadSourcesSublevel.get(
|
||||||
`${downloadSource.id}`
|
`${downloadSource.id}`
|
||||||
);
|
);
|
||||||
@@ -134,14 +225,6 @@ export const importDownloadSourceToLocal = async (
|
|||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
const urlExistsBeforeInsert = await checkUrlExists(url);
|
|
||||||
if (urlExistsBeforeInsert) {
|
|
||||||
if (throwOnDuplicate) {
|
|
||||||
throw new Error("Download source with this URL already exists");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextId = await getNextId(downloadSourcesSublevel);
|
const nextId = await getNextId(downloadSourcesSublevel);
|
||||||
|
|
||||||
const downloadSource = {
|
const downloadSource = {
|
||||||
@@ -158,6 +241,8 @@ export const importDownloadSourceToLocal = async (
|
|||||||
|
|
||||||
await downloadSourcesSublevel.put(`${downloadSource.id}`, downloadSource);
|
await downloadSourcesSublevel.put(`${downloadSource.id}`, downloadSource);
|
||||||
|
|
||||||
|
invalidateDownloadSourcesCache();
|
||||||
|
|
||||||
const objectIds = await addNewDownloads(
|
const objectIds = await addNewDownloads(
|
||||||
downloadSource,
|
downloadSource,
|
||||||
response.data.downloads,
|
response.data.downloads,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { HydraApi } from "@main/services";
|
import { HydraApi } from "@main/services";
|
||||||
import { downloadSourcesSublevel } from "@main/level";
|
import { importDownloadSourceToLocal, checkUrlExists } from "./helpers";
|
||||||
import { importDownloadSourceToLocal } from "./helpers";
|
|
||||||
|
|
||||||
export const syncDownloadSourcesFromApi = async () => {
|
export const syncDownloadSourcesFromApi = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -8,15 +7,9 @@ export const syncDownloadSourcesFromApi = async () => {
|
|||||||
{ url: string; createdAt: string; updatedAt: string }[]
|
{ url: string; createdAt: string; updatedAt: string }[]
|
||||||
>("/profile/download-sources");
|
>("/profile/download-sources");
|
||||||
|
|
||||||
const localSources: { url: string }[] = [];
|
|
||||||
for await (const [, source] of downloadSourcesSublevel.iterator()) {
|
|
||||||
localSources.push(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
const localUrls = new Set(localSources.map((s) => s.url));
|
|
||||||
|
|
||||||
for (const apiSource of apiSources) {
|
for (const apiSource of apiSources) {
|
||||||
if (!localUrls.has(apiSource.url)) {
|
const exists = await checkUrlExists(apiSource.url);
|
||||||
|
if (!exists) {
|
||||||
await importDownloadSourceToLocal(apiSource.url, false);
|
await importDownloadSourceToLocal(apiSource.url, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ import axios, { AxiosError } from "axios";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { downloadSourcesSublevel, repacksSublevel } from "@main/level";
|
import { downloadSourcesSublevel, repacksSublevel } from "@main/level";
|
||||||
import { DownloadSourceStatus } from "@shared";
|
import { DownloadSourceStatus } from "@shared";
|
||||||
|
import {
|
||||||
|
checkUrlExists,
|
||||||
|
invalidateDownloadSourcesCache,
|
||||||
|
invalidateIdCaches,
|
||||||
|
} from "./helpers";
|
||||||
|
|
||||||
const downloadSourceSchema = z.object({
|
const downloadSourceSchema = z.object({
|
||||||
name: z.string().max(255),
|
name: z.string().max(255),
|
||||||
@@ -17,6 +22,8 @@ const downloadSourceSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
type SteamGamesByLetter = Record<string, { id: string; name: string }[]>;
|
type SteamGamesByLetter = Record<string, { id: string; name: string }[]>;
|
||||||
|
type FormattedSteamGame = { id: string; name: string; formattedName: string };
|
||||||
|
type FormattedSteamGamesByLetter = Record<string, FormattedSteamGame[]>;
|
||||||
|
|
||||||
const formatName = (name: string) => {
|
const formatName = (name: string) => {
|
||||||
return name
|
return name
|
||||||
@@ -30,21 +37,31 @@ const formatRepackName = (name: string) => {
|
|||||||
return formatName(name.replace("[DL]", ""));
|
return formatName(name.replace("[DL]", ""));
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkUrlExists = async (url: string): Promise<boolean> => {
|
let steamGamesCache: FormattedSteamGamesByLetter | null = null;
|
||||||
for await (const [, source] of downloadSourcesSublevel.iterator()) {
|
let steamGamesCacheTime = 0;
|
||||||
if (source.url === url) {
|
const STEAM_GAMES_CACHE_TTL = 300000;
|
||||||
return true;
|
|
||||||
}
|
const getSteamGames = async (): Promise<FormattedSteamGamesByLetter> => {
|
||||||
}
|
const now = Date.now();
|
||||||
return false;
|
if (steamGamesCache && now - steamGamesCacheTime < STEAM_GAMES_CACHE_TTL) {
|
||||||
};
|
return steamGamesCache;
|
||||||
|
}
|
||||||
|
|
||||||
const getSteamGames = async () => {
|
|
||||||
const response = await axios.get<SteamGamesByLetter>(
|
const response = await axios.get<SteamGamesByLetter>(
|
||||||
`${import.meta.env.MAIN_VITE_EXTERNAL_RESOURCES_URL}/steam-games-by-letter.json`
|
`${import.meta.env.MAIN_VITE_EXTERNAL_RESOURCES_URL}/steam-games-by-letter.json`
|
||||||
);
|
);
|
||||||
|
|
||||||
return response.data;
|
const formattedData: FormattedSteamGamesByLetter = {};
|
||||||
|
for (const [letter, games] of Object.entries(response.data)) {
|
||||||
|
formattedData[letter] = games.map((game) => ({
|
||||||
|
...game,
|
||||||
|
formattedName: formatName(game.name),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
steamGamesCache = formattedData;
|
||||||
|
steamGamesCacheTime = now;
|
||||||
|
return formattedData;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SublevelIterator = AsyncIterable<[string, { id: number }]>;
|
type SublevelIterator = AsyncIterable<[string, { id: number }]>;
|
||||||
@@ -53,33 +70,56 @@ interface SublevelWithId {
|
|||||||
iterator: () => SublevelIterator;
|
iterator: () => SublevelIterator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let maxRepackId: number | null = null;
|
||||||
|
let maxDownloadSourceId: number | null = null;
|
||||||
|
|
||||||
const getNextId = async (sublevel: SublevelWithId): Promise<number> => {
|
const getNextId = async (sublevel: SublevelWithId): Promise<number> => {
|
||||||
|
const isRepackSublevel = sublevel === repacksSublevel;
|
||||||
|
const isDownloadSourceSublevel = sublevel === downloadSourcesSublevel;
|
||||||
|
|
||||||
|
if (isRepackSublevel && maxRepackId !== null) {
|
||||||
|
return ++maxRepackId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDownloadSourceSublevel && maxDownloadSourceId !== null) {
|
||||||
|
return ++maxDownloadSourceId;
|
||||||
|
}
|
||||||
|
|
||||||
let maxId = 0;
|
let maxId = 0;
|
||||||
for await (const [, value] of sublevel.iterator()) {
|
for await (const [, value] of sublevel.iterator()) {
|
||||||
if (value.id > maxId) {
|
if (value.id > maxId) {
|
||||||
maxId = value.id;
|
maxId = value.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isRepackSublevel) {
|
||||||
|
maxRepackId = maxId;
|
||||||
|
} else if (isDownloadSourceSublevel) {
|
||||||
|
maxDownloadSourceId = maxId;
|
||||||
|
}
|
||||||
|
|
||||||
return maxId + 1;
|
return maxId + 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const addNewDownloads = async (
|
const addNewDownloads = async (
|
||||||
downloadSource: { id: number; name: string },
|
downloadSource: { id: number; name: string },
|
||||||
downloads: z.infer<typeof downloadSourceSchema>["downloads"],
|
downloads: z.infer<typeof downloadSourceSchema>["downloads"],
|
||||||
steamGames: SteamGamesByLetter
|
steamGames: FormattedSteamGamesByLetter
|
||||||
) => {
|
) => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const objectIdsOnSource = new Set<string>();
|
const objectIdsOnSource = new Set<string>();
|
||||||
|
|
||||||
let nextRepackId = await getNextId(repacksSublevel);
|
let nextRepackId = await getNextId(repacksSublevel);
|
||||||
|
|
||||||
|
const batch = repacksSublevel.batch();
|
||||||
|
|
||||||
for (const download of downloads) {
|
for (const download of downloads) {
|
||||||
const formattedTitle = formatRepackName(download.title);
|
const formattedTitle = formatRepackName(download.title);
|
||||||
const [firstLetter] = formattedTitle;
|
const [firstLetter] = formattedTitle;
|
||||||
const games = steamGames[firstLetter] || [];
|
const games = steamGames[firstLetter] || [];
|
||||||
|
|
||||||
const gamesInSteam = games.filter((game) =>
|
const gamesInSteam = games.filter((game) =>
|
||||||
formattedTitle.startsWith(formatName(game.name))
|
formattedTitle.startsWith(game.formattedName)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (gamesInSteam.length === 0) continue;
|
if (gamesInSteam.length === 0) continue;
|
||||||
@@ -101,9 +141,11 @@ const addNewDownloads = async (
|
|||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
};
|
};
|
||||||
|
|
||||||
await repacksSublevel.put(`${repack.id}`, repack);
|
batch.put(`${repack.id}`, repack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await batch.write();
|
||||||
|
|
||||||
const existingSource = await downloadSourcesSublevel.get(
|
const existingSource = await downloadSourcesSublevel.get(
|
||||||
`${downloadSource.id}`
|
`${downloadSource.id}`
|
||||||
);
|
);
|
||||||
@@ -131,6 +173,9 @@ const deleteDownloadSource = async (id: number) => {
|
|||||||
await batch.write();
|
await batch.write();
|
||||||
|
|
||||||
await downloadSourcesSublevel.del(`${id}`);
|
await downloadSourcesSublevel.del(`${id}`);
|
||||||
|
|
||||||
|
invalidateDownloadSourcesCache();
|
||||||
|
invalidateIdCaches();
|
||||||
};
|
};
|
||||||
|
|
||||||
const importDownloadSource = async (url: string) => {
|
const importDownloadSource = async (url: string) => {
|
||||||
@@ -145,11 +190,6 @@ const importDownloadSource = async (url: string) => {
|
|||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
const urlExistsBeforeInsert = await checkUrlExists(url);
|
|
||||||
if (urlExistsBeforeInsert) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextId = await getNextId(downloadSourcesSublevel);
|
const nextId = await getNextId(downloadSourcesSublevel);
|
||||||
|
|
||||||
const downloadSource = {
|
const downloadSource = {
|
||||||
@@ -166,6 +206,8 @@ const importDownloadSource = async (url: string) => {
|
|||||||
|
|
||||||
await downloadSourcesSublevel.put(`${downloadSource.id}`, downloadSource);
|
await downloadSourcesSublevel.put(`${downloadSource.id}`, downloadSource);
|
||||||
|
|
||||||
|
invalidateDownloadSourcesCache();
|
||||||
|
|
||||||
await addNewDownloads(downloadSource, response.data.downloads, steamGames);
|
await addNewDownloads(downloadSource, response.data.downloads, steamGames);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user