ci: refactored service to event, fixed dynamic import

This commit is contained in:
Moyasee
2025-10-22 10:40:52 +03:00
parent ba4610705d
commit 1b4a1360a6
4 changed files with 175 additions and 160 deletions

View File

@@ -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<string> => {
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<string> => {
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<void> => {
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<string, User>(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);

View File

@@ -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<string> {
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<string> {
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<void> {
await HydraApi.patch(
`/profile/games/achievements/${shop}/${gameId}/${achievementName}/image`,
{ achievementImageUrl: imageUrl }
);
}
private static async hasActiveSubscription(): Promise<boolean> {
return db
.get<string, User>(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<void> {
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;
}
}
}

View File

@@ -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,

View File

@@ -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<string, UserPreferences | null>(
levelKeys.userPreferences,