mirror of
https://github.com/maxswa/osrs-json-hiscores.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
58e8eaad59 | ||
![]() |
6b6d561a1f | ||
![]() |
26aefe95a0 | ||
![]() |
880679b9e6 | ||
![]() |
f679e2bde8 | ||
![]() |
06a176873b | ||
![]() |
553f4f5c36 | ||
![]() |
e95efb5cdf | ||
![]() |
68eb807657 | ||
![]() |
93d6961a4c | ||
![]() |
fbce22fd07 |
15
README.md
15
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://www.npmjs.com/package/osrs-json-hiscores)
|
||||
[](https://npm-stat.com/charts.html?package=osrs-json-hiscores)
|
||||
[](https://github.com/maxswa/osrs-json-hiscores/src/types.ts)
|
||||
[](https://github.com/maxswa/osrs-json-hiscores/blob/master/src/types.ts)
|
||||
|
||||
**The Oldschool Runescape API wrapper that does more!**
|
||||
|
||||
@@ -15,6 +15,14 @@ Additional functions are provided that screen-scrape the OSRS leaderboards and r
|
||||
|
||||
`osrs-json-hiscores` has TypeScript support, with full definitions for all functions and custom data types.
|
||||
|
||||
---
|
||||
|
||||
### Disclaimer
|
||||
|
||||
Jagex does not provide `Access-Control-Allow-Origin` headers in their responses. This means that [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) will block all browser requests to their hiscores API. In order to get around this, osrs-json-hiscores should be installed on the server side and exposed to the front end via a simple API. Here is an example of this in use: [codesandbox.io/s/osrs-json-hiscores-demo](https://codesandbox.io/s/osrs-json-hiscores-demo-qz656)
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
With npm:
|
||||
@@ -41,7 +49,7 @@ Once you import it you can call the functions asynchronously:
|
||||
|
||||
```javascript
|
||||
hiscores
|
||||
.getStats('Lynx Titan', 'full')
|
||||
.getStats('Lynx Titan')
|
||||
.then(res => console.log(res))
|
||||
.catch(err => console.error(err));
|
||||
```
|
||||
@@ -57,7 +65,8 @@ const stats = await hiscores.getStats('Lynx Titan');
|
||||
const topPage = await getSkillPage('overall');
|
||||
```
|
||||
|
||||
`getStats` will return a `full` player object with game mode by default, but it will also accept any of the following game modes:
|
||||
`getStats` will return a full player object with gamemode.
|
||||
`getStatsByGameMode` will return a stats object and accepts a gamemode parameter:
|
||||
|
||||
| Game mode | Param |
|
||||
| ---------------- | :----: |
|
||||
|
@@ -1,5 +1,11 @@
|
||||
import { parseStats, getRSNFormat, getSkillPage, getStats } from '../src/index';
|
||||
import { PlayerSkillRow, Player } from '../src/types';
|
||||
import {
|
||||
parseStats,
|
||||
getRSNFormat,
|
||||
getSkillPage,
|
||||
getStats,
|
||||
getStatsByGamemode,
|
||||
} from '../src/index';
|
||||
import { PlayerSkillRow, Player, Stats } from '../src/types';
|
||||
import axios, { AxiosError } from 'axios';
|
||||
|
||||
test('Parse CSV to json', () => {
|
||||
@@ -93,10 +99,16 @@ test('Get rsn format', async done => {
|
||||
|
||||
test('Get attack top page', async done => {
|
||||
const callback = (data: PlayerSkillRow[]) => {
|
||||
expect(data).toStrictEqual([
|
||||
{ rsn: 'Heur', rank: 1, level: 99, xp: 200000000, dead: false },
|
||||
expect(data).toMatchObject([
|
||||
{
|
||||
rsn: 'Unohdettu2',
|
||||
rsn: expect.any(String),
|
||||
rank: 1,
|
||||
level: 99,
|
||||
xp: 200000000,
|
||||
dead: false,
|
||||
},
|
||||
{
|
||||
rsn: expect.any(String),
|
||||
rank: 2,
|
||||
level: 99,
|
||||
xp: 200000000,
|
||||
@@ -104,105 +116,159 @@ test('Get attack top page', async done => {
|
||||
},
|
||||
{ rsn: 'Drakon', rank: 3, level: 99, xp: 200000000, dead: false },
|
||||
{
|
||||
rsn: 'Ame Umehara',
|
||||
rsn: expect.any(String),
|
||||
rank: 4,
|
||||
level: 99,
|
||||
xp: 200000000,
|
||||
dead: false,
|
||||
},
|
||||
{ rsn: 'Jakee', rank: 5, level: 99, xp: 200000000, dead: false },
|
||||
{ rsn: 'Hitsuji', rank: 6, level: 99, xp: 200000000, dead: false },
|
||||
{ rsn: 'Howson', rank: 7, level: 99, xp: 200000000, dead: false },
|
||||
{ rsn: 'Dr PFAFF', rank: 8, level: 99, xp: 200000000, dead: false },
|
||||
{
|
||||
rsn: 'Malt Lickeys',
|
||||
rsn: expect.any(String),
|
||||
rank: 5,
|
||||
level: 99,
|
||||
xp: 200000000,
|
||||
dead: false,
|
||||
},
|
||||
{
|
||||
rsn: expect.any(String),
|
||||
rank: 6,
|
||||
level: 99,
|
||||
xp: 200000000,
|
||||
dead: false,
|
||||
},
|
||||
{
|
||||
rsn: expect.any(String),
|
||||
rank: 7,
|
||||
level: 99,
|
||||
xp: 200000000,
|
||||
dead: false,
|
||||
},
|
||||
{
|
||||
rsn: expect.any(String),
|
||||
rank: 8,
|
||||
level: 99,
|
||||
xp: 200000000,
|
||||
dead: false,
|
||||
},
|
||||
{
|
||||
rsn: expect.any(String),
|
||||
rank: 9,
|
||||
level: 99,
|
||||
xp: 200000000,
|
||||
dead: false,
|
||||
},
|
||||
{ rsn: 'Burned', rank: 10, level: 99, xp: 200000000, dead: false },
|
||||
{
|
||||
rsn: 'Blue Limes',
|
||||
rsn: expect.any(String),
|
||||
rank: 10,
|
||||
level: 99,
|
||||
xp: 200000000,
|
||||
dead: false,
|
||||
},
|
||||
{
|
||||
rsn: expect.any(String),
|
||||
rank: 11,
|
||||
level: 99,
|
||||
xp: 200000000,
|
||||
dead: false,
|
||||
},
|
||||
{
|
||||
rsn: 'Mini Finbarr',
|
||||
rsn: expect.any(String),
|
||||
rank: 12,
|
||||
level: 99,
|
||||
xp: 200000000,
|
||||
dead: false,
|
||||
},
|
||||
{
|
||||
rsn: 'Unohdettu3',
|
||||
rsn: expect.any(String),
|
||||
rank: 13,
|
||||
level: 99,
|
||||
xp: 200000000,
|
||||
dead: false,
|
||||
},
|
||||
{
|
||||
rsn: 'Eslihero',
|
||||
rsn: expect.any(String),
|
||||
rank: 14,
|
||||
level: 99,
|
||||
xp: 200000000,
|
||||
dead: false,
|
||||
},
|
||||
{
|
||||
rsn: 'Lynx Titan',
|
||||
rsn: expect.any(String),
|
||||
rank: 15,
|
||||
level: 99,
|
||||
xp: 200000000,
|
||||
dead: false,
|
||||
},
|
||||
{
|
||||
rsn: 'AndrewWigins',
|
||||
rsn: expect.any(String),
|
||||
rank: 16,
|
||||
level: 99,
|
||||
xp: 200000000,
|
||||
dead: false,
|
||||
},
|
||||
{ rsn: 'iMelee', rank: 17, level: 99, xp: 200000000, dead: false },
|
||||
{
|
||||
rsn: 'Portuguese',
|
||||
rsn: expect.any(String),
|
||||
rank: 17,
|
||||
level: 99,
|
||||
xp: 200000000,
|
||||
dead: false,
|
||||
},
|
||||
{
|
||||
rsn: expect.any(String),
|
||||
rank: 18,
|
||||
level: 99,
|
||||
xp: 200000000,
|
||||
dead: false,
|
||||
},
|
||||
{
|
||||
rsn: 'MarkoOSRS',
|
||||
rsn: expect.any(String),
|
||||
rank: 19,
|
||||
level: 99,
|
||||
xp: 200000000,
|
||||
dead: false,
|
||||
},
|
||||
{ rsn: 'Cairo', rank: 20, level: 99, xp: 200000000, dead: false },
|
||||
{
|
||||
rsn: 'Hey Jase',
|
||||
rsn: expect.any(String),
|
||||
rank: 20,
|
||||
level: 99,
|
||||
xp: 200000000,
|
||||
dead: false,
|
||||
},
|
||||
{
|
||||
rsn: expect.any(String),
|
||||
rank: 21,
|
||||
level: 99,
|
||||
xp: 200000000,
|
||||
dead: false,
|
||||
},
|
||||
{
|
||||
rsn: 'Sleighur',
|
||||
rsn: expect.any(String),
|
||||
rank: 22,
|
||||
level: 99,
|
||||
xp: 200000000,
|
||||
dead: false,
|
||||
},
|
||||
{
|
||||
rsn: 'KMSat200mALL',
|
||||
rsn: expect.any(String),
|
||||
rank: 23,
|
||||
level: 99,
|
||||
xp: 200000000,
|
||||
dead: false,
|
||||
},
|
||||
{ rsn: 'Yumemi', rank: 24, level: 99, xp: 200000000, dead: false },
|
||||
{ rsn: 'Fiiggy', rank: 25, level: 99, xp: 200000000, dead: false },
|
||||
{
|
||||
rsn: expect.any(String),
|
||||
rank: 24,
|
||||
level: 99,
|
||||
xp: 200000000,
|
||||
dead: false,
|
||||
},
|
||||
{
|
||||
rsn: expect.any(String),
|
||||
rank: 25,
|
||||
level: 99,
|
||||
xp: 200000000,
|
||||
dead: false,
|
||||
},
|
||||
]);
|
||||
done();
|
||||
};
|
||||
@@ -220,3 +286,37 @@ test('Get non-existant player', async done => {
|
||||
|
||||
getStats('fishy').catch(callback);
|
||||
});
|
||||
|
||||
test('Get stats by gamemode', async done => {
|
||||
const callback = (stats: Stats) => {
|
||||
expect(stats.skills).toStrictEqual({
|
||||
overall: { rank: 1, level: 2277, xp: 4600000000 },
|
||||
attack: { rank: 15, level: 99, xp: 200000000 },
|
||||
defence: { rank: 27, level: 99, xp: 200000000 },
|
||||
strength: { rank: 18, level: 99, xp: 200000000 },
|
||||
hitpoints: { rank: 7, level: 99, xp: 200000000 },
|
||||
ranged: { rank: 7, level: 99, xp: 200000000 },
|
||||
prayer: { rank: 11, level: 99, xp: 200000000 },
|
||||
magic: { rank: 32, level: 99, xp: 200000000 },
|
||||
cooking: { rank: 158, level: 99, xp: 200000000 },
|
||||
woodcutting: { rank: 15, level: 99, xp: 200000000 },
|
||||
fletching: { rank: 12, level: 99, xp: 200000000 },
|
||||
fishing: { rank: 9, level: 99, xp: 200000000 },
|
||||
firemaking: { rank: 49, level: 99, xp: 200000000 },
|
||||
crafting: { rank: 4, level: 99, xp: 200000000 },
|
||||
smithing: { rank: 3, level: 99, xp: 200000000 },
|
||||
mining: { rank: 25, level: 99, xp: 200000000 },
|
||||
herblore: { rank: 5, level: 99, xp: 200000000 },
|
||||
agility: { rank: 24, level: 99, xp: 200000000 },
|
||||
thieving: { rank: 12, level: 99, xp: 200000000 },
|
||||
slayer: { rank: 2, level: 99, xp: 200000000 },
|
||||
farming: { rank: 19, level: 99, xp: 200000000 },
|
||||
runecraft: { rank: 7, level: 99, xp: 200000000 },
|
||||
hunter: { rank: 4, level: 99, xp: 200000000 },
|
||||
construction: { rank: 4, level: 99, xp: 200000000 },
|
||||
});
|
||||
done();
|
||||
};
|
||||
|
||||
getStatsByGamemode('Lynx Titan').then(callback);
|
||||
});
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "osrs-json-hiscores",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.2",
|
||||
"description": "The Oldschool Runescape API wrapper that does more!",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
|
159
src/hiscores.ts
159
src/hiscores.ts
@@ -2,12 +2,11 @@ import axios from 'axios';
|
||||
import * as cheerio from 'cheerio';
|
||||
import {
|
||||
Player,
|
||||
Mode,
|
||||
Activity,
|
||||
Skill,
|
||||
Stats,
|
||||
Skills,
|
||||
BH as BHStats,
|
||||
BH,
|
||||
Clues,
|
||||
Gamemode,
|
||||
SkillName,
|
||||
@@ -20,7 +19,6 @@ import {
|
||||
SKILLS,
|
||||
BH_MODES,
|
||||
CLUES,
|
||||
MODES,
|
||||
getPlayerTableURL,
|
||||
getSkillPageURL,
|
||||
GAMEMODES,
|
||||
@@ -30,92 +28,93 @@ import {
|
||||
getActivityPageURL,
|
||||
} from './utils';
|
||||
|
||||
export async function getStats(
|
||||
rsn: string,
|
||||
mode: Mode = 'full'
|
||||
): Promise<Player> {
|
||||
export async function getStats(rsn: string): Promise<Player> {
|
||||
if (typeof rsn !== 'string') {
|
||||
throw Error('RSN must be a string');
|
||||
} else if (!/^[a-zA-Z0-9 _]+$/.test(rsn)) {
|
||||
throw Error('RSN contains invalid character');
|
||||
} else if (rsn.length > 12 || rsn.length < 1) {
|
||||
throw Error('RSN must be between 1 and 12 characters');
|
||||
} else if (!MODES.includes(mode)) {
|
||||
throw Error('Invalid game mode');
|
||||
}
|
||||
|
||||
if (mode === 'full') {
|
||||
const mainRes = await axios(getStatsURL('main', rsn));
|
||||
if (mainRes.status === 200) {
|
||||
const otherResponses = await Promise.all([
|
||||
axios(getStatsURL('iron', rsn)).catch(err => err),
|
||||
axios(getStatsURL('hc', rsn)).catch(err => err),
|
||||
axios(getStatsURL('ult', rsn)).catch(err => err),
|
||||
getRSNFormat(rsn),
|
||||
]);
|
||||
const mainRes = await axios(getStatsURL('main', rsn));
|
||||
if (mainRes.status === 200) {
|
||||
const otherResponses = await Promise.all([
|
||||
axios(getStatsURL('iron', rsn)).catch(err => err),
|
||||
axios(getStatsURL('hc', rsn)).catch(err => err),
|
||||
axios(getStatsURL('ult', rsn)).catch(err => err),
|
||||
getRSNFormat(rsn),
|
||||
]);
|
||||
|
||||
const [ironRes, hcRes, ultRes, formattedName] = otherResponses;
|
||||
const [ironRes, hcRes, ultRes, formattedName] = otherResponses;
|
||||
|
||||
const player: Player = {
|
||||
rsn: formattedName,
|
||||
mode: 'main',
|
||||
dead: false,
|
||||
deulted: false,
|
||||
deironed: false,
|
||||
};
|
||||
player.main = parseStats(mainRes.data);
|
||||
|
||||
if (ironRes.status === 200) {
|
||||
player.iron = parseStats(ironRes.data);
|
||||
if (hcRes.status === 200) {
|
||||
player.mode = 'hc';
|
||||
player.hc = parseStats(hcRes.data);
|
||||
if (player.iron.skills.overall.xp !== player.hc.skills.overall.xp) {
|
||||
player.dead = true;
|
||||
player.mode = 'iron';
|
||||
}
|
||||
if (player.main.skills.overall.xp !== player.iron.skills.overall.xp) {
|
||||
player.deironed = true;
|
||||
player.mode = 'main';
|
||||
}
|
||||
} else if (ultRes.status === 200) {
|
||||
player.mode = 'ult';
|
||||
player.ult = parseStats(ultRes.data);
|
||||
if (player.iron.skills.overall.xp !== player.ult.skills.overall.xp) {
|
||||
player.deulted = true;
|
||||
player.mode = 'iron';
|
||||
}
|
||||
if (player.main.skills.overall.xp !== player.iron.skills.overall.xp) {
|
||||
player.deironed = true;
|
||||
player.mode = 'main';
|
||||
}
|
||||
} else {
|
||||
player.mode = 'iron';
|
||||
if (player.main.skills.overall.xp !== player.iron.skills.overall.xp) {
|
||||
player.deironed = true;
|
||||
player.mode = 'main';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return player;
|
||||
}
|
||||
throw Error('Player not found');
|
||||
} else {
|
||||
const response = await axios(getStatsURL(mode, rsn));
|
||||
if (response.status !== 200) {
|
||||
throw Error('Player not found');
|
||||
}
|
||||
const player: Player = {
|
||||
rsn,
|
||||
mode,
|
||||
rsn: formattedName,
|
||||
mode: 'main',
|
||||
dead: false,
|
||||
deulted: false,
|
||||
deironed: false,
|
||||
[mode]: parseStats(response.data),
|
||||
};
|
||||
player.main = parseStats(mainRes.data);
|
||||
|
||||
if (ironRes.status === 200) {
|
||||
player.iron = parseStats(ironRes.data);
|
||||
if (hcRes.status === 200) {
|
||||
player.mode = 'hc';
|
||||
player.hc = parseStats(hcRes.data);
|
||||
if (player.iron.skills.overall.xp !== player.hc.skills.overall.xp) {
|
||||
player.dead = true;
|
||||
player.mode = 'iron';
|
||||
}
|
||||
if (player.main.skills.overall.xp !== player.iron.skills.overall.xp) {
|
||||
player.deironed = true;
|
||||
player.mode = 'main';
|
||||
}
|
||||
} else if (ultRes.status === 200) {
|
||||
player.mode = 'ult';
|
||||
player.ult = parseStats(ultRes.data);
|
||||
if (player.iron.skills.overall.xp !== player.ult.skills.overall.xp) {
|
||||
player.deulted = true;
|
||||
player.mode = 'iron';
|
||||
}
|
||||
if (player.main.skills.overall.xp !== player.iron.skills.overall.xp) {
|
||||
player.deironed = true;
|
||||
player.mode = 'main';
|
||||
}
|
||||
} else {
|
||||
player.mode = 'iron';
|
||||
if (player.main.skills.overall.xp !== player.iron.skills.overall.xp) {
|
||||
player.deironed = true;
|
||||
player.mode = 'main';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return player;
|
||||
}
|
||||
throw Error('Player not found');
|
||||
}
|
||||
|
||||
export async function getStatsByGamemode(
|
||||
rsn: string,
|
||||
mode: Gamemode = 'main'
|
||||
): Promise<Stats> {
|
||||
if (typeof rsn !== 'string') {
|
||||
throw Error('RSN must be a string');
|
||||
} else if (!/^[a-zA-Z0-9 _]+$/.test(rsn)) {
|
||||
throw Error('RSN contains invalid character');
|
||||
} else if (rsn.length > 12 || rsn.length < 1) {
|
||||
throw Error('RSN must be between 1 and 12 characters');
|
||||
} else if (!GAMEMODES.includes(mode)) {
|
||||
throw Error('Invalid game mode');
|
||||
}
|
||||
const response = await axios(getStatsURL(mode, rsn));
|
||||
if (response.status !== 200) {
|
||||
throw Error('Player not found');
|
||||
}
|
||||
const stats: Stats = parseStats(response.data);
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
export async function getSkillPage(
|
||||
@@ -219,10 +218,11 @@ export function parseStats(csv: string): Stats {
|
||||
const skillObjects: Skill[] = splitCSV
|
||||
.filter(stat => stat.length === 3)
|
||||
.map(stat => {
|
||||
const [rank, level, xp] = stat;
|
||||
const skill: Skill = {
|
||||
rank: parseInt(stat[0], 10),
|
||||
level: parseInt(stat[1], 10),
|
||||
xp: parseInt(stat[2], 10),
|
||||
rank: parseInt(rank, 10),
|
||||
level: parseInt(level, 10),
|
||||
xp: parseInt(xp, 10),
|
||||
};
|
||||
return skill;
|
||||
});
|
||||
@@ -230,9 +230,10 @@ export function parseStats(csv: string): Stats {
|
||||
const activityObjects: Activity[] = splitCSV
|
||||
.filter(stat => stat.length === 2)
|
||||
.map(stat => {
|
||||
const [rank, score] = stat;
|
||||
const activity: Activity = {
|
||||
rank: parseInt(stat[0], 10),
|
||||
score: parseInt(stat[1], 10),
|
||||
rank: parseInt(rank, 10),
|
||||
score: parseInt(score, 10),
|
||||
};
|
||||
return activity;
|
||||
});
|
||||
@@ -250,13 +251,13 @@ export function parseStats(csv: string): Stats {
|
||||
{} as Skills
|
||||
);
|
||||
|
||||
const bh: BHStats = bhObjects.reduce<BHStats>(
|
||||
const bh: BH = bhObjects.reduce<BH>(
|
||||
(prev, curr, index) => {
|
||||
const newBH = { ...prev };
|
||||
newBH[BH_MODES[index]] = curr;
|
||||
return newBH;
|
||||
},
|
||||
{} as BHStats
|
||||
{} as BH
|
||||
);
|
||||
|
||||
const clues: Clues = clueObjects.reduce<Clues>(
|
||||
|
@@ -1,7 +1,5 @@
|
||||
export type Gamemode = 'main' | 'iron' | 'hc' | 'ult' | 'dmm' | 'sdmm' | 'dmmt';
|
||||
|
||||
export type Mode = Gamemode | 'full';
|
||||
|
||||
export interface Skill {
|
||||
rank: number;
|
||||
level: number;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { SkillName, ClueType, BHType, Gamemode, Mode } from '../types';
|
||||
import { SkillName, ClueType, BHType, Gamemode } from '../types';
|
||||
|
||||
export const BASE_URL = 'http://services.runescape.com/m=hiscore_oldschool';
|
||||
export const STATS_URL = 'index_lite.ws?player=';
|
||||
@@ -69,4 +69,3 @@ export const GAMEMODES: Gamemode[] = [
|
||||
'sdmm',
|
||||
'dmmt',
|
||||
];
|
||||
export const MODES: Mode[] = [...GAMEMODES, 'full'];
|
||||
|
@@ -3089,9 +3089,9 @@ lodash.zip@^4.2.0:
|
||||
integrity sha1-7GZi5IlkCO1KtsVCo5kLcswIACA=
|
||||
|
||||
lodash@^4.15.0, lodash@^4.17.11, lodash@^4.3.0:
|
||||
version "4.17.11"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
|
||||
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
|
||||
version "4.17.14"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba"
|
||||
integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==
|
||||
|
||||
log-symbols@^1.0.2:
|
||||
version "1.0.2"
|
||||
|
Reference in New Issue
Block a user