Skip to content

Commit e50334d

Browse files
authored
Merge pull request #187 from game-node-app/dev
Playtime tracking improvements across platforms
2 parents 5f44a22 + b3a1b74 commit e50334d

21 files changed

+332
-233
lines changed

server_swagger.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

src/game/game-achievement/game-achievement.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { GameAchievementOverviewModule } from "./overview/game-achievement-overv
1313
import { PsnSyncModule } from "../../sync/psn/psn-sync.module";
1414
import { XboxSyncModule } from "../../sync/xbox/xbox-sync.module";
1515
import { GameAchievementV2Controller } from "./game-achievement-v2.controller";
16+
import { GameRepositoryModule } from "../game-repository/game-repository.module";
1617

1718
@Module({
1819
imports: [
@@ -29,6 +30,7 @@ import { GameAchievementV2Controller } from "./game-achievement-v2.controller";
2930
XboxSyncModule,
3031
ConnectionsModule,
3132
GameAchievementOverviewModule,
33+
GameRepositoryModule,
3234
],
3335
providers: [
3436
GameAchievementService,

src/game/game-achievement/game-achievement.service.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
getStoreAbbreviatedNameForExternalGameCategory,
2323
getStoreNameForExternalGameCategory,
2424
} from "../external-game/external-game.utils";
25+
import { GameRepositoryService } from "../game-repository/game-repository.service";
2526

2627
const getPSNAchievementId = (npCommunicationId: string, trophyId: number) =>
2728
`${npCommunicationId}_${trophyId}`;
@@ -53,6 +54,7 @@ export class GameAchievementService {
5354
private readonly psnSyncService: PsnSyncService,
5455
private readonly xboxSyncService: XboxSyncService,
5556
private readonly connectionsService: ConnectionsService,
57+
private readonly gameRepositoryService: GameRepositoryService,
5658
) {}
5759

5860
public async findOneByExternalGameId(
@@ -79,11 +81,17 @@ export class GameAchievementService {
7981
const externalGame =
8082
await this.externalGameService.findOneByIdOrFail(externalGameId);
8183

84+
const platformsMap =
85+
await this.gameRepositoryService.getGamePlatformsMap(
86+
"abbreviation",
87+
);
88+
8289
const achievements = await match<
8390
EGameExternalGameCategory,
8491
Promise<GameAchievementDto[]>
8592
>(externalGame.category!)
8693
.with(EGameExternalGameCategory.Steam, async () => {
94+
const PC_PLATFORM_ID = 6;
8795
const appId = Number.parseInt(externalGame.uid);
8896
const [achievements, achievementsPercentage] =
8997
await Promise.all([
@@ -108,7 +116,7 @@ export class GameAchievementService {
108116
relatedPercentage?.percent ?? 0,
109117
),
110118
},
111-
platformIds: [6],
119+
platformIds: [PC_PLATFORM_ID],
112120
} satisfies GameAchievementDto;
113121
});
114122
})
@@ -211,6 +219,33 @@ export class GameAchievementService {
211219
(reward) => reward.type === "Gamerscore",
212220
);
213221

222+
const availablePlatformIds = achievement.platforms.map(
223+
(platform) => {
224+
return match(platform)
225+
.with(
226+
P.union("PC", "Win32"),
227+
() => platformsMap.get("PC")!.id,
228+
)
229+
.with(
230+
"Xbox360",
231+
() => platformsMap.get("X360")!.id,
232+
)
233+
.with(
234+
"XboxOne",
235+
() => platformsMap.get("XONE")!.id,
236+
)
237+
.with(
238+
"XboxSeries",
239+
() =>
240+
platformsMap.get("Series X|S")!.id,
241+
)
242+
.otherwise(
243+
() =>
244+
platformsMap.get("Series X|S")!.id,
245+
);
246+
},
247+
);
248+
214249
return {
215250
externalGameId: externalGame.id,
216251
source: EGameExternalGameCategory.Microsoft,
@@ -225,7 +260,7 @@ export class GameAchievementService {
225260
: 0,
226261
},
227262
// XONE and X Series S/X
228-
platformIds: [49, 169],
263+
platformIds: availablePlatformIds,
229264
} satisfies GameAchievementDto;
230265
});
231266
},

src/game/game-repository/game-repository-cache.service.ts

Lines changed: 0 additions & 79 deletions
This file was deleted.

src/game/game-repository/game-repository.constants.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ enum EGameStorageSource {
8686
* Check /public/icons for reference. <br>
8787
* Format: iconName: [platforms]
8888
*/
89-
const platformAbbreviationToIconMap: { [p: string]: string[] } = {
89+
const PlatformToIconMap: { [p: string]: string[] } = {
9090
windows: ["PC"],
9191
linux: ["Linux"],
9292
ps1: ["PS1"],
@@ -120,14 +120,12 @@ const platformAbbreviationToIconMap: { [p: string]: string[] } = {
120120
android: ["Android", "iOS"],
121121
};
122122

123-
const DEFAULT_GAME_CATEGORIES = [EGameCategory.Main];
124-
125123
export {
126124
EGameCategory,
127125
EGameStatus,
128126
EGameExternalGameMedia,
129127
EGameExternalGameCategory,
130128
EGamePlatformCategory,
131129
EGameStorageSource,
132-
platformAbbreviationToIconMap,
130+
PlatformToIconMap,
133131
};

src/game/game-repository/game-repository.controller.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ export class GameRepositoryController {
3333
return this.gameRepositoryService.getIconsNamesForPlatforms(gameId);
3434
}
3535

36+
/**
37+
* TODO: Move this endpoint to {@link ExternalGameService}
38+
* @param gameId
39+
*/
3640
@Get(":id/external-stores")
3741
@HttpCode(200)
3842
getExternalStoresForGameId(@Param("id") gameId: number) {

src/game/game-repository/game-repository.module.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import { GameCompanyLogo } from "./entities/game-company-logo.entity";
2323
import { GamePlayerPerspective } from "./entities/game-player-perspective.entity";
2424
import { GameRepositoryCreateService } from "./game-repository-create.service";
2525
import { StatisticsQueueModule } from "../../statistics/statistics-queue/statistics-queue.module";
26-
import { GameRepositoryCacheService } from "./game-repository-cache.service";
2726
import { ExternalGameModule } from "../external-game/external-game.module";
2827

2928
/**
@@ -55,11 +54,7 @@ import { ExternalGameModule } from "../external-game/external-game.module";
5554
forwardRef(() => StatisticsQueueModule),
5655
ExternalGameModule,
5756
],
58-
providers: [
59-
GameRepositoryService,
60-
GameRepositoryCreateService,
61-
GameRepositoryCacheService,
62-
],
57+
providers: [GameRepositoryService, GameRepositoryCreateService],
6358
exports: [GameRepositoryService, GameRepositoryCreateService],
6459
controllers: [GameRepositoryController],
6560
})

src/game/game-repository/game-repository.service.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,18 @@ import { GameRepositoryFindOneDto } from "./dto/game-repository-find-one.dto";
1313
import { GameMode } from "./entities/game-mode.entity";
1414
import { GamePlayerPerspective } from "./entities/game-player-perspective.entity";
1515
import { GameRepositoryFilterDto } from "./dto/game-repository-filter.dto";
16-
import { platformAbbreviationToIconMap } from "./game-repository.constants";
16+
import { PlatformToIconMap } from "./game-repository.constants";
1717
import { GameExternalStoreDto } from "./dto/game-external-store.dto";
1818
import { toMap } from "../../utils/toMap";
1919
import { getRelationLoadStrategy } from "../../utils/getRelationLoadStrategy";
20-
import { GameRepositoryCacheService } from "./game-repository-cache.service";
2120
import { ExternalGameService } from "../external-game/external-game.service";
2221
import {
2322
getIconNameForExternalGameCategory,
2423
getStoreNameForExternalGameCategory,
2524
} from "../external-game/external-game.utils";
2625
import { buildGameFilterFindOptions } from "./utils/build-game-filter-find-options";
26+
import { Cacheable } from "../../utils/cacheable";
27+
import { hours, minutes } from "@nestjs/throttler";
2728

2829
/**
2930
* Look-up table between resource names and their respective entities
@@ -47,21 +48,23 @@ export class GameRepositoryService {
4748
* @param dataSource
4849
* @param gamePlatformRepository
4950
* @param externalGameService
50-
* @param gameRepositoryCacheService
51+
* @param gameRepository
5152
*/
5253
constructor(
5354
private readonly dataSource: DataSource,
5455
@InjectRepository(GamePlatform)
5556
private readonly gamePlatformRepository: Repository<GamePlatform>,
5657
private readonly externalGameService: ExternalGameService,
57-
private readonly gameRepositoryCacheService: GameRepositoryCacheService,
58+
@InjectRepository(Game)
59+
private readonly gameRepository: Repository<Game>,
5860
) {}
5961

62+
@Cacheable(GameRepositoryService.name, minutes(5))
6063
async findOneById(
6164
id: number,
6265
dto?: GameRepositoryFindOneDto,
6366
): Promise<Game> {
64-
const game = await this.gameRepositoryCacheService.findOne({
67+
const game = await this.gameRepository.findOne({
6568
where: {
6669
id,
6770
},
@@ -85,6 +88,7 @@ export class GameRepositoryService {
8588
.filter((game) => game != undefined) as Game[];
8689
}
8790

91+
@Cacheable(GameRepositoryService.name, minutes(5))
8892
async findAllByIds(dto: GameRepositoryFindAllDto) {
8993
if (
9094
dto == undefined ||
@@ -94,7 +98,7 @@ export class GameRepositoryService {
9498
throw new HttpException("Invalid query.", HttpStatus.BAD_REQUEST);
9599
}
96100

97-
const games = await this.gameRepositoryCacheService.find({
101+
const games = await this.gameRepository.find({
98102
where: {
99103
id: In(dto?.gameIds),
100104
},
@@ -114,20 +118,21 @@ export class GameRepositoryService {
114118

115119
async findAll(dto: BaseFindDto<Game>): Promise<TPaginationData<Game>> {
116120
const findOptions = buildBaseFindOptions(dto);
117-
return this.gameRepositoryCacheService.findAndCount(findOptions);
121+
return this.gameRepository.findAndCount(findOptions);
118122
}
119123

120124
/**
121125
* @warning This operation can be quite expensive.
122126
* @param filterDto
123127
*/
128+
@Cacheable(GameRepositoryService.name, minutes(5))
124129
async findAllIdsWithFilters(
125130
filterDto: GameRepositoryFilterDto,
126131
): Promise<Game[]> {
127132
const findOptions = buildBaseFindOptions(filterDto);
128133
const whereOptions = buildGameFilterFindOptions(filterDto);
129134

130-
const games = await this.gameRepositoryCacheService.find({
135+
const games = await this.gameRepository.find({
131136
...findOptions,
132137
where: whereOptions,
133138
});
@@ -139,6 +144,7 @@ export class GameRepositoryService {
139144
return games;
140145
}
141146

147+
@Cacheable(GameRepositoryService.name, minutes(30))
142148
async findGameExternalStores(gameId: number) {
143149
const externalGames = await this.externalGameService.findAllForGameId([
144150
gameId,
@@ -171,6 +177,7 @@ export class GameRepositoryService {
171177
});
172178
}
173179

180+
@Cacheable(GameRepositoryService.name, hours(1))
174181
async getResource(resource: TAllowedResource): Promise<any> {
175182
const resourceAsEntity = resourceToTargetEntityMap[resource];
176183
if (resourceAsEntity == undefined) {
@@ -182,6 +189,15 @@ export class GameRepositoryService {
182189
return await resourceRepository.find();
183190
}
184191

192+
async getGamePlatformsMap<T extends "id" | "name" | "abbreviation">(
193+
identifier: T,
194+
) {
195+
const platforms: GamePlatform[] = await this.getResource("platforms");
196+
197+
return toMap(platforms, identifier);
198+
}
199+
200+
@Cacheable(GameRepositoryService.name, minutes(30))
185201
async getIconsNamesForPlatforms(gameId: number) {
186202
const platforms = await this.gamePlatformRepository.find({
187203
where: {
@@ -195,9 +211,7 @@ export class GameRepositoryService {
195211
(platform) => platform.abbreviation,
196212
);
197213
const iconsNames: string[] = [];
198-
for (const [iconName, platforms] of Object.entries(
199-
platformAbbreviationToIconMap,
200-
)) {
214+
for (const [iconName, platforms] of Object.entries(PlatformToIconMap)) {
201215
const abbreviationPresent = platformAbbreviations.some(
202216
(abbreviation) => platforms.includes(abbreviation),
203217
);

src/game/game-repository/game-repository.utils.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
1-
import {
2-
EGameCategory,
3-
platformAbbreviationToIconMap,
4-
} from "./game-repository.constants";
1+
import { EGameCategory, PlatformToIconMap } from "./game-repository.constants";
52

63
export function getIconNamesForPlatformAbbreviations(abbreviations: string[]) {
74
const iconsNames: string[] = [];
8-
for (const [iconName, platforms] of Object.entries(
9-
platformAbbreviationToIconMap,
10-
)) {
5+
for (const [iconName, platforms] of Object.entries(PlatformToIconMap)) {
116
const abbreviationPresent = abbreviations.some((abbreviation) =>
127
platforms.includes(abbreviation),
138
);

src/playtime/dto/create-user-playtime.dto.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export class CreateUserPlaytimeDto extends OmitType(UserPlaytime, [
55
"id",
66
"profile",
77
"game",
8+
"platform",
89
"createdAt",
910
"updatedAt",
1011
"totalPlayCount",

0 commit comments

Comments
 (0)