mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-25 20:01:03 +00:00
ci: refactored service to event, fixed dynamic import
This commit is contained in:
@@ -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);
|
||||
172
src/main/services/achievements/achievement-image-service.ts
Normal file
172
src/main/services/achievements/achievement-image-service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user