feat: automatic videos

This commit is contained in:
TrèsAbhi committed 2024-04-21 19:40:38 -05:00
1 parent 9da723ed31
commit 9ead3eb627
4 files changed
+99 -37

No files matched your search

+1
View File
@@ -164,6 +164,7 @@
"xkdkw", "xkdkw",
"XMLDVPL", "XMLDVPL",
"YAMLDVPL", "YAMLDVPL",
"youtubers",
"zustand" "zustand"
], ],
"deno.enable": false, "deno.enable": false,
+4 -4
View File
@@ -38,7 +38,7 @@ import { DATA, POI } from './constants';
import { Avatar } from './skillIcons'; import { Avatar } from './skillIcons';
import { TankParameters } from './tankIcons'; import { TankParameters } from './tankIcons';
interface Strings { export interface BlitzStrings {
[key: string]: string; [key: string]: string;
} }
type ShellKind = type ShellKind =
@@ -334,7 +334,7 @@ const blitzShellKindToBLitzkrieg: Record<ShellKind, ShellType> = {
const missingStrings: Record<string, string> = { const missingStrings: Record<string, string> = {
'#artefacts:tungstentip/name': 'Tungsten Shells', '#artefacts:tungstentip/name': 'Tungsten Shells',
}; };
const botPattern = /^.+((tutorial_bot(\d+)?)|(TU(R?)))$/; export const botPattern = /^.+((tutorial_bot(\d+)?)|(TU(R?)))$/;
export async function definitions(production: boolean) { export async function definitions(production: boolean) {
console.log('Building definitions...'); console.log('Building definitions...');
@@ -361,12 +361,12 @@ export async function definitions(production: boolean) {
(nations) => nations.filter((nation) => nation !== 'common'), (nations) => nations.filter((nation) => nation !== 'common'),
); );
const tankStringIdMap: Record<string, number> = {}; const tankStringIdMap: Record<string, number> = {};
const stringsPreInstalled = await readYAMLDVPL<Strings>( const stringsPreInstalled = await readYAMLDVPL<BlitzStrings>(
`${DATA}/${POI.strings}/en.yaml.dvpl`, `${DATA}/${POI.strings}/en.yaml.dvpl`,
); );
const stringsCache = await fetch(POI.cachedStrings) const stringsCache = await fetch(POI.cachedStrings)
.then((response) => response.text()) .then((response) => response.text())
.then((string) => parseYaml(string) as Strings); .then((string) => parseYaml(string) as BlitzStrings);
const optionalDevices = await readXMLDVPL<{ root: OptionalDevices }>( const optionalDevices = await readXMLDVPL<{ root: OptionalDevices }>(
`${DATA}/${POI.optionalDevices}.dvpl`, `${DATA}/${POI.optionalDevices}.dvpl`,
); );
+79 -28
View File
@@ -1,45 +1,96 @@
import { readdir } from 'fs/promises';
import { google } from 'googleapis'; import { google } from 'googleapis';
import { env } from 'process'; import { parse as parseYaml } from 'yaml';
import { readXMLDVPL } from '../../src/core/blitz/readXMLDVPL';
import { readYAMLDVPL } from '../../src/core/blitz/readYAMLDVPL';
import { toUniqueId } from '../../src/core/blitz/toUniqueId';
import { commitAssets } from '../../src/core/blitzkrieg/commitAssets'; import { commitAssets } from '../../src/core/blitzkrieg/commitAssets';
import { DATA, POI } from './constants';
import { BlitzStrings, botPattern, VehicleDefinitionList } from './definitions';
const youtubers = [
'UCKBYXp4Xn2I2tL1UL4fpbhw', // droodles
'UCrQ-Dy8lVsm11u8pCJ8W7Tw', // fatness
];
export async function videos(production: boolean) { export async function videos(production: boolean) {
console.log('Building videos...'); console.log('Building videos...');
const auth = await google.auth.getClient({ const auth = await google.auth.getClient({
scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly'], scopes: ['https://www.googleapis.com/auth/youtube.readonly'],
});
const sheets = google.sheets({ version: 'v4', auth });
const response = await sheets.spreadsheets.values.get({
spreadsheetId: env.VIDEOS_SHEET,
range: 'Sheet1!A:Z',
}); });
const youtube = google.youtube({ version: 'v3', auth });
if (!response.data.values) { async function search(channel: string, search: string) {
console.warn('Google returned no data; skipping videos...'); (
return; await youtube.search.list({
channelId: channel,
maxResults: 1,
part: ['snippet'],
q: search,
})
).data.items?.at(0)?.id ?? null;
} }
const content = response.data.values const nations = await readdir(`${DATA}/${POI.vehicleDefinitions}`).then(
.slice(1) (nations) => nations.filter((nation) => nation !== 'common'),
.map((row) => { );
const [idString, ...videosRaw] = row.slice(1); const stringsPreInstalled = await readYAMLDVPL<BlitzStrings>(
const id = Number(idString); `${DATA}/${POI.strings}/en.yaml.dvpl`,
const videos = videosRaw );
.map((raw) => new URL(raw).searchParams.get('v')) const stringsCache = await fetch(POI.cachedStrings)
.filter((video) => video !== null); .then((response) => response.text())
return `${id},${videos.join(',')}`; .then((string) => parseYaml(string) as BlitzStrings);
}) const strings = {
.join('\n'); ...stringsPreInstalled,
...stringsCache,
};
const tanks: { id: number; name: string }[] = [];
await Promise.all(
nations.map(async (nation) => {
const tankList = await readXMLDVPL<{ root: VehicleDefinitionList }>(
`${DATA}/${POI.vehicleDefinitions}/${nation}/list.xml.dvpl`,
);
await Promise.all(
Object.entries(tankList.root).map(async ([tankKey, tank]) => {
if (botPattern.test(tankKey)) return null;
const id = toUniqueId(nation, tank.id);
const name =
(tank.shortUserString
? strings[tank.shortUserString]
: undefined) ?? strings[tank.userString];
tanks.push({ id, name });
}),
);
}),
);
let content = '';
let done = 0;
for (const tank of tanks) {
const results = await Promise.all(
youtubers.map((channel) => search(channel, tank.name)),
).then((results) => results.filter((result) => result !== null));
if (results.length === 0) continue;
const line = `${tank.id},${results.join(',')}`;
content += `${line}\n`;
console.log(`${++done} / ${tanks.length} done`, tank.name, line);
await new Promise((resolve) =>
setTimeout(resolve, (1000 / 5) * youtubers.length),
);
}
console.log(content);
await commitAssets( await commitAssets(
'videos', 'videos',
[ [{ content, encoding: 'utf-8', path: 'definitions/videos.csv' }],
{
content,
encoding: 'utf-8',
path: 'definitions/videos.csv',
},
],
production, production,
); );
} }
@@ -1,5 +1,5 @@
import { PlusIcon } from '@radix-ui/react-icons'; import { InfoCircledIcon, PlusIcon } from '@radix-ui/react-icons';
import { Card, Flex, Heading, Link, Text } from '@radix-ui/themes'; import { Callout, Card, Flex, Heading, Link, Text } from '@radix-ui/themes';
import { use } from 'react'; import { use } from 'react';
import { videoDefinitions } from '../../../../../core/blitzkrieg/videos'; import { videoDefinitions } from '../../../../../core/blitzkrieg/videos';
import { useFullScreen } from '../../../../../hooks/useFullScreen'; import { useFullScreen } from '../../../../../hooks/useFullScreen';
@@ -7,14 +7,14 @@ import { useDuel } from '../../../../../stores/duel';
export function Videos() { export function Videos() {
const isFullScreen = useFullScreen(); const isFullScreen = useFullScreen();
const id = useDuel((state) => state.protagonist!.tank.id); const tank = useDuel((state) => state.protagonist!.tank);
const awaitedVideoDefinitions = use(videoDefinitions); const awaitedVideoDefinitions = use(videoDefinitions);
const videos = awaitedVideoDefinitions[id] ?? []; const videos = awaitedVideoDefinitions[tank.id] ?? [];
if (isFullScreen) return null; if (isFullScreen) return null;
return ( return (
<Flex direction="column" gap="2" mt="4"> <Flex direction="column" gap="2" mt="4" align="start">
<Heading size="6">Review videos</Heading> <Heading size="6">Review videos</Heading>
<Flex gap="3" wrap="wrap"> <Flex gap="3" wrap="wrap">
@@ -59,6 +59,16 @@ export function Videos() {
</Card> </Card>
</Link> </Link>
</Flex> </Flex>
<Callout.Root>
<Callout.Icon>
<InfoCircledIcon />
</Callout.Icon>
<Callout.Text>
Some YouTubers may not have videos on the {tank.name} leading to
off-topic videos.
</Callout.Text>
</Callout.Root>
</Flex> </Flex>
); );
} }