Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

optional year for all pitcher/batter commands #50

Merged
merged 1 commit into from
Mar 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions commands/batter.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ module.exports = {
option.setName('player')
.setDescription('An active player\'s name.')
.setRequired(false))
.addIntegerOption(option =>
option.setName('year')
.setDescription('Which season?')
.setRequired(false)
.setMinValue(new Date().getFullYear() - 10)
.setMaxValue(new Date().getFullYear()))
.addStringOption(option =>
option.setName('stat_type')
.setDescription('Regular Season (default), Postseason, or Spring Training?')
Expand Down
8 changes: 7 additions & 1 deletion commands/batter_savant.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ module.exports = {
.addStringOption(option =>
option.setName('player')
.setDescription('An active player\'s name.')
.setRequired(false)),
.setRequired(false))
.addIntegerOption(option =>
option.setName('year')
.setDescription('Which season?')
.setRequired(false)
.setMinValue(new Date().getFullYear() - 10)
.setMaxValue(new Date().getFullYear())),
async execute (interaction) {
try {
await interactionHandlers.batterSavantHandler(interaction);
Expand Down
6 changes: 6 additions & 0 deletions commands/pitcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ module.exports = {
option.setName('player')
.setDescription('An active player\'s name.')
.setRequired(false))
.addIntegerOption(option =>
option.setName('year')
.setDescription('Which season?')
.setRequired(false)
.setMinValue(new Date().getFullYear() - 10)
.setMaxValue(new Date().getFullYear()))
.addStringOption(option =>
option.setName('stat_type')
.setDescription('Regular Season (default), Postseason, or Spring Training?')
Expand Down
8 changes: 7 additions & 1 deletion commands/pitcher_savant.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ module.exports = {
.addStringOption(option =>
option.setName('player')
.setDescription('An active player\'s name.')
.setRequired(false)),
.setRequired(false))
.addIntegerOption(option =>
option.setName('year')
.setDescription('Which season?')
.setRequired(false)
.setMinValue(new Date().getFullYear() - 10)
.setMaxValue(new Date().getFullYear())),
async execute (interaction) {
try {
await interactionHandlers.pitcherSavantHandler(interaction);
Expand Down
30 changes: 15 additions & 15 deletions modules/MLB-API-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ const endpoints = {
LOGGER.debug('https://statsapi.mlb.com/api/v1/schedule?hydrate=lineups&sportId=1&gamePk=' + gamePk + '&teamId=' + teamId);
return 'https://statsapi.mlb.com/api/v1/schedule?hydrate=lineups&sportId=1&gamePk=' + gamePk + '&teamId=' + teamId;
},
hitter: (personId, statType) => {
LOGGER.debug(`https://statsapi.mlb.com/api/v1/people?personIds=${personId}&hydrate=stats(type=[season,statSplits,lastXGames],group=hitting,gameType=${statType},sitCodes=[vl,vr,risp],limit=7)`);
return `https://statsapi.mlb.com/api/v1/people?personIds=${personId}&hydrate=stats(type=[season,statSplits,lastXGames],group=hitting,gameType=${statType},sitCodes=[vl,vr,risp],limit=7)`;
hitter: (personId, statType, season) => {
LOGGER.debug(`https://statsapi.mlb.com/api/v1/people?personIds=${personId}&hydrate=stats(type=[season,statSplits,lastXGames],group=hitting,gameType=${statType},sitCodes=[vl,vr,risp],limit=7,season=${season})`);
return `https://statsapi.mlb.com/api/v1/people?personIds=${personId}&hydrate=stats(type=[season,statSplits,lastXGames],group=hitting,gameType=${statType},sitCodes=[vl,vr,risp],limit=7,season=${season})`;
},
pitcher: (personId, lastXGamesLimit, statType) => {
LOGGER.debug(`https://statsapi.mlb.com/api/v1/people?personIds=${personId}&hydrate=stats(type=[season,lastXGames,sabermetrics,seasonAdvanced,expectedStatistics],groups=pitching,limit=${lastXGamesLimit},gameType=${statType})`);
return `https://statsapi.mlb.com/api/v1/people?personIds=${personId}&hydrate=stats(type=[season,lastXGames,sabermetrics,seasonAdvanced,expectedStatistics],groups=pitching,limit=${lastXGamesLimit},gameType=${statType})`;
pitcher: (personId, lastXGamesLimit, statType, season) => {
LOGGER.debug(`https://statsapi.mlb.com/api/v1/people?personIds=${personId}&hydrate=stats(type=[season,lastXGames,sabermetrics,seasonAdvanced,expectedStatistics],groups=pitching,limit=${lastXGamesLimit},gameType=${statType},season=${season})`);
return `https://statsapi.mlb.com/api/v1/people?personIds=${personId}&hydrate=stats(type=[season,lastXGames,sabermetrics,seasonAdvanced,expectedStatistics],groups=pitching,limit=${lastXGamesLimit},gameType=${statType},season=${season})`;
},
liveFeed: (gamePk, fields = []) => {
LOGGER.debug('https://statsapi.mlb.com/api/v1.1/game/' + gamePk + '/feed/live' + (fields.length > 0 ? '?fields=' + fields.join() : ''));
Expand Down Expand Up @@ -70,11 +70,11 @@ const endpoints = {
LOGGER.debug('https://ws.statsapi.mlb.com/api/v1.1/game/' + gamePk + '/feed/live?fields=gameData,players,boxscoreName');
return 'https://ws.statsapi.mlb.com/api/v1.1/game/' + gamePk + '/feed/live?fields=gameData,players,boxscoreName';
},
savantPitchData: (personId) => {
savantPitchData: (personId, season) => {
LOGGER.debug('https://baseballsavant.mlb.com/player-services/statcast-pitches-breakdown?playerId=' + personId +
'&position=1&hand=&pitchBreakdown=pitches&timeFrame=yearly&pitchType=&count=&updatePitches=true&gameType=RP');
`&position=1&hand=&pitchBreakdown=pitches&timeFrame=yearly&pitchType=&count=&updatePitches=true&gameType=RP&season=${season}`);
return 'https://baseballsavant.mlb.com/player-services/statcast-pitches-breakdown?playerId=' + personId +
'&position=1&hand=&pitchBreakdown=pitches&timeFrame=yearly&pitchType=&count=&updatePitches=true&gameType=RP';
`&position=1&hand=&pitchBreakdown=pitches&timeFrame=yearly&pitchType=&count=&updatePitches=true&gameType=RP&season=${season}`;
},
savantPage: (personId, type) => {
LOGGER.debug(`https://baseballsavant.mlb.com/savant-player/${personId}?stats=statcast-r-${type}-mlb`);
Expand Down Expand Up @@ -208,9 +208,9 @@ module.exports = {
linescore: async (gamePk) => {
return (await fetch(endpoints.linescore(gamePk))).json();
},
savantPitchData: async (personId) => {
savantPitchData: async (personId, season) => {
try {
return (await fetch(endpoints.savantPitchData(personId),
return (await fetch(endpoints.savantPitchData(personId, season),
{
signal: AbortSignal.timeout(5000)
}
Expand Down Expand Up @@ -269,14 +269,14 @@ module.exports = {
return {};
}
},
hitter: async (personId, statType) => {
return (await fetch(endpoints.hitter(personId, statType))).json();
hitter: async (personId, statType, season) => {
return (await fetch(endpoints.hitter(personId, statType, season))).json();
},
team: async (teamId) => {
return (await fetch(endpoints.team(teamId))).json();
},
pitcher: async (personId, lastXGamesLimit, statType) => {
return (await fetch(endpoints.pitcher(personId, lastXGamesLimit, statType))).json();
pitcher: async (personId, lastXGamesLimit, statType, season) => {
return (await fetch(endpoints.pitcher(personId, lastXGamesLimit, statType, season))).json();
},
players: async () => {
return (await fetch(endpoints.players())).json();
Expand Down
65 changes: 35 additions & 30 deletions modules/command-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ module.exports = {
table.removeBorder();
return await getScreenshotOfHTMLTables([table]);
},
hydrateProbable: async (probable, statType) => {
hydrateProbable: async (probable, statType, season = (new Date().getFullYear())) => {
const [spot, savant, people] = await Promise.all([
new Promise((resolve, reject) => {
if (probable) {
Expand All @@ -59,10 +59,10 @@ module.exports = {
}
reject(new Error('There was a problem getting the player spot.'));
}),
mlbAPIUtil.savantPitchData(probable),
mlbAPIUtil.savantPitchData(probable, season),
new Promise((resolve, reject) => {
if (probable) {
resolve(mlbAPIUtil.pitcher(probable, 3, statType));
resolve(mlbAPIUtil.pitcher(probable, 3, statType, season));
} else {
resolve(undefined);
}
Expand All @@ -79,7 +79,7 @@ module.exports = {
};
},

hydrateHitter: async (hitter, statType) => {
hydrateHitter: async (hitter, statType, season = new Date().getFullYear()) => {
const [spot, stats] = await Promise.all([
new Promise((resolve, reject) => {
if (hitter) {
Expand All @@ -94,7 +94,7 @@ module.exports = {
}),
new Promise((resolve, reject) => {
if (hitter) {
resolve(mlbAPIUtil.hitter(hitter, statType));
resolve(mlbAPIUtil.hitter(hitter, statType, season));
} else {
resolve(undefined);
}
Expand Down Expand Up @@ -285,21 +285,23 @@ module.exports = {
return (await getScreenshotOfHTMLTables([table]));
},

getStatcastData: (savantText) => {
getStatcastData: (savantText, season) => {
const statcast = /statcast: \[(?<statcast>.+)],/.exec(savantText)?.groups.statcast;
const metricSummaries = /metricSummaryStats: {(?<metricSummaries>.+)},/.exec(savantText)?.groups.metricSummaries;
if (statcast) {
try {
const statcastJSON = JSON.parse('[' + statcast + ']');
const metricSummaryJSON = JSON.parse('{' + metricSummaries + '}');
const mostRecentStatcast = statcastJSON.findLast(set => set.year != null);
const matchingStatcast = season ? statcastJSON.find(set => set.year === season) : statcastJSON.findLast(set => set.year != null);
// object properties are not guaranteed to always be in the same order, so we need to find the most recent year of data
const mostRecentMetricYear = Object.keys(metricSummaryJSON)
.map(k => parseInt(k))
.sort((a, b) => {
return a < b ? 1 : -1;
})[0];
return { mostRecentStatcast, metricSummaryJSON, mostRecentMetricYear };
const matchingMetricYear = season
? Object.keys(metricSummaryJSON).find(k => k === season.toString())
: Object.keys(metricSummaryJSON)
.map(k => parseInt(k))
.sort((a, b) => {
return a < b ? 1 : -1;
})[0];
return { matchingStatcast, metricSummaryJSON, matchingMetricYear };
} catch (e) {
console.error(e);
return {};
Expand Down Expand Up @@ -756,7 +758,7 @@ module.exports = {
return matchingPlayers;
},

getPitcherEmbed: (pitcher, pitcherInfo, isLiveGame, description, statType = 'R', savantMode = false) => {
getPitcherEmbed: (pitcher, pitcherInfo, isLiveGame, description, statType = 'R', savantMode = false, season = undefined) => {
const feed = liveFeed.init(globalCache.values.game.currentLiveFeed);
if (isLiveGame) {
const abbreviations = {
Expand All @@ -774,7 +776,7 @@ module.exports = {
.setDescription('### ' + (pitcherInfo.handedness
? pitcherInfo.handedness + 'HP **'
: '**') + (pitcher.fullName || 'TBD') +
'** (' + abbreviation + `): ${pitcherInfo.pitchingStats.yearOfStats || 'Latest'} ${(() => {
'** (' + abbreviation + `): ${season || pitcherInfo.pitchingStats.yearOfStats || 'Latest'} ${(() => {
if (savantMode) {
return 'Percentile Rankings';
}
Expand Down Expand Up @@ -802,7 +804,7 @@ module.exports = {
const embed = new EmbedBuilder()
.setTitle((pitcherInfo.handedness
? pitcherInfo.handedness + 'HP '
: '') + pitcher.fullName + ` (${globals.TEAMS.find(t => t.id === pitcher.currentTeam.id).abbreviation}): ${pitcherInfo.pitchingStats.yearOfStats || 'Latest'} ${(() => {
: '') + pitcher.fullName + ` (${globals.TEAMS.find(t => t.id === pitcher.currentTeam.id).abbreviation}): ${season || pitcherInfo.pitchingStats.yearOfStats || 'Latest'} ${(() => {
if (savantMode) {
return 'Percentile Rankings';
}
Expand Down Expand Up @@ -831,7 +833,7 @@ module.exports = {
}
},

getBatterEmbed: (batter, batterInfo, isLiveGame, description, statType = 'R', savantMode = false) => {
getBatterEmbed: (batter, batterInfo, isLiveGame, description, statType = 'R', savantMode = false, season = undefined) => {
const feed = liveFeed.init(globalCache.values.game.currentLiveFeed);
let expandedBatter;
if (isLiveGame) {
Expand All @@ -847,7 +849,7 @@ module.exports = {
const inning = feed.inning();
const embed = new EmbedBuilder()
.setTitle(halfInning.toUpperCase() + ' ' + inning + ', ' +
abbreviations.away + ' vs. ' + abbreviations.home + ': Current Batter' + (savantMode ? ': Latest Percentile Rankings' : ''))
abbreviations.away + ' vs. ' + abbreviations.home + ': Current Batter' + (savantMode ? `: ${season || 'Latest'} Percentile Rankings` : ''))
.setDescription(`### ${batter.fullName} (${abbreviation})\n ${expandedBatter.primaryPosition.abbreviation} | Bats ${expandedBatter.batSide.description} ${(description || '')}`)
.setImage('attachment://savant.png')
.setColor((halfInning === 'top'
Expand All @@ -862,7 +864,7 @@ module.exports = {
return embed;
} else {
const embed = new EmbedBuilder()
.setTitle(`${batter.fullName} (${globals.TEAMS.find(team => team.id === batter.currentTeam.id).abbreviation})` + (savantMode ? ': Latest Percentile Rankings' : ''))
.setTitle(`${batter.fullName} (${globals.TEAMS.find(team => team.id === batter.currentTeam.id).abbreviation})` + (savantMode ? `: ${season || 'Latest'} Percentile Rankings` : ''))
.setDescription(`${batter.primaryPosition.abbreviation} | Bats ${batterInfo.stats.batSide.description}`)
.setImage('attachment://savant.png')
.setColor(globals.TEAMS.find(team => team.id === batter.currentTeam.id).primaryColor);
Expand Down Expand Up @@ -1144,20 +1146,23 @@ async function getScreenshotOfSavantTable (savantHTML) {
function buildSavantSection (statCollection, metricSummaries, isPitcher = false) {
const scale = chroma.scale(['#325aa1', '#a8c1c3', '#c91f26']);
const sliderScale = chroma.scale(['#3661ad', '#b4cfd1', '#d8221f']);
statCollection.forEach(stat => {
if (!stat.percentile) {
stat.percentile = calculateRoundedPercentileFromNormalDistribution(
stat.metric,
stat.value,
metricSummaries[stat.metric].avg_metric,
metricSummaries[stat.metric].stddev_metric,
stat.shouldInvert
for (let i = 0; i < statCollection.length; i ++) {
if (!statCollection[i].value) { // some metrics have been added in later years, like Bat Speed. Earlier seasons will have no value.
continue;
}
if (!statCollection[i].percentile) {
statCollection[i].percentile = calculateRoundedPercentileFromNormalDistribution(
statCollection[i].metric,
statCollection[i].value,
metricSummaries[statCollection[i].metric]?.avg_metric,
metricSummaries[statCollection[i].metric]?.stddev_metric,
statCollection[i].shouldInvert
);
stat.isQualified = false;
statCollection[i].isQualified = false;
} else {
stat.isQualified = true;
statCollection[i].isQualified = true;
}
});
}
return statCollection.reduce((acc, value) => acc + (value.value !== null
? `
<div class='savant-stat'>
Expand Down
Loading
Loading