From 1b4a1360a6a4b8c801cd1c0a989acda989acd6e4 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Wed, 22 Oct 2025 10:40:52 +0300 Subject: [PATCH] ci: refactored service to event, fixed dynamic import --- .../achievements/upload-achievement-image.ts | 154 ---------------- .../achievements/achievement-image-service.ts | 172 ++++++++++++++++++ .../achievements/merge-achievements.ts | 7 +- src/main/services/user/get-user-data.ts | 2 +- 4 files changed, 175 insertions(+), 160 deletions(-) delete mode 100644 src/main/events/achievements/upload-achievement-image.ts create mode 100644 src/main/services/achievements/achievement-image-service.ts diff --git a/src/main/events/achievements/upload-achievement-image.ts b/src/main/events/achievements/upload-achievement-image.ts deleted file mode 100644 index a050b941..00000000 --- a/src/main/events/achievements/upload-achievement-image.ts +++ /dev/null @@ -1,154 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import axios from "axios"; -import { fileTypeFromFile } from "file-type"; -import { HydraApi } from "@main/services/hydra-api"; -import { registerEvent } from "../register-event"; -import { gameAchievementsSublevel, levelKeys, db } from "@main/level"; -import { logger } from "@main/services/logger"; -import type { GameShop, User } from "@types"; - -const uploadImageToCDN = async (imagePath: string): Promise => { - const stat = fs.statSync(imagePath); - const fileBuffer = fs.readFileSync(imagePath); - const fileSizeInBytes = stat.size; - - const response = await HydraApi.post<{ - presignedUrl: string; - achievementImageUrl: string; - }>("/presigned-urls/achievement-image", { - imageExt: path.extname(imagePath).slice(1), - imageLength: fileSizeInBytes, - }); - - const mimeType = await fileTypeFromFile(imagePath); - - await axios.put(response.presignedUrl, fileBuffer, { - headers: { - "Content-Type": mimeType?.mime, - }, - }); - - return response.achievementImageUrl; -}; - -const storeImageLocally = async (imagePath: string): Promise => { - const fileBuffer = fs.readFileSync(imagePath); - const base64Image = fileBuffer.toString("base64"); - const mimeType = await fileTypeFromFile(imagePath); - - return `data:${mimeType?.mime || "image/jpeg"};base64,${base64Image}`; -}; - -const updateAchievementWithImageUrl = async ( - shop: GameShop, - gameId: string, - achievementName: string, - imageUrl: string -): Promise => { - await HydraApi.patch( - `/profile/games/achievements/${shop}/${gameId}/${achievementName}/image`, - { achievementImageUrl: imageUrl } - ); -}; - -export const uploadAchievementImage = async ( - gameId: string, - achievementName: string, - imagePath: string, - shop?: GameShop -): Promise<{ success: boolean; imageUrl: string }> => { - try { - let imageUrl: string; - - const hasSubscription = await db - .get(levelKeys.user, { valueEncoding: "json" }) - .then((user) => { - const expiresAt = new Date(user?.subscription?.expiresAt ?? 0); - return expiresAt > new Date(); - }) - .catch(() => false); - - if (hasSubscription) { - imageUrl = await uploadImageToCDN(imagePath); - if (shop) { - await updateAchievementWithImageUrl( - shop, - gameId, - achievementName, - imageUrl - ); - } - logger.log( - `Achievement image uploaded to CDN for ${gameId}:${achievementName}` - ); - } else { - imageUrl = await storeImageLocally(imagePath); - logger.log( - `Achievement image stored locally for ${gameId}:${achievementName}` - ); - } - - return { success: true, imageUrl }; - } catch (error) { - logger.error( - `Failed to upload achievement image for ${gameId}:${achievementName}:`, - error - ); - throw error; - } -}; - -const uploadAchievementImageEvent = async ( - _event: Electron.IpcMainInvokeEvent, - params: { - imagePath: string; - gameId: string; - achievementName: string; - shop: GameShop; - } -) => { - const { imagePath, gameId, achievementName, shop } = params; - - try { - const result = await uploadAchievementImage( - gameId, - achievementName, - imagePath, - shop - ); - - const achievementKey = levelKeys.game(shop, gameId); - const existingData = await gameAchievementsSublevel - .get(achievementKey) - .catch(() => null); - - if (existingData) { - await gameAchievementsSublevel.put(achievementKey, { - ...existingData, - achievementImageUrl: result.imageUrl, - }); - } - - try { - fs.unlinkSync(imagePath); - } catch (error) { - logger.error(`Failed to cleanup screenshot file ${imagePath}:`, error); - } - - return result; - } catch (error) { - try { - fs.unlinkSync(imagePath); - } catch (cleanupError) { - logger.error( - `Failed to cleanup screenshot file ${imagePath}:`, - cleanupError - ); - } - - throw error; - } -}; - -registerEvent("uploadAchievementImage", uploadAchievementImageEvent); diff --git a/src/main/services/achievements/achievement-image-service.ts b/src/main/services/achievements/achievement-image-service.ts new file mode 100644 index 00000000..bb3d272f --- /dev/null +++ b/src/main/services/achievements/achievement-image-service.ts @@ -0,0 +1,172 @@ +import fs from "node:fs"; +import path from "node:path"; +import axios from "axios"; +import { fileTypeFromFile } from "file-type"; +import { HydraApi } from "@main/services/hydra-api"; +import { gameAchievementsSublevel, levelKeys, db } from "@main/level"; +import { logger } from "@main/services/logger"; +import type { GameShop, User } from "@types"; + +export class AchievementImageService { + private static async uploadImageToCDN(imagePath: string): Promise { + const stat = fs.statSync(imagePath); + const fileBuffer = fs.readFileSync(imagePath); + const fileSizeInBytes = stat.size; + + const response = await HydraApi.post<{ + presignedUrl: string; + achievementImageUrl: string; + }>("/presigned-urls/achievement-image", { + imageExt: path.extname(imagePath).slice(1), + imageLength: fileSizeInBytes, + }); + + const mimeType = await fileTypeFromFile(imagePath); + + await axios.put(response.presignedUrl, fileBuffer, { + headers: { + "Content-Type": mimeType?.mime, + }, + }); + + return response.achievementImageUrl; + } + + private static async storeImageLocally(imagePath: string): Promise { + const fileBuffer = fs.readFileSync(imagePath); + const base64Image = fileBuffer.toString("base64"); + const mimeType = await fileTypeFromFile(imagePath); + + return `data:${mimeType?.mime || "image/jpeg"};base64,${base64Image}`; + } + + private static async updateAchievementWithImageUrl( + shop: GameShop, + gameId: string, + achievementName: string, + imageUrl: string + ): Promise { + await HydraApi.patch( + `/profile/games/achievements/${shop}/${gameId}/${achievementName}/image`, + { achievementImageUrl: imageUrl } + ); + } + + private static async hasActiveSubscription(): Promise { + return db + .get(levelKeys.user, { valueEncoding: "json" }) + .then((user) => { + const expiresAt = new Date(user?.subscription?.expiresAt ?? 0); + return expiresAt > new Date(); + }) + .catch(() => false); + } + + private static async updateLocalAchievementData( + shop: GameShop, + gameId: string, + imageUrl: string + ): Promise { + const achievementKey = levelKeys.game(shop, gameId); + const existingData = await gameAchievementsSublevel + .get(achievementKey) + .catch(() => null); + + if (existingData) { + await gameAchievementsSublevel.put(achievementKey, { + ...existingData, + achievementImageUrl: imageUrl, + }); + } + } + + private static cleanupImageFile(imagePath: string): void { + try { + fs.unlinkSync(imagePath); + } catch (error) { + logger.error(`Failed to cleanup screenshot file ${imagePath}:`, error); + } + } + + /** + * Uploads an achievement image either to CDN (for subscribers) or stores locally + * @param gameId - The game identifier + * @param achievementName - The achievement name + * @param imagePath - Path to the image file to upload + * @param shop - The game shop (optional) + * @returns Promise with success status and image URL + */ + static async uploadAchievementImage( + gameId: string, + achievementName: string, + imagePath: string, + shop?: GameShop + ): Promise<{ success: boolean; imageUrl: string }> { + try { + let imageUrl: string; + + const hasSubscription = await this.hasActiveSubscription(); + + if (hasSubscription) { + imageUrl = await this.uploadImageToCDN(imagePath); + if (shop) { + await this.updateAchievementWithImageUrl( + shop, + gameId, + achievementName, + imageUrl + ); + } + logger.log( + `Achievement image uploaded to CDN for ${gameId}:${achievementName}` + ); + } else { + imageUrl = await this.storeImageLocally(imagePath); + logger.log( + `Achievement image stored locally for ${gameId}:${achievementName}` + ); + } + + return { success: true, imageUrl }; + } catch (error) { + logger.error( + `Failed to upload achievement image for ${gameId}:${achievementName}:`, + error + ); + throw error; + } + } + + /** + * Uploads achievement image and updates local database, with automatic cleanup + * @param gameId - The game identifier + * @param achievementName - The achievement name + * @param imagePath - Path to the image file to upload + * @param shop - The game shop + * @returns Promise with success status and image URL + */ + static async uploadAndUpdateAchievementImage( + gameId: string, + achievementName: string, + imagePath: string, + shop: GameShop + ): Promise<{ success: boolean; imageUrl: string }> { + try { + const result = await this.uploadAchievementImage( + gameId, + achievementName, + imagePath, + shop + ); + + await this.updateLocalAchievementData(shop, gameId, result.imageUrl); + + this.cleanupImageFile(imagePath); + + return result; + } catch (error) { + this.cleanupImageFile(imagePath); + throw error; + } + } +} \ No newline at end of file diff --git a/src/main/services/achievements/merge-achievements.ts b/src/main/services/achievements/merge-achievements.ts index 7eaf44dc..8d07ecf6 100644 --- a/src/main/services/achievements/merge-achievements.ts +++ b/src/main/services/achievements/merge-achievements.ts @@ -16,6 +16,7 @@ import { db, gameAchievementsSublevel, levelKeys } from "@main/level"; import { getGameAchievementData } from "./get-game-achievement-data"; import { AchievementWatcherManager } from "./achievement-watcher-manager"; import { ScreenshotService } from "../screenshot"; +import { AchievementImageService } from "./achievement-image-service"; const isRareAchievement = (points: number) => { const rawPercentage = (50 - Math.sqrt(points)) * 2; @@ -195,10 +196,6 @@ export const mergeAchievements = async ( userPreferences.enableAchievementScreenshots === true ) { try { - const { uploadAchievementImage } = await import( - "@main/events/achievements/upload-achievement-image" - ); - for (const achievement of newAchievements) { try { const achievementData = achievementsData.find( @@ -219,7 +216,7 @@ export const mergeAchievements = async ( achievementDisplayName ); - await uploadAchievementImage( + await AchievementImageService.uploadAchievementImage( game.objectId, achievement.name, screenshotPath, diff --git a/src/main/services/user/get-user-data.ts b/src/main/services/user/get-user-data.ts index 383aafa8..c17df53c 100644 --- a/src/main/services/user/get-user-data.ts +++ b/src/main/services/user/get-user-data.ts @@ -11,7 +11,7 @@ import { db } from "@main/level"; import { levelKeys } from "@main/level/sublevels"; export const getUserData = async () => { - let language = "en"; + let language = "en"; try { const userPreferences = await db.get( levelKeys.userPreferences,