From 90f74754e33c012fe07f442c729c8d5b3275f4b7 Mon Sep 17 00:00:00 2001 From: Petr Vecera Date: Mon, 28 Oct 2024 22:05:01 +0100 Subject: [PATCH] Ad replay download (#633) --- __tests__/src/coh3/helpers.test.ts | 34 + .../processed-match-4v4-leavers.json | 739 ++++++++++++++++++ config.ts | 1 + .../matches-table/download-replay.tsx | 85 ++ .../player-recent-matches-tab.tsx | 27 +- src/apis/coh3stats-api.ts | 65 ++ src/coh3/coh3-types.ts | 1 + src/coh3/helpers.ts | 18 + 8 files changed, 958 insertions(+), 12 deletions(-) create mode 100644 __tests__/test-assets/processed-match-4v4-leavers.json create mode 100644 screens/players/tabs/recent-matches-tab/matches-table/download-replay.tsx diff --git a/__tests__/src/coh3/helpers.test.ts b/__tests__/src/coh3/helpers.test.ts index 4ccaa7ac..23cde2ee 100644 --- a/__tests__/src/coh3/helpers.test.ts +++ b/__tests__/src/coh3/helpers.test.ts @@ -3,9 +3,11 @@ import { getMatchDuration, getMatchDurationGameTime, getMatchPlayersByFaction, + getMatchURlsWithoutLeavers, } from "../../../src/coh3/helpers"; import { PlayerRanks } from "../../../src/coh3/coh3-data"; import { PlayerReport } from "../../../src/coh3/coh3-types"; +import pm4v4Leavers from "../../test-assets/processed-match-4v4-leavers.json"; describe("getMatchDurationGameTime", () => { test("calculates the game duration", () => { @@ -299,3 +301,35 @@ describe("calculatePlayerTier", () => { expect(calculatePlayerTier(1, 1601)).toBe(PlayerRanks.CHALLENGER_1); }); }); + +describe("getMatchURlsWithoutLeavers", () => { + test("should remove the leavers", () => { + const results = getMatchURlsWithoutLeavers(pm4v4Leavers); + expect(results).toEqual([ + { + profile_id: 4566, + key: "7944879acfd6086891d35172e7297e51aff310bf760ac68647c8a30de852d994", + }, + { + profile_id: 370468, + key: "177e19d7be3b510e02b6d8b64fc1c1ddf8ac92a91880948f570b465180da9a3e", + }, + { + profile_id: 234442, + key: "b9e91a67c12b753d0e63fa7d0e8b9abe8dc2da935b3f5a2a3e10f11039a9e8ad", + }, + { + profile_id: 53301, + key: "afb40c2fee136d79a50f020930242350a1a56eb2f62ea1cdad65f77f3f73c7da", + }, + { + profile_id: 292098, + key: "487aff730a1bab4eedca5ffbe1480e265f4064fcfc179dcb1dd915dd4fdedb14", + }, + { + profile_id: 45243, + key: "c2a0344395dcdac7820361ff5ca6a4c315701b4ddd6eb3c10267502e24ae497", + }, + ]); + }); +}); diff --git a/__tests__/test-assets/processed-match-4v4-leavers.json b/__tests__/test-assets/processed-match-4v4-leavers.json new file mode 100644 index 00000000..d562be93 --- /dev/null +++ b/__tests__/test-assets/processed-match-4v4-leavers.json @@ -0,0 +1,739 @@ +{ + "id": 32044730, + "creator_profile_id": 234442, + "mapname": "winter_line_8p_mkii", + "maxplayers": 16, + "matchtype_id": 23, + "description": "AUTOMATCH", + "startgametime": 1729262264, + "completiontime": 1729262619, + "matchhistoryreportresults": [ + { + "profile_id": 234442, + "resulttype": 0, + "teamid": 0, + "race_id": 137123, + "counters": { + "abil": 8, + "addonkill": 0, + "blost": 0, + "bprod": 9, + "cabil": 3, + "cflags": 0, + "cpearn": 0, + "dmgdone": 13, + "edeaths": 0, + "ekills": 0, + "elitekill": 0, + "erein": 0, + "gammaspnt": 0, + "gt": 327, + "inactperiod": 19, + "lowintperiod": 0, + "objdmh": 0, + "pcap": 3, + "plost": 3, + "popmax": 0, + "powearn": 0, + "powmax": 0, + "powspnt": 0, + "precap": 0, + "reqearn": 0, + "reqmax": 0, + "reqspnt": 0, + "sqkill": 0, + "sqlost": 0, + "sqprod": 7, + "structdmg": 0, + "svetrank": 0, + "svetxp": 0, + "totalcmds": 613, + "unitprod": 34, + "upg": 55, + "vabnd": 0, + "vcap": 0, + "vkill": 0, + "vlost": 0, + "vp0": 0, + "vp1": 0, + "vprod": 0, + "vvetrank": 0, + "vvetxp": 0, + "wpnpu": 0 + }, + "profile": { + "profile_id": 234442, + "name": "/steam/76561199242888174", + "alias": "ikire", + "personal_statgroup_id": 109842, + "xp": 28901, + "level": 28901, + "leaderboardregion_id": 2074437, + "country": "cn" + }, + "matchhistorymember": { + "statgroup_id": 109842, + "wins": 350, + "losses": 263, + "streak": -2, + "arbitration": 1, + "outcome": 0, + "oldrating": 1668, + "newrating": 1664, + "reporttype": 1 + } + }, + { + "profile_id": 370468, + "resulttype": 1, + "teamid": 1, + "race_id": 203852, + "counters": { + "abil": 4, + "addonkill": 4, + "blost": 0, + "bprod": 5, + "cabil": 1, + "cflags": 0, + "cpearn": 0, + "dmgdone": 405, + "edeaths": 1, + "ekills": 6, + "elitekill": 2, + "erein": 0, + "gammaspnt": 0, + "gt": 327, + "inactperiod": 27, + "lowintperiod": 0, + "objdmh": 0, + "pcap": 8, + "plost": 0, + "popmax": 0, + "powearn": 0, + "powmax": 0, + "powspnt": 0, + "precap": 3, + "reqearn": 0, + "reqmax": 0, + "reqspnt": 0, + "sqkill": 1, + "sqlost": 0, + "sqprod": 4, + "structdmg": 0, + "svetrank": 0, + "svetxp": 0, + "totalcmds": 180, + "unitprod": 20, + "upg": 11, + "vabnd": 0, + "vcap": 0, + "vkill": 0, + "vlost": 0, + "vp0": 0, + "vp1": 0, + "vprod": 0, + "vvetrank": 0, + "vvetxp": 0, + "wpnpu": 0 + }, + "profile": { + "profile_id": 370468, + "name": "/steam/76561198079282861", + "alias": "既定之天命", + "personal_statgroup_id": 267511, + "xp": 27591, + "level": 27591, + "leaderboardregion_id": 2074437, + "country": "cn" + }, + "matchhistorymember": { + "statgroup_id": 267511, + "wins": 714, + "losses": 630, + "streak": 4, + "arbitration": 1, + "outcome": 1, + "oldrating": 1623, + "newrating": 1627, + "reporttype": 1 + } + }, + { + "profile_id": 897906, + "resulttype": 0, + "teamid": 0, + "race_id": 137123, + "counters": { + "abil": 1, + "addonkill": 0, + "blost": 0, + "bprod": 3, + "cabil": 0, + "cflags": 0, + "cpearn": 0, + "dmgdone": 0, + "edeaths": 0, + "ekills": 0, + "elitekill": 0, + "erein": 0, + "gammaspnt": 0, + "gt": 7, + "inactperiod": 7, + "lowintperiod": 0, + "objdmh": 0, + "pcap": 0, + "plost": 0, + "popmax": 0, + "powearn": 0, + "powmax": 0, + "powspnt": 0, + "precap": 0, + "reqearn": 0, + "reqmax": 0, + "reqspnt": 0, + "sqkill": 0, + "sqlost": 0, + "sqprod": 0, + "structdmg": 0, + "svetrank": 0, + "svetxp": 0, + "totalcmds": 1, + "unitprod": 0, + "upg": 1, + "vabnd": 0, + "vcap": 0, + "vkill": 0, + "vlost": 0, + "vp0": 0, + "vp1": 0, + "vprod": 0, + "vvetrank": 0, + "vvetxp": 0, + "wpnpu": 0 + }, + "profile": { + "profile_id": 897906, + "name": "/steam/76561198195037237", + "alias": "aerafield", + "personal_statgroup_id": 643933, + "xp": 2211, + "level": 2211, + "leaderboardregion_id": 2074389, + "country": "no" + }, + "matchhistorymember": { + "statgroup_id": 643933, + "wins": 17, + "losses": 7, + "streak": -3, + "arbitration": 2, + "outcome": 0, + "oldrating": 1343, + "newrating": 1339, + "reporttype": 3 + } + }, + { + "profile_id": 4566, + "resulttype": 1, + "teamid": 1, + "race_id": 203852, + "counters": { + "abil": 14, + "addonkill": 9, + "blost": 0, + "bprod": 5, + "cabil": 0, + "cflags": 0, + "cpearn": 1, + "dmgdone": 557, + "edeaths": 2, + "ekills": 9, + "elitekill": 0, + "erein": 0, + "gammaspnt": 0, + "gt": 327, + "inactperiod": 20, + "lowintperiod": 0, + "objdmh": 0, + "pcap": 7, + "plost": 0, + "popmax": 0, + "powearn": 0, + "powmax": 0, + "powspnt": 0, + "precap": 2, + "reqearn": 0, + "reqmax": 0, + "reqspnt": 0, + "sqkill": 0, + "sqlost": 0, + "sqprod": 4, + "structdmg": 0, + "svetrank": 0, + "svetxp": 0, + "totalcmds": 371, + "unitprod": 15, + "upg": 28, + "vabnd": 0, + "vcap": 0, + "vkill": 0, + "vlost": 0, + "vp0": 0, + "vp1": 0, + "vprod": 1, + "vvetrank": 3, + "vvetxp": 2500, + "wpnpu": 0 + }, + "profile": { + "profile_id": 4566, + "name": "/steam/76561198335566486", + "alias": "豪师傅", + "personal_statgroup_id": 113172, + "xp": 36831, + "level": 36831, + "leaderboardregion_id": 2074437, + "country": "cn" + }, + "matchhistorymember": { + "statgroup_id": 113172, + "wins": 367, + "losses": 55, + "streak": 1, + "arbitration": 1, + "outcome": 1, + "oldrating": 2526, + "newrating": 2530, + "reporttype": 1 + } + }, + { + "profile_id": 915526, + "resulttype": 0, + "teamid": 0, + "race_id": 198437, + "counters": { + "abil": 1, + "addonkill": 0, + "blost": 0, + "bprod": 2, + "cabil": 0, + "cflags": 0, + "cpearn": 0, + "dmgdone": 0, + "edeaths": 0, + "ekills": 0, + "elitekill": 0, + "erein": 0, + "gammaspnt": 0, + "gt": 10, + "inactperiod": 6, + "lowintperiod": 0, + "objdmh": 0, + "pcap": 0, + "plost": 0, + "popmax": 0, + "powearn": 0, + "powmax": 0, + "powspnt": 0, + "precap": 0, + "reqearn": 0, + "reqmax": 0, + "reqspnt": 0, + "sqkill": 0, + "sqlost": 0, + "sqprod": 0, + "structdmg": 0, + "svetrank": 0, + "svetxp": 0, + "totalcmds": 7, + "unitprod": 0, + "upg": 1, + "vabnd": 0, + "vcap": 0, + "vkill": 0, + "vlost": 0, + "vp0": 0, + "vp1": 0, + "vprod": 0, + "vvetrank": 0, + "vvetxp": 0, + "wpnpu": 0 + }, + "profile": { + "profile_id": 915526, + "name": "/steam/76561199790990092", + "alias": "A Concept of an RTS", + "personal_statgroup_id": 659072, + "xp": 1071, + "level": 1071, + "leaderboardregion_id": 2074389, + "country": "fi" + }, + "matchhistorymember": { + "statgroup_id": 659072, + "wins": 7, + "losses": 3, + "streak": -3, + "arbitration": 2, + "outcome": 0, + "oldrating": 1345, + "newrating": 1332, + "reporttype": 3 + } + }, + { + "profile_id": 45243, + "resulttype": 1, + "teamid": 1, + "race_id": 203852, + "counters": { + "abil": 4, + "addonkill": 14, + "blost": 0, + "bprod": 6, + "cabil": 0, + "cflags": 0, + "cpearn": 2, + "dmgdone": 818, + "edeaths": 7, + "ekills": 16, + "elitekill": 2, + "erein": 6, + "gammaspnt": 0, + "gt": 327, + "inactperiod": 11, + "lowintperiod": 0, + "objdmh": 0, + "pcap": 3, + "plost": 0, + "popmax": 0, + "powearn": 0, + "powmax": 0, + "powspnt": 0, + "precap": 1, + "reqearn": 0, + "reqmax": 0, + "reqspnt": 0, + "sqkill": 1, + "sqlost": 0, + "sqprod": 4, + "structdmg": 0, + "svetrank": 0, + "svetxp": 0, + "totalcmds": 224, + "unitprod": 20, + "upg": 12, + "vabnd": 0, + "vcap": 0, + "vkill": 0, + "vlost": 0, + "vp0": 0, + "vp1": 0, + "vprod": 0, + "vvetrank": 1, + "vvetxp": 900, + "wpnpu": 0 + }, + "profile": { + "profile_id": 45243, + "name": "/steam/76561199077532995", + "alias": "乌拉", + "personal_statgroup_id": 102581, + "xp": 25561, + "level": 25561, + "leaderboardregion_id": 2074437, + "country": "cn" + }, + "matchhistorymember": { + "statgroup_id": 102581, + "wins": 88, + "losses": 50, + "streak": 5, + "arbitration": 1, + "outcome": 1, + "oldrating": 1505, + "newrating": 1509, + "reporttype": 1 + } + }, + { + "profile_id": 53301, + "resulttype": 0, + "teamid": 0, + "race_id": 137123, + "counters": { + "abil": 7, + "addonkill": 1, + "blost": 1, + "bprod": 4, + "cabil": 5, + "cflags": 0, + "cpearn": 0, + "dmgdone": 244, + "edeaths": 9, + "ekills": 1, + "elitekill": 0, + "erein": 8, + "gammaspnt": 0, + "gt": 327, + "inactperiod": 47, + "lowintperiod": 0, + "objdmh": 0, + "pcap": 3, + "plost": 2, + "popmax": 0, + "powearn": 0, + "powmax": 0, + "powspnt": 0, + "precap": 0, + "reqearn": 0, + "reqmax": 0, + "reqspnt": 0, + "sqkill": 0, + "sqlost": 0, + "sqprod": 5, + "structdmg": 0, + "svetrank": 0, + "svetxp": 0, + "totalcmds": 180, + "unitprod": 28, + "upg": 18, + "vabnd": 0, + "vcap": 0, + "vkill": 0, + "vlost": 0, + "vp0": 0, + "vp1": 0, + "vprod": 0, + "vvetrank": 0, + "vvetxp": 0, + "wpnpu": 0 + }, + "profile": { + "profile_id": 53301, + "name": "/steam/76561198034764198", + "alias": "Lupin", + "personal_statgroup_id": 283330, + "xp": 17721, + "level": 17721, + "leaderboardregion_id": 2074437, + "country": "kr" + }, + "matchhistorymember": { + "statgroup_id": 283330, + "wins": 1293, + "losses": 1014, + "streak": -2, + "arbitration": 1, + "outcome": 0, + "oldrating": 1519, + "newrating": 1515, + "reporttype": 1 + } + }, + { + "profile_id": 292098, + "resulttype": 1, + "teamid": 1, + "race_id": 129494, + "counters": { + "abil": 4, + "addonkill": 13, + "blost": 0, + "bprod": 5, + "cabil": 1, + "cflags": 0, + "cpearn": 0, + "dmgdone": 725, + "edeaths": 3, + "ekills": 9, + "elitekill": -4, + "erein": 0, + "gammaspnt": 0, + "gt": 327, + "inactperiod": 25, + "lowintperiod": 0, + "objdmh": 0, + "pcap": 7, + "plost": 0, + "popmax": 0, + "powearn": 0, + "powmax": 0, + "powspnt": 0, + "precap": 2, + "reqearn": 0, + "reqmax": 0, + "reqspnt": 0, + "sqkill": 1, + "sqlost": 0, + "sqprod": 4, + "structdmg": 0, + "svetrank": 0, + "svetxp": 0, + "totalcmds": 227, + "unitprod": 23, + "upg": 8, + "vabnd": 0, + "vcap": 0, + "vkill": 0, + "vlost": 0, + "vp0": 0, + "vp1": 0, + "vprod": 0, + "vvetrank": 0, + "vvetxp": 0, + "wpnpu": 4 + }, + "profile": { + "profile_id": 292098, + "name": "/steam/76561199290562467", + "alias": "小鱼@", + "personal_statgroup_id": 179164, + "xp": 15341, + "level": 15341, + "leaderboardregion_id": 2074437, + "country": "cn" + }, + "matchhistorymember": { + "statgroup_id": 179164, + "wins": 121, + "losses": 75, + "streak": 2, + "arbitration": 1, + "outcome": 1, + "oldrating": 1441, + "newrating": 1445, + "reporttype": 1 + } + } + ], + "matchhistoryitems": [ + { + "profile_id": 4566, + "itemdefinition_id": 452979, + "itemlocation_id": 2 + }, + { + "profile_id": 4566, + "itemdefinition_id": 451861, + "itemlocation_id": 6 + }, + { + "profile_id": 45243, + "itemdefinition_id": 452979, + "itemlocation_id": 2 + }, + { + "profile_id": 45243, + "itemdefinition_id": 452655, + "itemlocation_id": 6 + }, + { + "profile_id": 53301, + "itemdefinition_id": 451845, + "itemlocation_id": 2 + }, + { + "profile_id": 53301, + "itemdefinition_id": 451866, + "itemlocation_id": 6 + }, + { + "profile_id": 234442, + "itemdefinition_id": 451845, + "itemlocation_id": 2 + }, + { + "profile_id": 234442, + "itemdefinition_id": 451866, + "itemlocation_id": 6 + }, + { + "profile_id": 292098, + "itemdefinition_id": 451707, + "itemlocation_id": 2 + }, + { + "profile_id": 292098, + "itemdefinition_id": 451861, + "itemlocation_id": 6 + }, + { + "profile_id": 370468, + "itemdefinition_id": 452979, + "itemlocation_id": 2 + }, + { + "profile_id": 370468, + "itemdefinition_id": 452655, + "itemlocation_id": 6 + }, + { + "profile_id": 897906, + "itemdefinition_id": 451845, + "itemlocation_id": 2 + }, + { + "profile_id": 897906, + "itemdefinition_id": 451866, + "itemlocation_id": 6 + }, + { + "profile_id": 915526, + "itemdefinition_id": 452654, + "itemlocation_id": 2 + }, + { + "profile_id": 915526, + "itemdefinition_id": 452655, + "itemlocation_id": 6 + } + ], + "matchurls": [ + { + "profile_id": 897906, + "key": "e3d4a1199c33d503e847d4bce18dc059b1a5a4568a93b22b18759bbf92e3a6d9" + }, + { + "profile_id": 915526, + "key": "d86a891ce21712e49d0ce6754a7fdff06b0090ce27a71efc884185f57fb94e0c" + }, + { + "profile_id": 4566, + "key": "7944879acfd6086891d35172e7297e51aff310bf760ac68647c8a30de852d994" + }, + { + "profile_id": 370468, + "key": "177e19d7be3b510e02b6d8b64fc1c1ddf8ac92a91880948f570b465180da9a3e" + }, + { + "profile_id": 234442, + "key": "b9e91a67c12b753d0e63fa7d0e8b9abe8dc2da935b3f5a2a3e10f11039a9e8ad" + }, + { + "profile_id": 53301, + "key": "afb40c2fee136d79a50f020930242350a1a56eb2f62ea1cdad65f77f3f73c7da" + }, + { + "profile_id": 292098, + "key": "487aff730a1bab4eedca5ffbe1480e265f4064fcfc179dcb1dd915dd4fdedb14" + }, + { + "profile_id": 45243, + "key": "c2a0344395dcdac7820361ff5ca6a4c315701b4ddd6eb3c10267502e24ae497" + } + ], + "platform": "steam", + "profile_ids": [ + 234442, + 370468, + 897906, + 4566, + 915526, + 45243, + 53301, + 292098 + ] +} diff --git a/config.ts b/config.ts index 6bb7923e..6aa6e3c8 100644 --- a/config.ts +++ b/config.ts @@ -354,6 +354,7 @@ const config = { statsPatchSelector, defaultStatsPatchSelector, mainContainerSize: 1310, + BASE_REPLAY_STORAGE_URL: "https://replays.coh3stats.com", }; export default config; diff --git a/screens/players/tabs/recent-matches-tab/matches-table/download-replay.tsx b/screens/players/tabs/recent-matches-tab/matches-table/download-replay.tsx new file mode 100644 index 00000000..1c88c1e3 --- /dev/null +++ b/screens/players/tabs/recent-matches-tab/matches-table/download-replay.tsx @@ -0,0 +1,85 @@ +import { ProcessedMatch } from "../../../../../src/coh3/coh3-types"; +import React from "react"; +import { Button, Tooltip } from "@mantine/core"; +import { IconAlertCircle, IconDownload } from "@tabler/icons-react"; +import { generateReplayUrl } from "../../../../../src/apis/coh3stats-api"; +import { matchTypesAsObject } from "../../../../../src/coh3/coh3-data"; +import dayjs from "dayjs"; + +const DownloadReplayButton = ({ match }: { match: ProcessedMatch }) => { + const [loading, setLoading] = React.useState(false); + const [error, setError] = React.useState(null); + + const downloadReplay = async () => { + setLoading(true); + try { + const replayStatus = await generateReplayUrl(match); + + if (replayStatus.status === "success" && replayStatus.url) { + // Fetch the file content + const response = await fetch(replayStatus.url); + const blob = await response.blob(); + + // Create a local URL for the blob + const blobUrl = window.URL.createObjectURL(blob); + + // Trigger browser download of the file + const matchType = + matchTypesAsObject[match.matchtype_id as number]["localizedName"] || + matchTypesAsObject[match.matchtype_id as number]["name"] || + ""; + + const link = document.createElement("a"); + link.href = blobUrl; + link.download = `${match.id}-${matchType.toLowerCase().replace(/\s+/g, "")}-${match.mapname.replace(/\s+/g, "_")}-${dayjs(match.completiontime * 1000).format("DD-MMM-YY")}.rec`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // Clean up the blob URL + window.URL.revokeObjectURL(blobUrl); + } else if (replayStatus.status === "error") { + setError(replayStatus.message); + } + } catch (e) { + console.error(e); + setError("There was an error downloading the replay. Old games might not have replays."); + } finally { + setLoading(false); + } + }; + + if (error) { + return ( + + + + ); + } + + return ( + + + + ); +}; + +export default DownloadReplayButton; diff --git a/screens/players/tabs/recent-matches-tab/player-recent-matches-tab.tsx b/screens/players/tabs/recent-matches-tab/player-recent-matches-tab.tsx index e7923403..ae8979ef 100644 --- a/screens/players/tabs/recent-matches-tab/player-recent-matches-tab.tsx +++ b/screens/players/tabs/recent-matches-tab/player-recent-matches-tab.tsx @@ -9,7 +9,6 @@ import { Tooltip, Center, Flex, - ActionIcon, } from "@mantine/core"; import { DataTable, DataTableSortStatus } from "mantine-datatable"; import React from "react"; @@ -31,6 +30,7 @@ import RenderMap from "./matches-table/render-map"; import classes from "./matches-table.module.css"; import MatchDetailDrawer from "./match-detail-drawer"; +import DownloadReplayButton from "./matches-table/download-replay"; /** * Timeago is causing issues with SSR, move to client side @@ -400,17 +400,20 @@ const PlayerRecentMatchesTab = ({ textAlign: "center", render: (record) => { return ( - { - setSelectedMatchRecord(record as unknown as ProcessedMatch); - open(); - }} - > - - + + + + ); }, }, diff --git a/src/apis/coh3stats-api.ts b/src/apis/coh3stats-api.ts index c7989bd8..a60af0f5 100644 --- a/src/apis/coh3stats-api.ts +++ b/src/apis/coh3stats-api.ts @@ -16,6 +16,7 @@ import { } from "../analysis-types"; import { cleanXForwardedFor, parseFirstIPFromString } from "../utils"; import { logger } from "../logger"; +import { getMatchURlsWithoutLeavers } from "../coh3/helpers"; export const GET_ANALYSIS_STATS = "v10"; @@ -61,6 +62,17 @@ const getGlobalAchievementsUrl = (cache_proxy = true) => { : encodeURI(`${config.BASE_CLOUD_FUNCTIONS_URL}${path}`); }; +const getReplayUrl = (matchID: string | number) => { + return encodeURI(`${config.BASE_REPLAY_STORAGE_URL}/${matchID}.rec`); +}; + +const setReplayFileUrl = () => { + return encodeURI( + // This will be in the browser / we don't want to touch our GCP directly without proxy + `${config.BASED_CLOUD_FUNCTIONS_PROXY_URL}/setReplayFileHttp`, + ); +}; + const getStatsUrl = ( startDate: number, endDate: number | "now" = "now", @@ -366,6 +378,58 @@ const triggerPlayerNemesisAliasesUpdate = async (playerID: string | number) => { return await response.json(); }; +const generateReplayUrl = async (matchObject: ProcessedMatch) => { + const r2url = getReplayUrl(matchObject.id); + + let response; + + try { + response = await fetch(r2url, { method: "HEAD" }); + } catch (e) {} + if (response && response.status === 200) { + return { + url: r2url, + status: "success", + }; + } else { + const response = await fetch(setReplayFileUrl(), { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + matchID: matchObject.id, + replayURLs: JSON.stringify( + getMatchURlsWithoutLeavers(matchObject).map((data) => { + return { + profile_id: data.profile_id, + replay_id: data.key, + }; + }), + ), + }), + }); + + const parsedResponse = await response.json(); + if (parsedResponse.status === "success") { + return { + url: r2url, + status: "success", + }; + } else if (parsedResponse.status === "error") { + return { + url: null, + status: "error", + message: parsedResponse.message, + }; + } + + console.error(parsedResponse); + + throw new Error("Error generating replay url"); + } +}; + const _fetchFirstXLinesOfLeaderBoards = async (url: string, amountOfPlayers: number) => { const response = await fetch(url); @@ -465,4 +529,5 @@ export { getYouTubeVideosHttp, triggerPlayerNemesisAliasesUpdate, getOldLeaderboardData, + generateReplayUrl, }; diff --git a/src/coh3/coh3-types.ts b/src/coh3/coh3-types.ts index d7a13b12..e9fc42a3 100644 --- a/src/coh3/coh3-types.ts +++ b/src/coh3/coh3-types.ts @@ -212,6 +212,7 @@ export interface ProcessedMatch { matchhistoryreportresults: Array; matchhistoryitems: Array; profile_ids: Array; + matchurls: Array<{ profile_id: number; key: string }>; } export interface TwitchStream { diff --git a/src/coh3/helpers.ts b/src/coh3/helpers.ts index 901a5e12..2e610ced 100644 --- a/src/coh3/helpers.ts +++ b/src/coh3/helpers.ts @@ -3,6 +3,7 @@ import { LaddersDataArrayObject, LaddersDataObject, PlayerReport, + ProcessedMatch, raceType, } from "./coh3-types"; import { isOfficialMap, maps, PlayerRanks, raceIDsAsObject } from "./coh3-data"; @@ -144,6 +145,22 @@ const getMapLocalizedName = (mapName: string) => { } }; +const getMatchURlsWithoutLeavers = (match: ProcessedMatch) => { + // If someone left they have lower game time + const gameTime = match.matchhistoryreportresults.reduce((acc, cur) => { + return acc > cur.counters.gt ? acc : cur.counters.gt; + }, 0); + + // Filter out players that left the game + const profileIDsWithoutLeavers = match.matchhistoryreportresults.filter( + (player) => player.counters.gt === gameTime, + ); + + return match.matchurls.filter((url) => + profileIDsWithoutLeavers.find((player) => player.profile_id === url.profile_id), + ); +}; + export { getFactionSide, findAndMergeStatGroups, @@ -152,4 +169,5 @@ export { getMatchPlayersByFaction, calculatePlayerTier, getMapLocalizedName, + getMatchURlsWithoutLeavers, };