feat: popular tanks

This commit is contained in:
TrèsAbhi committed 2026-02-03 09:04:36 -06:00
1 parent 0ebf9f6c3d
commit ea782af079
9 files changed
+125 -16

No files matched your search

+14 -3
View File
@@ -7,6 +7,7 @@ import type { Backgrounds } from "../../protos/backgrounds";
import type { Tank } from "../../protos/tank";
import type { TankList } from "../../protos/tank_list";
import type { Tanks } from "../../protos/tanks";
import type { PopularTanks } from "../../types/popularTanks";
export abstract class AbstractAPI {
private _tankListCache: TankList | undefined;
@@ -41,7 +42,7 @@ export abstract class AbstractAPI {
private assertLocale(locale: string) {
const supported = locales.supported.find(
(supported) => supported.locale === locale
(supported) => supported.locale === locale,
);
if (supported === undefined) {
@@ -65,7 +66,7 @@ export abstract class AbstractAPI {
private _gameStringsCache: Record<string, Record<string, string>> = {};
protected abstract _gameStrings(
locale: string
locale: string,
): Promise<Record<string, string>>;
async gameStrings(locale: string) {
this.assertLocale(locale);
@@ -83,7 +84,7 @@ export abstract class AbstractAPI {
> = {};
protected abstract _groupedGameStrings(
locale: string,
group: string
group: string,
): Promise<Record<string, string>>;
async groupedGameStrings(locale: string, group: string, prefix: boolean) {
this.assertLocale(locale);
@@ -158,4 +159,14 @@ export abstract class AbstractAPI {
return this._gameStringGroupsCache;
}
private _popularTanksCache: PopularTanks | undefined;
protected abstract _popularTanks(): Promise<PopularTanks>;
async popularTanks() {
if (this._popularTanksCache === undefined) {
this._popularTanksCache = await this._popularTanks();
}
return this._popularTanksCache;
}
}
+89 -6
View File
@@ -1,8 +1,12 @@
import type { MetadataAccessor } from "@blitzkit/closed";
import { sluggify, type DeepPartial } from "@blitzkit/core";
import { assertSecret, sluggify } from "@blitzkit/core";
import type { Strings } from "@blitzkit/i18n";
import locales from "@blitzkit/i18n/locales.json";
import type { RemoteStorageComponent } from "@protos/blitz_static_remote_storage_component";
import type { DeepPartial } from "@protos/blitz_static_reward_currency";
import { existsSync } from "fs";
import { mkdir, readFile, writeFile } from "fs/promises";
import { google } from "googleapis";
import { merge } from "lodash-es";
import { parse } from "yaml";
import type { Avatar } from "../../protos/avatar";
@@ -10,6 +14,7 @@ import type { Background } from "../../protos/background";
import type { Tank } from "../../protos/tank";
import type { TankListEntry } from "../../protos/tank_list";
import type { Tanks } from "../../protos/tanks";
import type { PopularTanks } from "../../types/popularTanks";
import { AbstractAPI } from "./abstract";
if (typeof window !== "undefined") {
@@ -19,6 +24,7 @@ if (typeof window !== "undefined") {
const globbedStrings = import.meta.glob("../../../../i18n/strings/*.json", {
import: "default",
});
const tankSlugPattern = /^(\/\w+)?\/tanks\/([a-z0-9-]+)\/$/;
interface LocalizationConfig {
namespaces: string[];
@@ -34,7 +40,7 @@ export class ServerAPI extends AbstractAPI {
if (group.length !== 1) {
throw new RangeError(
`Don't know how to handle ${group.length} ClientConfigsEntities`
`Don't know how to handle ${group.length} ClientConfigsEntities`,
);
}
@@ -76,7 +82,7 @@ export class ServerAPI extends AbstractAPI {
Object.assign(
strings,
await fetch(
`${remoteStorage.url}${remoteStorage.relative_path}/${namespace}/${locale}.yaml`
`${remoteStorage.url}${remoteStorage.relative_path}/${namespace}/${locale}.yaml`,
)
.then((response) => response.text())
.then((text) => {
@@ -88,13 +94,13 @@ export class ServerAPI extends AbstractAPI {
}
return strings;
})
}),
);
}
} catch (error) {
console.warn(
`Failed to fetch config from ${configPath}. Returning empty strings. This will throw an error in production. Error:`,
error
error,
);
}
@@ -174,7 +180,7 @@ export class ServerAPI extends AbstractAPI {
const tank = item.TankCatalog();
const compensation = item.Compensation();
return { tank, compensation, slug } satisfies Tank;
return { id, slug, tank, compensation } satisfies Tank;
}
protected async _avatars() {
@@ -232,4 +238,81 @@ export class ServerAPI extends AbstractAPI {
return Array.from(groups.values());
}
protected async _popularTanks() {
if (!existsSync("../../temp/popular.json")) {
const { tanks } = await this.tanks();
const tanksArray = Object.values(tanks);
const auth = await google.auth.getClient({
keyFile: assertSecret(import.meta.env.GOOGLE_APPLICATION_CREDENTIALS),
scopes: ["https://www.googleapis.com/auth/analytics.readonly"],
});
const analytics = google.analyticsdata({ version: "v1beta", auth });
const report = await analytics.properties.runReport({
property: `properties/${assertSecret(
import.meta.env.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID,
)}`,
requestBody: {
dateRanges: [{ startDate: "7daysAgo", endDate: "today" }],
dimensions: [{ name: "pagePath" }],
metrics: [{ name: "screenPageViews" }],
orderBys: [{ metric: { metricName: "screenPageViews" }, desc: true }],
dimensionFilter: {
filter: {
fieldName: "pagePath",
stringFilter: {
matchType: "BEGINS_WITH",
value: "/tanks/",
},
},
},
},
});
const views: Record<string, number> = {};
if (!report.data.rows) {
throw new Error("No rows in report");
}
for (const row of report.data.rows) {
if (
!row.dimensionValues ||
!row.dimensionValues[0].value ||
!row.metricValues ||
!row.metricValues[0].value
) {
continue;
}
const matches = row.dimensionValues[0].value.match(tankSlugPattern);
if (!matches) continue;
const slug = matches[2];
const tank = tanksArray.find((tank) => tank.slug === slug);
if (!tank) continue;
if (tank.id in views) {
views[tank.id] += Number(row.metricValues[0].value);
} else {
views[tank.id] = Number(row.metricValues[0].value);
}
}
const popularTanks: PopularTanks = Object.entries(views)
.sort(([, a], [, b]) => b - a)
.map(([id, views]) => ({ id, views }));
await mkdir("../../temp", { recursive: true });
await writeFile("../../temp/popular.json", JSON.stringify(popularTanks));
return popularTanks;
}
const data = await readFile("../../temp/popular.json", "utf-8");
return JSON.parse(data) as PopularTanks;
}
}