From 9ead3eb6271f19e173cdfdb6088c840561404e23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Sun, 21 Apr 2024 19:40:38 -0500 Subject: [PATCH] feat: automatic videos --- .vscode/settings.json | 1 + scripts/buildAssets/definitions.ts | 8 +- scripts/buildAssets/videos.ts | 107 +++++++++++++----- .../tankopedia/[id]/components/Videos.tsx | 20 +++- 4 files changed, 99 insertions(+), 37 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 5ceb1e2b5..44da9a81a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -164,6 +164,7 @@ "xkdkw", "XMLDVPL", "YAMLDVPL", + "youtubers", "zustand" ], "deno.enable": false, diff --git a/scripts/buildAssets/definitions.ts b/scripts/buildAssets/definitions.ts index ac27aa7c8..ad0e052a2 100644 --- a/scripts/buildAssets/definitions.ts +++ b/scripts/buildAssets/definitions.ts @@ -38,7 +38,7 @@ import { DATA, POI } from './constants'; import { Avatar } from './skillIcons'; import { TankParameters } from './tankIcons'; -interface Strings { +export interface BlitzStrings { [key: string]: string; } type ShellKind = @@ -334,7 +334,7 @@ const blitzShellKindToBLitzkrieg: Record = { const missingStrings: Record = { '#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) { console.log('Building definitions...'); @@ -361,12 +361,12 @@ export async function definitions(production: boolean) { (nations) => nations.filter((nation) => nation !== 'common'), ); const tankStringIdMap: Record = {}; - const stringsPreInstalled = await readYAMLDVPL( + const stringsPreInstalled = await readYAMLDVPL( `${DATA}/${POI.strings}/en.yaml.dvpl`, ); const stringsCache = await fetch(POI.cachedStrings) .then((response) => response.text()) - .then((string) => parseYaml(string) as Strings); + .then((string) => parseYaml(string) as BlitzStrings); const optionalDevices = await readXMLDVPL<{ root: OptionalDevices }>( `${DATA}/${POI.optionalDevices}.dvpl`, ); diff --git a/scripts/buildAssets/videos.ts b/scripts/buildAssets/videos.ts index 9126027a9..5fdcd6fd7 100644 --- a/scripts/buildAssets/videos.ts +++ b/scripts/buildAssets/videos.ts @@ -1,45 +1,96 @@ +import { readdir } from 'fs/promises'; 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 { DATA, POI } from './constants'; +import { BlitzStrings, botPattern, VehicleDefinitionList } from './definitions'; + +const youtubers = [ + 'UCKBYXp4Xn2I2tL1UL4fpbhw', // droodles + 'UCrQ-Dy8lVsm11u8pCJ8W7Tw', // fatness +]; export async function videos(production: boolean) { console.log('Building videos...'); const auth = await google.auth.getClient({ - scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly'], - }); - const sheets = google.sheets({ version: 'v4', auth }); - const response = await sheets.spreadsheets.values.get({ - spreadsheetId: env.VIDEOS_SHEET, - range: 'Sheet1!A:Z', + scopes: ['https://www.googleapis.com/auth/youtube.readonly'], }); + const youtube = google.youtube({ version: 'v3', auth }); - if (!response.data.values) { - console.warn('Google returned no data; skipping videos...'); - return; + async function search(channel: string, search: string) { + ( + await youtube.search.list({ + channelId: channel, + maxResults: 1, + part: ['snippet'], + q: search, + }) + ).data.items?.at(0)?.id ?? null; } - const content = response.data.values - .slice(1) - .map((row) => { - const [idString, ...videosRaw] = row.slice(1); - const id = Number(idString); - const videos = videosRaw - .map((raw) => new URL(raw).searchParams.get('v')) - .filter((video) => video !== null); - return `${id},${videos.join(',')}`; - }) - .join('\n'); + const nations = await readdir(`${DATA}/${POI.vehicleDefinitions}`).then( + (nations) => nations.filter((nation) => nation !== 'common'), + ); + const stringsPreInstalled = await readYAMLDVPL( + `${DATA}/${POI.strings}/en.yaml.dvpl`, + ); + const stringsCache = await fetch(POI.cachedStrings) + .then((response) => response.text()) + .then((string) => parseYaml(string) as BlitzStrings); + const strings = { + ...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( 'videos', - [ - { - content, - encoding: 'utf-8', - path: 'definitions/videos.csv', - }, - ], + [{ content, encoding: 'utf-8', path: 'definitions/videos.csv' }], production, ); } diff --git a/src/app/tools/tankopedia/[id]/components/Videos.tsx b/src/app/tools/tankopedia/[id]/components/Videos.tsx index 1d9bb63c6..ab351f6bb 100644 --- a/src/app/tools/tankopedia/[id]/components/Videos.tsx +++ b/src/app/tools/tankopedia/[id]/components/Videos.tsx @@ -1,5 +1,5 @@ -import { PlusIcon } from '@radix-ui/react-icons'; -import { Card, Flex, Heading, Link, Text } from '@radix-ui/themes'; +import { InfoCircledIcon, PlusIcon } from '@radix-ui/react-icons'; +import { Callout, Card, Flex, Heading, Link, Text } from '@radix-ui/themes'; import { use } from 'react'; import { videoDefinitions } from '../../../../../core/blitzkrieg/videos'; import { useFullScreen } from '../../../../../hooks/useFullScreen'; @@ -7,14 +7,14 @@ import { useDuel } from '../../../../../stores/duel'; export function Videos() { const isFullScreen = useFullScreen(); - const id = useDuel((state) => state.protagonist!.tank.id); + const tank = useDuel((state) => state.protagonist!.tank); const awaitedVideoDefinitions = use(videoDefinitions); - const videos = awaitedVideoDefinitions[id] ?? []; + const videos = awaitedVideoDefinitions[tank.id] ?? []; if (isFullScreen) return null; return ( - + Review videos @@ -59,6 +59,16 @@ export function Videos() { + + + + + + + Some YouTubers may not have videos on the {tank.name} leading to + off-topic videos. + + ); }