Move Splatfest and photo album records to their own commands

This commit is contained in:
Samuel Elliott 2022-10-26 13:37:47 +01:00
parent f88d10b61c
commit a33a36c4f5
No known key found for this signature in database
GPG Key ID: 8420C7CDE43DC4D6
4 changed files with 306 additions and 226 deletions

View File

@ -0,0 +1,127 @@
import * as path from 'node:path';
import * as fs from 'node:fs/promises';
import createDebug from 'debug';
import mkdirp from 'mkdirp';
import fetch from 'node-fetch';
import { PhotoAlbumResult, RequestId } from 'splatnet3-types/splatnet3';
import type { Arguments as ParentArguments } from '../splatnet3.js';
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
import { initStorage } from '../../util/storage.js';
import { getBulletToken } from '../../common/auth/splatnet3.js';
import SplatNet3Api from '../../api/splatnet3.js';
import { ResponseSymbol } from '../../api/util.js';
import { timeoutSignal } from '../../util/misc.js';
const debug = createDebug('cli:splatnet3:dump-album');
export const command = 'dump-album [directory]';
export const desc = 'Download all album photos';
export function builder(yargs: Argv<ParentArguments>) {
return yargs.positional('directory', {
describe: 'Directory to write record data to',
type: 'string',
}).option('user', {
describe: 'Nintendo Account ID',
type: 'string',
}).option('token', {
describe: 'Nintendo Account session token',
type: 'string',
});
}
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 getBulletToken(storage, token, argv.zncProxyUrl, argv.autoUpdateSession);
const directory = argv.directory ?? path.join(argv.dataPath, 'splatnet3');
await mkdirp(directory);
await dumpAlbumPhotos(splatnet, directory);
}
export async function dumpAlbumPhotos(
splatnet: SplatNet3Api, directory: string,
refresh: PhotoAlbumResult | boolean = false
) {
debug('Fetching photo album items');
console.warn('Fetching photo album items');
const results = refresh ?
await splatnet.getPhotoAlbumRefetch() :
await splatnet.getPhotoAlbum();
if (typeof refresh !== 'object' ||
results.data.photoAlbum.items.nodes[0].id !== refresh.photoAlbum.items.nodes[0].id
) {
const filename = 'splatnet3-photoalbum-' + Date.now() + '.json';
const file = path.join(directory, filename);
debug('Writing %s', filename);
await fs.writeFile(file, JSON.stringify({
result: results.data.photoAlbum,
query: refresh ? RequestId.PhotoAlbumRefetchQuery : RequestId.PhotoAlbumQuery,
app_version: splatnet.version,
be_version: results[ResponseSymbol].headers.get('x-be-version'),
}, null, 4) + '\n', 'utf-8');
}
for (const item of [...results.data.photoAlbum.items.nodes].reverse()) {
const id_str = Buffer.from(item.id, 'base64').toString() || item.id;
const match = id_str.match(/^PhotoAlbumItem-(\d+)$/);
const id = match ? match[1] : id_str;
const thumbnail_filename = 'splatnet3-photothumbnail-' + id + '.jpeg';
const thumbnail_file = path.join(directory, thumbnail_filename);
try {
await fs.stat(thumbnail_file);
} catch (err) {
debug('Fetching photo thumbnail %s', id, item.uploadedTime);
console.warn('Fetching photo thumbnail %s', id, item.uploadedTime);
const [signal, cancel] = timeoutSignal();
const response = await fetch(item.thumbnail.url, {
headers: {
'User-Agent': splatnet.useragent,
},
signal,
}).finally(cancel);
const data = new Uint8Array(await response.arrayBuffer());
debug('Writing %s', thumbnail_filename);
await fs.writeFile(thumbnail_file, data);
}
const filename = 'splatnet3-photo-' + id + '.jpeg';
const file = path.join(directory, filename);
try {
await fs.stat(file);
} catch (err) {
debug('Fetching photo %s', id, item.uploadedTime);
console.warn('Fetching photo %s', id, item.uploadedTime);
const [signal, cancel] = timeoutSignal();
const response = await fetch(item.photo.url, {
headers: {
'User-Agent': splatnet.useragent,
},
signal,
}).finally(cancel);
const data = new Uint8Array(await response.arrayBuffer());
debug('Writing %s', filename);
await fs.writeFile(file, data);
}
}
return results.data;
}

View File

@ -0,0 +1,173 @@
import * as path from 'node:path';
import * as fs from 'node:fs/promises';
import createDebug from 'debug';
import mkdirp from 'mkdirp';
import { FestState, Fest_detail, RequestId } from 'splatnet3-types/splatnet3';
import type { Arguments as ParentArguments } from '../splatnet3.js';
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
import { initStorage } from '../../util/storage.js';
import { getBulletToken } from '../../common/auth/splatnet3.js';
import SplatNet3Api from '../../api/splatnet3.js';
import { ResponseSymbol } from '../../api/util.js';
const debug = createDebug('cli:splatnet3:dump-records');
export const command = 'dump-fests [directory]';
export const desc = 'Download all Splatfest records';
export function builder(yargs: Argv<ParentArguments>) {
return yargs.positional('directory', {
describe: 'Directory to write record data to',
type: 'string',
}).option('user', {
describe: 'Nintendo Account ID',
type: 'string',
}).option('token', {
describe: 'Nintendo Account session token',
type: 'string',
}).option('include-rankings', {
describe: 'Include fest rankings',
type: 'boolean',
default: false,
});
}
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 getBulletToken(storage, token, argv.zncProxyUrl, argv.autoUpdateSession);
const directory = argv.directory ?? path.join(argv.dataPath, 'splatnet3');
await mkdirp(directory);
await dumpFestRecords(splatnet, directory, argv.includeRankings);
}
export async function dumpFestRecords(splatnet: SplatNet3Api, directory: string, include_rankings = false) {
debug('Fetching fest records');
console.warn('Fetching fest records');
const records = await splatnet.getFestRecords();
const filename = 'splatnet3-fests-' + Date.now() + '.json';
const file = path.join(directory, filename);
debug('Writing %s', filename);
await fs.writeFile(file, JSON.stringify({
result: records.data.festRecords,
player: records.data.currentPlayer,
query: RequestId.FestRecordQuery,
app_version: splatnet.version,
be_version: records[ResponseSymbol].headers.get('x-be-version'),
}, null, 4) + '\n', 'utf-8');
const skipped = [];
for (const fest_record of [...records.data.festRecords.nodes].reverse()) {
// Fest-EU:JUEA-00001
const id_str = Buffer.from(fest_record.id, 'base64').toString() || fest_record.id;
const match = id_str.match(/^Fest-([A-Z]{2}):(([A-Z]+)-(\d+))$/);
const id = match ? match[1] + '-' + match[2] : id_str;
const filename = 'splatnet3-fest-' + id + '-' +
(fest_record.state !== FestState.CLOSED ? Date.now() + '-' : '') +
RequestId.DetailFestRecordDetailQuery + '.json';
const file = path.join(directory, filename);
let record: Fest_detail | null = null;
try {
await fs.stat(file);
// skipped.push(id);
} catch (err) {
debug('Fetching fest record %s', id);
console.warn('Fetching fest record %s', id);
const result = await splatnet.getFestDetail(fest_record.id);
record = result.data.fest;
debug('Writing %s', filename);
await fs.writeFile(file, JSON.stringify({
result: result.data.fest,
player: result.data.currentPlayer,
query: RequestId.DetailFestRecordDetailQuery,
app_version: splatnet.version,
be_version: result[ResponseSymbol].headers.get('x-be-version'),
}, null, 4) + '\n', 'utf-8');
}
if (fest_record.state !== FestState.CLOSED) {
const filename = 'splatnet3-festvotes-' + id + '-' + Date.now() + '-' +
RequestId.DetailVotingStatusQuery + '.json';
const file = path.join(directory, filename);
// Fetch this now to match the behavour of Nintendo's app
// If state !== closed it shouldn't be possible to get here
if (!record) {
const result = await splatnet.getFestDetail(fest_record.id);
record = result.data.fest;
}
debug('Fetching fest voting status %s', id);
console.warn('Fetching fest voting status %s', id);
const result = await splatnet.getFestVotingStatus(fest_record.id);
debug('Writing %s', filename);
await fs.writeFile(file, JSON.stringify({
result: result.data.fest,
query: RequestId.DetailVotingStatusQuery,
app_version: splatnet.version,
be_version: result[ResponseSymbol].headers.get('x-be-version'),
}, null, 4) + '\n', 'utf-8');
}
if (include_rankings) {
const filename = 'splatnet3-festranking-' + id + '-' + RequestId.DetailRankingQuery + '.json';
const file = path.join(directory, filename);
try {
await fs.stat(file);
} catch (err) {
// Fetch this now to match the behavour of Nintendo's app
if (!record) {
const result = await splatnet.getFestDetail(fest_record.id);
record = result.data.fest;
}
const rankings_available = record.state === FestState.CLOSED &&
!!record.teams[0].result;
if (rankings_available) {
debug('Fetching fest rankings %s', id);
console.warn('Fetching fest rankings %s', id);
const result = await splatnet.getFestRanking(fest_record.id);
debug('Writing %s', filename);
await fs.writeFile(file, JSON.stringify({
result: result.data.fest,
query: RequestId.DetailFestRecordDetailQuery,
app_version: splatnet.version,
be_version: result[ResponseSymbol].headers.get('x-be-version'),
}, null, 4) + '\n', 'utf-8');
} else {
debug('Skipping downloading rankings for %s, not yet available', id);
}
}
}
if (!record) {
skipped.push(id);
}
}
if (skipped.length) {
if (skipped.length === 1) debug('Skipped fest %s, file already exists', skipped[0]);
else debug('Skipped %d fests, files already exist', skipped.length);
}
}

View File

@ -2,20 +2,18 @@ import * as path from 'node:path';
import * as fs from 'node:fs/promises';
import createDebug from 'debug';
import mkdirp from 'mkdirp';
import fetch from 'node-fetch';
import { FestState, Fest_detail, PhotoAlbumResult, RequestId } from 'splatnet3-types/splatnet3';
import { RequestId } from 'splatnet3-types/splatnet3';
import type { Arguments as ParentArguments } from '../splatnet3.js';
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
import { initStorage } from '../../util/storage.js';
import { getBulletToken } from '../../common/auth/splatnet3.js';
import SplatNet3Api from '../../api/splatnet3.js';
import { ResponseSymbol } from '../../api/util.js';
import { timeoutSignal } from '../../util/misc.js';
const debug = createDebug('cli:splatnet3:dump-records');
export const command = 'dump-records [directory]';
export const desc = 'Download all player history/hero/fest/catalog records, stage/weapon stats and album photos';
export const desc = 'Download all player history/hero/catalog records and stage/weapon stats';
export function builder(yargs: Argv<ParentArguments>) {
return yargs.positional('directory', {
@ -33,13 +31,6 @@ export function builder(yargs: Argv<ParentArguments>) {
}).option('hero', {
describe: 'Include hero records',
type: 'boolean',
}).option('fest', {
describe: 'Include fest records',
type: 'boolean',
}).option('fest-rankings', {
describe: 'Include fest rankings (requires --fest)',
type: 'boolean',
default: false,
}).option('catalog', {
describe: 'Include catalog records',
type: 'boolean',
@ -49,22 +40,17 @@ export function builder(yargs: Argv<ParentArguments>) {
}).option('weapon', {
describe: 'Include weapon stats',
type: 'boolean',
}).option('album', {
describe: 'Include photo album',
type: 'boolean',
});
}
type Arguments = YargsArguments<ReturnType<typeof builder>>;
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
const _all = [argv.history, argv.hero, argv.fest, argv.catalog, argv.stage, argv.weapon, argv.album];
const _all = [argv.history, argv.hero, argv.catalog, argv.stage, argv.weapon];
const _default = !_all.find(f => f === true);
debug('default', _all, _default, _all.some(f => f ?? _default));
if (!_all.some(f => f ?? _default)) {
throw new Error('Enable one of --history, --hero, --fest, --catalog, --stage, --weapon or --album');
throw new Error('Enable one of --history, --hero, --catalog, --stage or --weapon');
}
const storage = await initStorage(argv.dataPath);
@ -84,9 +70,6 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
if (argv.hero ?? _default) {
await dumpHeroRecords(splatnet, directory);
}
if (argv.fest ?? _default) {
await dumpFestRecords(splatnet, directory, argv.festRankings);
}
if (argv.catalog ?? _default) {
await dumpCatalogRecords(splatnet, directory);
}
@ -96,9 +79,6 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
if (argv.weapon ?? _default) {
await dumpWeaponStats(splatnet, directory);
}
if (argv.album ?? _default) {
await dumpAlbumPhotos(splatnet, directory);
}
}
export async function dumpHistoryRecords(splatnet: SplatNet3Api, directory: string, refresh = false) {
@ -140,129 +120,6 @@ export async function dumpHeroRecords(splatnet: SplatNet3Api, directory: string)
}, null, 4) + '\n', 'utf-8');
}
export async function dumpFestRecords(splatnet: SplatNet3Api, directory: string, include_rankings = false) {
debug('Fetching fest records');
console.warn('Fetching fest records');
const records = await splatnet.getFestRecords();
const filename = 'splatnet3-fests-' + Date.now() + '.json';
const file = path.join(directory, filename);
debug('Writing %s', filename);
await fs.writeFile(file, JSON.stringify({
result: records.data.festRecords,
player: records.data.currentPlayer,
query: RequestId.FestRecordQuery,
app_version: splatnet.version,
be_version: records[ResponseSymbol].headers.get('x-be-version'),
}, null, 4) + '\n', 'utf-8');
const skipped = [];
for (const fest_record of [...records.data.festRecords.nodes].reverse()) {
// Fest-EU:JUEA-00001
const id_str = Buffer.from(fest_record.id, 'base64').toString() || fest_record.id;
const match = id_str.match(/^Fest-([A-Z]{2}):(([A-Z]+)-(\d+))$/);
const id = match ? match[1] + '-' + match[2] : id_str;
const filename = 'splatnet3-fest-' + id + '-' +
(fest_record.state !== FestState.CLOSED ? Date.now() + '-' : '') +
RequestId.DetailFestRecordDetailQuery + '.json';
const file = path.join(directory, filename);
let record: Fest_detail | null = null;
try {
await fs.stat(file);
// skipped.push(id);
} catch (err) {
debug('Fetching fest record %s', id);
console.warn('Fetching fest record %s', id);
const result = await splatnet.getFestDetail(fest_record.id);
record = result.data.fest;
debug('Writing %s', filename);
await fs.writeFile(file, JSON.stringify({
result: result.data.fest,
player: result.data.currentPlayer,
query: RequestId.DetailFestRecordDetailQuery,
app_version: splatnet.version,
be_version: result[ResponseSymbol].headers.get('x-be-version'),
}, null, 4) + '\n', 'utf-8');
}
if (fest_record.state !== FestState.CLOSED) {
const filename = 'splatnet3-festvotes-' + id + '-' + Date.now() + '-' +
RequestId.DetailVotingStatusQuery + '.json';
const file = path.join(directory, filename);
// Fetch this now to match the behavour of Nintendo's app
// If state !== closed it shouldn't be possible to get here
if (!record) {
const result = await splatnet.getFestDetail(fest_record.id);
record = result.data.fest;
}
debug('Fetching fest voting status %s', id);
console.warn('Fetching fest voting status %s', id);
const result = await splatnet.getFestVotingStatus(fest_record.id);
debug('Writing %s', filename);
await fs.writeFile(file, JSON.stringify({
result: result.data.fest,
query: RequestId.DetailVotingStatusQuery,
app_version: splatnet.version,
be_version: result[ResponseSymbol].headers.get('x-be-version'),
}, null, 4) + '\n', 'utf-8');
}
if (include_rankings) {
const filename = 'splatnet3-festranking-' + id + '-' + RequestId.DetailRankingQuery + '.json';
const file = path.join(directory, filename);
try {
await fs.stat(file);
} catch (err) {
// Fetch this now to match the behavour of Nintendo's app
if (!record) {
const result = await splatnet.getFestDetail(fest_record.id);
record = result.data.fest;
}
const rankings_available = record.state === FestState.CLOSED &&
!!record.teams[0].result;
if (rankings_available) {
debug('Fetching fest rankings %s', id);
console.warn('Fetching fest rankings %s', id);
const result = await splatnet.getFestRanking(fest_record.id);
debug('Writing %s', filename);
await fs.writeFile(file, JSON.stringify({
result: result.data.fest,
query: RequestId.DetailFestRecordDetailQuery,
app_version: splatnet.version,
be_version: result[ResponseSymbol].headers.get('x-be-version'),
}, null, 4) + '\n', 'utf-8');
} else {
debug('Skipping downloading rankings for %s, not yet available', id);
}
}
}
if (!record) {
skipped.push(id);
}
}
if (skipped.length) {
if (skipped.length === 1) debug('Skipped fest %s, file already exists', skipped[0]);
else debug('Skipped %d fests, files already exist', skipped.length);
}
}
export async function dumpCatalogRecords(splatnet: SplatNet3Api, directory: string, refresh = false) {
debug('Fetching catalog records');
console.warn('Fetching catalog records');
@ -322,82 +179,3 @@ export async function dumpWeaponStats(splatnet: SplatNet3Api, directory: string,
be_version: results[ResponseSymbol].headers.get('x-be-version'),
}, null, 4) + '\n', 'utf-8');
}
export async function dumpAlbumPhotos(
splatnet: SplatNet3Api, directory: string,
refresh: PhotoAlbumResult | boolean = false
) {
debug('Fetching photo album items');
console.warn('Fetching photo album items');
const results = refresh ?
await splatnet.getPhotoAlbumRefetch() :
await splatnet.getPhotoAlbum();
if (typeof refresh !== 'object' ||
results.data.photoAlbum.items.nodes[0].id !== refresh.photoAlbum.items.nodes[0].id
) {
const filename = 'splatnet3-photoalbum-' + Date.now() + '.json';
const file = path.join(directory, filename);
debug('Writing %s', filename);
await fs.writeFile(file, JSON.stringify({
result: results.data.photoAlbum,
query: refresh ? RequestId.PhotoAlbumRefetchQuery : RequestId.PhotoAlbumQuery,
app_version: splatnet.version,
be_version: results[ResponseSymbol].headers.get('x-be-version'),
}, null, 4) + '\n', 'utf-8');
}
for (const item of [...results.data.photoAlbum.items.nodes].reverse()) {
const id_str = Buffer.from(item.id, 'base64').toString() || item.id;
const match = id_str.match(/^PhotoAlbumItem-(\d+)$/);
const id = match ? match[1] : id_str;
const thumbnail_filename = 'splatnet3-photothumbnail-' + id + '.jpeg';
const thumbnail_file = path.join(directory, thumbnail_filename);
try {
await fs.stat(thumbnail_file);
} catch (err) {
debug('Fetching photo thumbnail %s', id, item.uploadedTime);
console.warn('Fetching photo thumbnail %s', id, item.uploadedTime);
const [signal, cancel] = timeoutSignal();
const response = await fetch(item.thumbnail.url, {
headers: {
'User-Agent': splatnet.useragent,
},
signal,
}).finally(cancel);
const data = new Uint8Array(await response.arrayBuffer());
debug('Writing %s', thumbnail_filename);
await fs.writeFile(thumbnail_file, data);
}
const filename = 'splatnet3-photo-' + id + '.jpeg';
const file = path.join(directory, filename);
try {
await fs.stat(file);
} catch (err) {
debug('Fetching photo %s', id, item.uploadedTime);
console.warn('Fetching photo %s', id, item.uploadedTime);
const [signal, cancel] = timeoutSignal();
const response = await fetch(item.photo.url, {
headers: {
'User-Agent': splatnet.useragent,
},
signal,
}).finally(cancel);
const data = new Uint8Array(await response.arrayBuffer());
debug('Writing %s', filename);
await fs.writeFile(file, data);
}
}
return results.data;
}

View File

@ -6,4 +6,6 @@ export * as festivals from './festivals.js';
export * as festival from './festival.js';
export * as battles from './battles.js';
export * as dumpRecords from './dump-records.js';
export * as dumpFests from './dump-fests.js';
export * as dumpAlbum from './dump-album.js';
export * as dumpResults from './dump-results.js';