Add commands to list stages, challenges, weapon stats, hero stats, battles and stage schedules from SplatNet 2

This commit is contained in:
Samuel Elliott 2022-03-25 18:41:06 +00:00
parent 28b3468835
commit f1d262bfa7
No known key found for this signature in database
GPG Key ID: 8420C7CDE43DC4D6
13 changed files with 553 additions and 12 deletions

View File

@ -17,7 +17,8 @@ export function builder(yargs: Argv<ParentArguments>) {
return yargs.option('znc-proxy-url', {
describe: 'URL of Nintendo Switch Online app API proxy server to use',
type: 'string',
}).option('auto-update-iksm-session', {
}).option('auto-update-session', {
alias: ['auto-update-iksm-session'],
describe: 'Automatically obtain and refresh the iksm_session cookie',
type: 'boolean',
default: true,

View File

@ -0,0 +1,92 @@
import createDebug from 'debug';
// @ts-expect-error
import Table from 'cli-table/lib/index.js';
import type { Arguments as ParentArguments } from '../splatnet2.js';
import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js';
import { getIksmToken } from './util.js';
const debug = createDebug('cli:splatnet2:battles');
export const command = 'battles';
export const desc = 'List the last 50 regular/ranked/private/festival battles';
export function builder(yargs: Argv<ParentArguments>) {
return yargs.option('user', {
describe: 'Nintendo Account ID',
type: 'string',
}).option('token', {
describe: 'Nintendo Account session token',
type: 'string',
}).option('json', {
describe: 'Output raw JSON',
type: 'boolean',
}).option('json-pretty-print', {
describe: 'Output pretty-printed JSON',
type: 'boolean',
});
}
type Arguments = YargsArguments<ReturnType<typeof builder>>;
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
const storage = await initStorage(argv.dataPath);
const usernsid = argv.user ?? await storage.getItem('SelectedUser');
const token: string = argv.token ||
await storage.getItem('NintendoAccountToken.' + usernsid);
const {splatnet} = await getIksmToken(storage, token, argv.zncProxyUrl, argv.autoUpdateSession);
const results = await splatnet.getResults();
if (argv.jsonPrettyPrint) {
console.log(JSON.stringify(results, null, 4));
return;
}
if (argv.json) {
console.log(JSON.stringify(results));
return;
}
console.log('Summary', results.summary);
const table = new Table({
head: [
'#',
'Type',
'Mode',
'Rule',
'Stage',
'Result',
'Inked',
'K (A)',
'D',
'S',
'Timestamp',
],
});
results.results.sort((a, b) => a.start_time > b.start_time ? 1 : a.start_time < b.start_time ? -1 : 0);
for (const result of results.results) {
table.push([
result.battle_number,
result.type,
(result.game_mode.key === 'regular' ? '\u001b[32m' :
result.game_mode.key === 'ranked' ? '\u001b[33m' :
result.game_mode.key === 'league' ? '\u001b[31m' :
result.game_mode.key === 'private' ? '\u001b[35m' : '') +
result.game_mode.name + '\u001b[0m',
result.rule.key,
result.stage.name,
(result.my_team_result.key === 'victory' ? '\u001b[32m' : '\u001b[31m') +
result.my_team_result.name + '\u001b[0m',
result.player_result.game_paint_point + 'p',
result.player_result.kill_count + ' (' + result.player_result.assist_count + ')',
result.player_result.death_count,
result.player_result.special_count,
new Date(result.start_time * 1000).toISOString(),
]);
}
console.log(table.toString());
}

View File

@ -0,0 +1,92 @@
import createDebug from 'debug';
// @ts-expect-error
import Table from 'cli-table/lib/index.js';
import type { Arguments as ParentArguments } from '../splatnet2.js';
import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js';
import { getIksmToken } from './util.js';
const debug = createDebug('cli:splatnet2:challenges');
export const command = 'challenges';
export const desc = 'List lifetime inkage challenges';
export function builder(yargs: Argv<ParentArguments>) {
return yargs.option('user', {
describe: 'Nintendo Account ID',
type: 'string',
}).option('token', {
describe: 'Nintendo Account session token',
type: 'string',
}).option('json', {
describe: 'Output raw JSON',
type: 'boolean',
}).option('json-pretty-print', {
describe: 'Output pretty-printed JSON',
type: 'boolean',
});
}
type Arguments = YargsArguments<ReturnType<typeof builder>>;
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
const storage = await initStorage(argv.dataPath);
const usernsid = argv.user ?? await storage.getItem('SelectedUser');
const token: string = argv.token ||
await storage.getItem('NintendoAccountToken.' + usernsid);
const {splatnet} = await getIksmToken(storage, token, argv.zncProxyUrl, argv.autoUpdateSession);
const records = await splatnet.getRecords();
if (argv.jsonPrettyPrint) {
console.log(JSON.stringify(records.challenges, null, 4));
return;
}
if (argv.json) {
console.log(JSON.stringify(records.challenges));
return;
}
for (const [text, season, challenges, next, total_paint_point] of [
[
'Lifetime inkage challenges season 1', 1,
records.challenges.archived_challenges, records.challenges.next_challenge,
records.challenges.total_paint_point,
] as const,
[
'Lifetime inkage challenges season 2 (Octoling)', 2,
records.challenges.archived_challenges_octa, records.challenges.next_challenge_octa,
records.challenges.total_paint_point_octa,
] as const,
]) {
const table = new Table({
head: [
'ID',
'Name',
'Turf covered',
'Completion',
],
});
for (const challenge of challenges) {
table.push([
challenge.key,
challenge.name,
challenge.paint_points + 'p',
'100%',
]);
}
if (next) {
table.push([
'???',
'???',
next.paint_points + 'p',
(Math.round((total_paint_point / next.paint_points) * 10000) / 100) + '%',
]);
}
console.log(text);
console.log(table.toString());
}
}

View File

@ -67,7 +67,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
const usernsid = argv.user ?? await storage.getItem('SelectedUser');
const token: string = argv.token ||
await storage.getItem('NintendoAccountToken.' + usernsid);
const {splatnet} = await getIksmToken(storage, token, argv.zncProxyUrl, argv.autoUpdateIksmSession);
const {splatnet} = await getIksmToken(storage, token, argv.zncProxyUrl, argv.autoUpdateSession);
await mkdirp(argv.directory);

View File

@ -56,7 +56,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
const usernsid = argv.user ?? await storage.getItem('SelectedUser');
const token: string = argv.token ||
await storage.getItem('NintendoAccountToken.' + usernsid);
const {splatnet} = await getIksmToken(storage, token, argv.zncProxyUrl, argv.autoUpdateIksmSession);
const {splatnet} = await getIksmToken(storage, token, argv.zncProxyUrl, argv.autoUpdateSession);
await mkdirp(argv.directory);
@ -123,13 +123,16 @@ export async function dumpResults(
await fs.writeFile(image_file, image);
}
const skipped = [];
const skipped_images = [];
for (const item of results.results) {
const filename = 'splatnet2-result-' + results.unique_id + '-' + item.battle_number + '-' + item.type + '.json';
const file = path.join(directory, filename);
try {
await fs.stat(file);
debug('Skipping battle result %d, file already exists', item.battle_number);
skipped.push(item.battle_number);
} catch (err) {
debug('Fetching battle result %d', item.battle_number);
const result = await splatnet.getResult(item.battle_number);
@ -159,26 +162,36 @@ export async function dumpResults(
try {
await fs.stat(file);
await fs.stat(image_file);
debug('Skipping battle result image %d, file already exists', item.battle_number);
skipped_images.push(item.battle_number);
} catch (err) {
debug('Fetching battle results summary image URL');
debug('Fetching battle results image URL');
const share = await splatnet.shareResult(item.battle_number);
debug('Fetching battle results summary image');
debug('Fetching battle results image');
const image_response = await fetch(share.url);
const image = await image_response.buffer();
debug('Writing battle results summary image data %s', filename);
debug('Writing battle results image data %s', filename);
await fs.writeFile(file, JSON.stringify({
share,
}, null, 4) + '\n', 'utf-8');
debug('Writing battle results summary image %s', filename);
debug('Writing battle results image %s', filename);
await fs.writeFile(image_file, image);
}
}
}
if (skipped.length) {
if (skipped.length === 1) debug('Skipped battle result %d, file already exists', skipped[0]);
else debug('Skipped battle results %s, files already exist', skipped.join(', '));
}
if (skipped_images.length) {
if (skipped_images.length === 1) debug('Skipped battle result image %d, file already exists',
skipped_images[0]);
else debug('Skipped battle result images %s, files already exist', skipped_images.join(', '));
}
await fs.writeFile(latest_file, JSON.stringify({timestamp}, null, 4) + '\n', 'utf-8');
}
@ -207,13 +220,15 @@ export async function dumpCoopResults(splatnet: SplatNet2Api, directory: string,
debug('Writing summary %s', summary_filename);
await fs.writeFile(summary_file, JSON.stringify(results, null, 4) + '\n', 'utf-8');
const skipped = [];
for (const item of results.results) {
const filename = 'splatnet2-coop-result-' + user_id + '-' + item.job_id + '.json';
const file = path.join(directory, filename);
try {
await fs.stat(file);
debug('Skipping coop result %d, file already exists', item.job_id);
skipped.push(item.job_id);
continue;
} catch (err) {}
@ -234,5 +249,10 @@ export async function dumpCoopResults(splatnet: SplatNet2Api, directory: string,
}, null, 4) + '\n', 'utf-8');
}
if (skipped.length) {
if (skipped.length === 1) debug('Skipped coop result %d, file already exists', skipped[0]);
else debug('Skipped coop results %s, files already exist', skipped.join(', '));
}
await fs.writeFile(latest_file, JSON.stringify({timestamp}, null, 4) + '\n', 'utf-8');
}

97
src/cli/splatnet2/hero.ts Normal file
View File

@ -0,0 +1,97 @@
import createDebug from 'debug';
// @ts-expect-error
import Table from 'cli-table/lib/index.js';
import type { Arguments as ParentArguments } from '../splatnet2.js';
import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js';
import { getIksmToken } from './util.js';
const debug = createDebug('cli:splatnet2:hero');
export const command = 'hero';
export const desc = 'Show hero (Octo Canyon) records';
export function builder(yargs: Argv<ParentArguments>) {
return yargs.option('user', {
describe: 'Nintendo Account ID',
type: 'string',
}).option('token', {
describe: 'Nintendo Account session token',
type: 'string',
}).option('json', {
describe: 'Output raw JSON',
type: 'boolean',
}).option('json-pretty-print', {
describe: 'Output pretty-printed JSON',
type: 'boolean',
});
}
type Arguments = YargsArguments<ReturnType<typeof builder>>;
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
const storage = await initStorage(argv.dataPath);
const usernsid = argv.user ?? await storage.getItem('SelectedUser');
const token: string = argv.token ||
await storage.getItem('NintendoAccountToken.' + usernsid);
const {splatnet} = await getIksmToken(storage, token, argv.zncProxyUrl, argv.autoUpdateSession);
const hero = await splatnet.getHeroRecords();
if (argv.jsonPrettyPrint) {
console.log(JSON.stringify(hero, null, 4));
return;
}
if (argv.json) {
console.log(JSON.stringify(hero));
return;
}
console.log('Summary', hero.summary);
const table = new Table({
head: [
'Mission',
'Hero Shot',
'Hero Roller',
'Hero Charger',
'Hero Dualies',
'Hero Brella',
'Hero Splatling',
'Hero Blaster',
'Hero Slosher',
'Herobrush',
],
});
for (const stage of hero.stage_infos) {
table.push([
stage.stage.area + '-' + (stage.stage.is_boss ? 'B' : stage.stage.id),
stage.clear_weapons[0] ? 'Level ' + stage.clear_weapons[0].weapon_level + ', ' +
hrduration(stage.clear_weapons[0].clear_time) : '',
stage.clear_weapons[1] ? hrduration(stage.clear_weapons[1].clear_time) : '',
stage.clear_weapons[2] ? hrduration(stage.clear_weapons[2].clear_time) : '',
stage.clear_weapons[3] ? hrduration(stage.clear_weapons[3].clear_time) : '',
stage.clear_weapons[4] ? hrduration(stage.clear_weapons[4].clear_time) : '',
stage.clear_weapons[5] ? hrduration(stage.clear_weapons[5].clear_time) : '',
stage.clear_weapons[6] ? hrduration(stage.clear_weapons[6].clear_time) : '',
stage.clear_weapons[7] ? hrduration(stage.clear_weapons[7].clear_time) : '',
stage.clear_weapons[8] ? hrduration(stage.clear_weapons[8].clear_time) : '',
]);
}
console.log('Stages');
console.log(table.toString());
}
function hrduration(duration: number) {
const minutes = Math.floor(duration / 60);
const seconds = duration - (minutes * 60);
if (minutes >= 1) {
return minutes + 'm' +
(seconds ? ' ' + seconds + 's' : '');
} else {
return seconds + 's';
}
}

View File

@ -1,4 +1,10 @@
export * as user from './user.js';
export * as stages from './stages.js';
export * as challenges from './challenges.js';
export * as weapons from './weapons.js';
export * as hero from './hero.js';
export * as battles from './battles.js';
export * as schedule from './schedule.js';
export * as dumpResults from './dump-results.js';
export * as dumpRecords from './dump-records.js';
export * as monitor from './monitor.js';

View File

@ -91,6 +91,8 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
i.cached_records = records;
i.auto_update_iksm_session = argv.autoUpdateSession;
console.log('Player %s (Splatoon 2 ID %s, NSA ID %s) level %d',
records.records.player.nickname,
records.records.unique_id,
@ -120,6 +122,8 @@ export class SplatNet2RecordsMonitor {
/** Prevents redownloading user records on the first loop run */
cached_records: Records | null = null;
auto_update_iksm_session = true;
constructor(
public storage: persist.LocalStorage,
public token: string,

View File

@ -0,0 +1,78 @@
import createDebug from 'debug';
// @ts-expect-error
import Table from 'cli-table/lib/index.js';
import type { Arguments as ParentArguments } from '../splatnet2.js';
import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js';
import { getIksmToken } from './util.js';
const debug = createDebug('cli:splatnet2:schedule');
export const command = 'schedule';
export const desc = 'Show stage schedules';
export function builder(yargs: Argv<ParentArguments>) {
return yargs.option('user', {
describe: 'Nintendo Account ID',
type: 'string',
}).option('token', {
describe: 'Nintendo Account session token',
type: 'string',
}).option('json', {
describe: 'Output raw JSON',
type: 'boolean',
}).option('json-pretty-print', {
describe: 'Output pretty-printed JSON',
type: 'boolean',
});
}
type Arguments = YargsArguments<ReturnType<typeof builder>>;
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
const storage = await initStorage(argv.dataPath);
const usernsid = argv.user ?? await storage.getItem('SelectedUser');
const token: string = argv.token ||
await storage.getItem('NintendoAccountToken.' + usernsid);
const {splatnet} = await getIksmToken(storage, token, argv.zncProxyUrl, argv.autoUpdateSession);
const schedules = await splatnet.getSchedules();
if (argv.jsonPrettyPrint) {
console.log(JSON.stringify(schedules, null, 4));
return;
}
if (argv.json) {
console.log(JSON.stringify(schedules));
return;
}
for (const [text, schedule] of [
['Regular Battle', schedules.regular],
['Ranked Battle', schedules.gachi],
['League Battle', schedules.league],
] as const) {
const table = new Table({
head: [
'ID',
'Start',
'Rule',
'Stage',
'Stage',
],
});
for (const item of schedule) {
table.push([
item.id,
new Date(item.start_time * 1000).toISOString(),
item.rule.name,
item.stage_a.name,
item.stage_b.name,
]);
}
console.log(text);
console.log(table.toString());
}
}

View File

@ -0,0 +1,67 @@
import createDebug from 'debug';
// @ts-expect-error
import Table from 'cli-table/lib/index.js';
import type { Arguments as ParentArguments } from '../splatnet2.js';
import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js';
import { getIksmToken } from './util.js';
const debug = createDebug('cli:splatnet2:stages');
export const command = 'stages';
export const desc = 'List stages';
export function builder(yargs: Argv<ParentArguments>) {
return yargs.option('user', {
describe: 'Nintendo Account ID',
type: 'string',
}).option('token', {
describe: 'Nintendo Account session token',
type: 'string',
}).option('json', {
describe: 'Output raw JSON',
type: 'boolean',
}).option('json-pretty-print', {
describe: 'Output pretty-printed JSON',
type: 'boolean',
});
}
type Arguments = YargsArguments<ReturnType<typeof builder>>;
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
const storage = await initStorage(argv.dataPath);
const usernsid = argv.user ?? await storage.getItem('SelectedUser');
const token: string = argv.token ||
await storage.getItem('NintendoAccountToken.' + usernsid);
const {splatnet} = await getIksmToken(storage, token, argv.zncProxyUrl, argv.autoUpdateSession);
const stages = await splatnet.getStages();
if (argv.jsonPrettyPrint) {
console.log(JSON.stringify(stages, null, 4));
return;
}
if (argv.json) {
console.log(JSON.stringify(stages));
return;
}
const table = new Table({
head: [
'ID',
'Name',
],
});
stages.stages.sort((a, b) => parseInt(a.id) > parseInt(b.id) ? 1 : parseInt(a.id) < parseInt(b.id) ? -1 : 0);
for (const stage of stages.stages) {
table.push([
stage.id,
stage.name,
]);
}
console.log(table.toString());
}

View File

@ -26,7 +26,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
const usernsid = argv.user ?? await storage.getItem('SelectedUser');
const token: string = argv.token ||
await storage.getItem('NintendoAccountToken.' + usernsid);
const {splatnet} = await getIksmToken(storage, token, argv.zncProxyUrl, argv.autoUpdateIksmSession);
const {splatnet, data} = await getIksmToken(storage, token, argv.zncProxyUrl, argv.autoUpdateSession);
const [records, stages, activefestivals, timeline] = await Promise.all([
splatnet.getRecords(),
@ -42,4 +42,6 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
records.records.player.principal_id,
records.records.player.player_rank,
records.records.player.player_type);
console.log(data.iksm_session);
}

View File

@ -28,7 +28,7 @@ export async function getIksmToken(storage: persist.LocalStorage, token: string,
const existingToken: SavedIksmSessionToken | undefined = await storage.getItem('IksmToken.' + token);
const td = 2 * 24 * 60 * 60 * 1000; // 2 days in ms
const td = 24 * 60 * 60 * 1000; // 1 day in ms
const last_used_days_ago = existingToken?.last_used && (existingToken.last_used + td) <= Date.now();
const expired = existingToken && existingToken.expires_at <= Date.now();

View File

@ -0,0 +1,82 @@
import createDebug from 'debug';
// @ts-expect-error
import Table from 'cli-table/lib/index.js';
import type { Arguments as ParentArguments } from '../splatnet2.js';
import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js';
import { getIksmToken } from './util.js';
const debug = createDebug('cli:splatnet2:weapons');
export const command = 'weapons';
export const desc = 'Show weapon stats';
export function builder(yargs: Argv<ParentArguments>) {
return yargs.option('user', {
describe: 'Nintendo Account ID',
type: 'string',
}).option('token', {
describe: 'Nintendo Account session token',
type: 'string',
}).option('json', {
describe: 'Output raw JSON',
type: 'boolean',
}).option('json-pretty-print', {
describe: 'Output pretty-printed JSON',
type: 'boolean',
});
}
type Arguments = YargsArguments<ReturnType<typeof builder>>;
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
const storage = await initStorage(argv.dataPath);
const usernsid = argv.user ?? await storage.getItem('SelectedUser');
const token: string = argv.token ||
await storage.getItem('NintendoAccountToken.' + usernsid);
const {splatnet} = await getIksmToken(storage, token, argv.zncProxyUrl, argv.autoUpdateSession);
const records = await splatnet.getRecords();
if (argv.jsonPrettyPrint) {
console.log(JSON.stringify(records.records.weapon_stats, null, 4));
return;
}
if (argv.json) {
console.log(JSON.stringify(records.records.weapon_stats));
return;
}
const table = new Table({
head: [
'ID',
'Name',
'Sub',
'Special',
'Wins',
'Losses',
'Meter',
'H. meter',
'Turf inked',
'Last used',
],
});
for (const weaponstats of Object.values(records.records.weapon_stats)) {
table.push([
weaponstats.weapon.id,
weaponstats.weapon.name,
weaponstats.weapon.sub.name,
weaponstats.weapon.special.name,
weaponstats.win_count,
weaponstats.lose_count,
weaponstats.win_meter,
weaponstats.max_win_meter,
weaponstats.total_paint_point + 'p',
new Date(weaponstats.last_use_time * 1000).toISOString(),
]);
}
console.log('Weapon stats');
console.log(table.toString());
}