mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +02:00
feat: move popular tanks to cron job
This commit is contained in:
1 parent
ff9ec55da6
commit
365c68aa4d
12 files changed
+318
-45
No files matched your search
@@ -41,6 +41,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sharp": "^0.33.5",
|
||||
"googleapis": "^144.0.0",
|
||||
"lodash-es": "^4.17.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -45,5 +45,6 @@ export * from './votes';
|
||||
// export * from './withCORSProxy';
|
||||
export * from './createDefaultProvisions';
|
||||
export * from './metaSortTank';
|
||||
export * from './popularTanks';
|
||||
export * from './romanize';
|
||||
export * from './youtubers';
|
||||
@@ -0,0 +1,7 @@
|
||||
import { fetchPB } from '../protobuf';
|
||||
import { PopularTanks } from '../protos';
|
||||
import { asset } from './asset';
|
||||
|
||||
export function fetchPopularTanks() {
|
||||
return fetchPB(asset('definitions/popular-tanks.pb'), PopularTanks);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { MessageFns } from '../protos';
|
||||
|
||||
export async function fetchPB<Type>(url: string, message: MessageFns<Type>) {
|
||||
const response = await fetch(url);
|
||||
|
||||
const buffer = await response.arrayBuffer();
|
||||
const array = new Uint8Array(buffer);
|
||||
return message.decode(array);
|
||||
|
||||
@@ -7,6 +7,7 @@ export * from './game_definitions.ts';
|
||||
export * from './i18n.ts';
|
||||
export * from './map_definitions.ts';
|
||||
export * from './model_definitions.ts';
|
||||
export * from './popular_tanks.ts';
|
||||
export * from './provision_definitions.ts';
|
||||
export * from './rating_leaderboard.ts';
|
||||
export * from './reviews.ts';
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
syntax = "proto2";
|
||||
|
||||
package blitzkit;
|
||||
|
||||
message PopularTanks {
|
||||
repeated PopularTank tanks = 1;
|
||||
}
|
||||
|
||||
message PopularTank {
|
||||
required uint32 id = 1;
|
||||
required uint32 views = 2; // views in the last 7 days
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-ts_proto v2.2.2
|
||||
// protoc v5.28.2
|
||||
// source: packages/core/src/protos/popular_tanks.proto
|
||||
|
||||
/* eslint-disable */
|
||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
||||
|
||||
export const protobufPackage = "blitzkit";
|
||||
|
||||
export interface PopularTanks {
|
||||
tanks: PopularTank[];
|
||||
}
|
||||
|
||||
export interface PopularTank {
|
||||
id: number;
|
||||
/** views in the last 7 days */
|
||||
views: number;
|
||||
}
|
||||
|
||||
function createBasePopularTanks(): PopularTanks {
|
||||
return { tanks: [] };
|
||||
}
|
||||
|
||||
export const PopularTanks: MessageFns<PopularTanks> = {
|
||||
encode(message: PopularTanks, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
for (const v of message.tanks) {
|
||||
PopularTank.encode(v!, writer.uint32(10).fork()).join();
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): PopularTanks {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBasePopularTanks();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.tanks.push(PopularTank.decode(reader, reader.uint32()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): PopularTanks {
|
||||
return {
|
||||
tanks: globalThis.Array.isArray(object?.tanks) ? object.tanks.map((e: any) => PopularTank.fromJSON(e)) : [],
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: PopularTanks): unknown {
|
||||
const obj: any = {};
|
||||
if (message.tanks?.length) {
|
||||
obj.tanks = message.tanks.map((e) => PopularTank.toJSON(e));
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<PopularTanks>, I>>(base?: I): PopularTanks {
|
||||
return PopularTanks.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<PopularTanks>, I>>(object: I): PopularTanks {
|
||||
const message = createBasePopularTanks();
|
||||
message.tanks = object.tanks?.map((e) => PopularTank.fromPartial(e)) || [];
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBasePopularTank(): PopularTank {
|
||||
return { id: 0, views: 0 };
|
||||
}
|
||||
|
||||
export const PopularTank: MessageFns<PopularTank> = {
|
||||
encode(message: PopularTank, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.id !== 0) {
|
||||
writer.uint32(8).uint32(message.id);
|
||||
}
|
||||
if (message.views !== 0) {
|
||||
writer.uint32(16).uint32(message.views);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): PopularTank {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBasePopularTank();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 8) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.id = reader.uint32();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 16) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.views = reader.uint32();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): PopularTank {
|
||||
return {
|
||||
id: globalThis.Number(assertSet("PopularTank.id", object.id)),
|
||||
views: globalThis.Number(assertSet("PopularTank.views", object.views)),
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: PopularTank): unknown {
|
||||
const obj: any = {};
|
||||
if (message.id !== 0) {
|
||||
obj.id = Math.round(message.id);
|
||||
}
|
||||
if (message.views !== 0) {
|
||||
obj.views = Math.round(message.views);
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<PopularTank>, I>>(base?: I): PopularTank {
|
||||
return PopularTank.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
fromPartial<I extends Exact<DeepPartial<PopularTank>, I>>(object: I): PopularTank {
|
||||
const message = createBasePopularTank();
|
||||
message.id = object.id ?? 0;
|
||||
message.views = object.views ?? 0;
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
||||
|
||||
export type DeepPartial<T> = T extends Builtin ? T
|
||||
: T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
||||
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
||||
: T extends { $case: string; value: unknown } ? { $case: T["$case"]; value?: DeepPartial<T["value"]> }
|
||||
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: Partial<T>;
|
||||
|
||||
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||
export type Exact<P, I extends P> = P extends Builtin ? P
|
||||
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
|
||||
|
||||
function isSet(value: any): boolean {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
|
||||
function assertSet<T>(field: string, value: T | undefined): T {
|
||||
if (!isSet(value)) {
|
||||
throw new TypeError(`Required field ${field} is not set`);
|
||||
}
|
||||
|
||||
return value as T;
|
||||
}
|
||||
|
||||
export interface MessageFns<T> {
|
||||
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
||||
fromJSON(object: any): T;
|
||||
toJSON(message: T): unknown;
|
||||
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
|
||||
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { flags } from './flags';
|
||||
import { gameModeBanners } from './gameModeBanners';
|
||||
import { migration } from './migration';
|
||||
import { moduleIcons } from './moduleIcons';
|
||||
import { popularTanks } from './popularTanks';
|
||||
import { shellIcons } from './shellIcons';
|
||||
import { skillIcons } from './skillIcons';
|
||||
import { tankArmors } from './tankArmors';
|
||||
@@ -45,6 +46,7 @@ const methods = [
|
||||
boosterIcons,
|
||||
tankIcons,
|
||||
blitzkitTankIcons,
|
||||
popularTanks,
|
||||
];
|
||||
|
||||
for (const method of methods) {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
assertSecret,
|
||||
encodePBBuffer,
|
||||
fetchTankDefinitions,
|
||||
PopularTanks,
|
||||
} from '@blitzkit/core';
|
||||
import { google } from 'googleapis';
|
||||
import { commitAssets } from '../core/github/commitAssets';
|
||||
|
||||
export async function popularTanks() {
|
||||
const tankDefinitions = await fetchTankDefinitions();
|
||||
|
||||
const auth = await google.auth.getClient({
|
||||
keyFile: 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: '/tools/tankopedia/',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!report.data.rows) {
|
||||
throw new Error('No rows in report');
|
||||
}
|
||||
|
||||
const tanks = report.data.rows
|
||||
.filter(
|
||||
(row) =>
|
||||
row.dimensionValues &&
|
||||
row.dimensionValues[0].value &&
|
||||
row.dimensionValues[0].value !== '/tools/tankopedia/' &&
|
||||
row.metricValues &&
|
||||
row.metricValues[0].value,
|
||||
)
|
||||
.map((row) => ({
|
||||
id: Number(
|
||||
row.dimensionValues![0].value!.match(
|
||||
/\/tools\/tankopedia\/(\d+)\/?/,
|
||||
)?.[1],
|
||||
),
|
||||
views: Number(row.metricValues![0].value!),
|
||||
}))
|
||||
.filter((row) => {
|
||||
if (!(row.id in tankDefinitions.tanks)) {
|
||||
console.warn('Skipping unknown popular tank', row.id);
|
||||
return false;
|
||||
}
|
||||
|
||||
return row.id in tankDefinitions.tanks;
|
||||
})
|
||||
.slice(0, 8)
|
||||
.map(({ id, views }) => ({ id, views }));
|
||||
const popularTanks: PopularTanks = { tanks };
|
||||
|
||||
await commitAssets('popular tanks', [
|
||||
{
|
||||
path: 'definitions/popular-tanks.pb',
|
||||
content: encodePBBuffer(PopularTanks, popularTanks),
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -25,7 +25,6 @@
|
||||
"@react-three/fiber": "^8.17.10",
|
||||
"compare-versions": "^6.1.1",
|
||||
"echarts": "^5.5.1",
|
||||
"googleapis": "^144.0.0",
|
||||
"immer": "^10.1.1",
|
||||
"js-cookie": "^3.0.5",
|
||||
"markdown-to-jsx": "^7.5.0",
|
||||
|
||||
@@ -3,6 +3,7 @@ import { literals } from '@blitzkit/i18n/src/literals';
|
||||
import { EyeOpenIcon } from '@radix-ui/react-icons';
|
||||
import { Box, Flex, Heading, Text } from '@radix-ui/themes';
|
||||
import { google } from 'googleapis';
|
||||
import { awaitablePopularTanks } from '../core/awaitables/popularTanks';
|
||||
import { awaitableTankDefinitions } from '../core/awaitables/tankDefinitions';
|
||||
import {
|
||||
LocaleProvider,
|
||||
@@ -12,7 +13,10 @@ import {
|
||||
import { TankopediaPersistent } from '../stores/tankopediaPersistent';
|
||||
import { TankCard } from './TankCard';
|
||||
|
||||
const [tankDefinitions] = await Promise.all([awaitableTankDefinitions, ,]);
|
||||
const [tankDefinitions, popularTanks] = await Promise.all([
|
||||
awaitableTankDefinitions,
|
||||
awaitablePopularTanks,
|
||||
]);
|
||||
|
||||
const auth = await google.auth.getClient({
|
||||
keyFile: import.meta.env.GOOGLE_APPLICATION_CREDENTIALS,
|
||||
@@ -39,36 +43,6 @@ if (!report.data.rows) {
|
||||
throw new Error('No rows in report');
|
||||
}
|
||||
|
||||
const hotTanks = report.data.rows
|
||||
.filter(
|
||||
(row) =>
|
||||
row.dimensionValues &&
|
||||
row.dimensionValues[0].value &&
|
||||
row.dimensionValues[0].value !== '/tools/tankopedia/' &&
|
||||
row.metricValues &&
|
||||
row.metricValues[0].value,
|
||||
)
|
||||
.map((row) => ({
|
||||
id: Number(
|
||||
row.dimensionValues![0].value!.match(
|
||||
/\/tools\/tankopedia\/(\d+)\/?/,
|
||||
)?.[1],
|
||||
),
|
||||
views: Number(row.metricValues![0].value!),
|
||||
}))
|
||||
.filter((row) => {
|
||||
if (!(row.id in tankDefinitions.tanks)) {
|
||||
// console.log('Unknown tank', row.id);
|
||||
}
|
||||
|
||||
return row.id in tankDefinitions.tanks;
|
||||
})
|
||||
.slice(0, 8)
|
||||
.map(({ id, views }) => ({
|
||||
tank: tankDefinitions.tanks[id],
|
||||
views,
|
||||
}));
|
||||
|
||||
export function HomePageHotTanks({ locale }: LocaleAcceptorProps) {
|
||||
return (
|
||||
<LocaleProvider locale={locale}>
|
||||
@@ -92,19 +66,25 @@ function Content() {
|
||||
</Text>
|
||||
|
||||
<Flex justify="center" gap="4" wrap="wrap">
|
||||
{hotTanks.map((row) => (
|
||||
<Box width="7rem" key={row.tank.id}>
|
||||
<TankCard
|
||||
tank={row.tank}
|
||||
discriminator={
|
||||
<Flex align="center" gap="1" justify="center">
|
||||
<EyeOpenIcon />
|
||||
{Math.round(row.views * (30 / 7)).toLocaleString(locale)}
|
||||
</Flex>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
{popularTanks.tanks.map(({ id, views }) => {
|
||||
const tank = tankDefinitions.tanks[id];
|
||||
|
||||
if (tank === undefined) return null;
|
||||
|
||||
return (
|
||||
<Box width="7rem" key={tank.id}>
|
||||
<TankCard
|
||||
tank={tank}
|
||||
discriminator={
|
||||
<Flex align="center" gap="1" justify="center">
|
||||
<EyeOpenIcon />
|
||||
{Math.round(views * (30 / 7)).toLocaleString(locale)}
|
||||
</Flex>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { fetchPopularTanks } from '@blitzkit/core';
|
||||
|
||||
export const awaitablePopularTanks = fetchPopularTanks();
|
||||
Reference in new issue
Block a user