diff --git a/assets/craftofexile.png b/assets/craftofexile.png new file mode 100644 index 0000000..7743947 Binary files /dev/null and b/assets/craftofexile.png differ diff --git a/data/craftofexile.py b/data/craftofexile.py new file mode 100644 index 0000000..1e59435 --- /dev/null +++ b/data/craftofexile.py @@ -0,0 +1,48 @@ +import dataclasses +import functools +import http +import json +from typing import Any + +from .types import URL +from .utils import DefaultHTTPSession + + +@dataclasses.dataclass(frozen=True) +class CraftOfExileIndex: + raw: dict[str, tuple[int, int]] # (b, bi) + + def match(self, item_name: str) -> tuple[int, int] | None: + return self.raw.get(item_name, None) + + +@functools.cache +def get_craftofexile_index(craftofexile_session: DefaultHTTPSession) -> CraftOfExileIndex: + """ + Downloads current data from Craft of Exile and makes it available as a sort-of index. + """ + index = {} + + url = 'https://www.craftofexile.com/json/data/main/poec_data.json' + res = craftofexile_session.get(url) + assert res.status_code == http.HTTPStatus.OK, f'{res.status_code} {url}' + + # the endpoint returns a JSON string, but its prefixed with some junk that + # make it invalid JSON. clean that up. + junk = 'poecd=' + assert res.text.startswith(junk), f'{res.text[:20]}' + res_parsed = json.loads(res.text[len(junk):]) + + item: dict[str, Any] + for item in res_parsed['bitems']['seq']: + index[item['name_bitem']] = (int(item['id_base']), int(item['id_bitem'])) + + return CraftOfExileIndex(raw=index) + + +def make_craftofexile_url(b: int, bi: int) -> URL | None: + """ + b is CoE's internal id for the type of crafting base, e.g. 33 for "Gloves (STR)". + bi is CoE's internal id for the concrete crafting base, e.g. 7595 for "Spiked Gloves". + """ + return f'https://www.craftofexile.com/?b={b}&bi={bi}' diff --git a/data/utils.py b/data/utils.py index 28ad941..ad73841 100644 --- a/data/utils.py +++ b/data/utils.py @@ -60,6 +60,7 @@ class Entry: tft_url: URL | None = None tool_url: URL | None = None antiquary_url: URL | None = None + craftofexile_url: URL | None = None def make_wiki_url(item_name: str) -> URL: diff --git a/data/wiki.py b/data/wiki.py index 69f1dd1..89374a8 100644 --- a/data/wiki.py +++ b/data/wiki.py @@ -6,6 +6,7 @@ from tabulate import tabulate from .antiquary import make_antiquary_url +from .craftofexile import get_craftofexile_index, make_craftofexile_url from .leagues import League from .ninja import NinjaCategory, get_ninja_index, make_ninja_url from .trade import automake_trade_url @@ -229,9 +230,11 @@ def get_items(league: League) -> Generator[Entry, None, None]: DefaultHTTPSession() as wiki_session, DefaultHTTPSession() as ninja_session, DefaultHTTPSession() as antiquary_session, + DefaultHTTPSession() as craftofexile_session, ): ninja_unknown = [] ninja_index = get_ninja_index(ninja_session, league) + craftofexile_index = get_craftofexile_index(craftofexile_session) for item in iter_wiki_query( wiki_session, @@ -272,6 +275,10 @@ def get_items(league: League) -> Generator[Entry, None, None]: if tradable: entry_kwargs['trade_url'] = automake_trade_url(league, ninja_category, name, base_item=base_item) + craftofexile_ids = craftofexile_index.match(name) + if craftofexile_ids is not None: + entry_kwargs['craftofexile_url'] = make_craftofexile_url(*craftofexile_ids) + yield Entry(**entry_kwargs) print( diff --git a/src/main/storage.js b/src/main/storage.js index 359881e..d22fb33 100644 --- a/src/main/storage.js +++ b/src/main/storage.js @@ -13,6 +13,7 @@ const userSettingsSchema = { tradeEnabled: { type: 'boolean' }, tftEnabled: { type: 'boolean' }, antiquaryEnabled: { type: 'boolean' }, + craftofexileEnabled: { type: 'boolean' }, toolsEnabled: { type: 'boolean' }, league: { type: 'string', @@ -29,6 +30,7 @@ setdefault(userSettings, 'ninjaEnabled', true) setdefault(userSettings, 'tradeEnabled', true) setdefault(userSettings, 'tftEnabled', false) setdefault(userSettings, 'antiquaryEnabled', false) +setdefault(userSettings, 'craftofexileEnabled', false) setdefault(userSettings, 'toolsEnabled', true) setdefault(userSettings, 'league', 'challenge') setdefault(userSettings, 'paletteShortcut', 'CommandOrControl+P') @@ -41,6 +43,7 @@ userSettings.getEnabledResultTypes = () => { if (userSettings.get('tradeEnabled')) enabled.push('trade') if (userSettings.get('tftEnabled')) enabled.push('tft') if (userSettings.get('antiquaryEnabled')) enabled.push('antiquary') + if (userSettings.get('craftofexileEnabled')) enabled.push('craftofexile') if (userSettings.get('toolsEnabled')) enabled.push('tools') return enabled } diff --git a/src/main/tray.js b/src/main/tray.js index 09d68cf..c89d2d3 100644 --- a/src/main/tray.js +++ b/src/main/tray.js @@ -63,6 +63,15 @@ exports.createTray = (leftClickCallback, window) => { window.webContents.send('enabledResultTypesChanged', userSettings.getEnabledResultTypes()) }, }, + { + type: 'checkbox', + label: 'Craft of Exile', + checked: userSettings.get('craftofexileEnabled'), + click: (menuItem) => { + userSettings.set('craftofexileEnabled', menuItem.checked) + window.webContents.send('enabledResultTypesChanged', userSettings.getEnabledResultTypes()) + }, + }, { type: 'checkbox', label: 'Tools', diff --git a/src/renderer/palette.js b/src/renderer/palette.js index e2893ab..ccdabc8 100644 --- a/src/renderer/palette.js +++ b/src/renderer/palette.js @@ -13,10 +13,11 @@ const ICONS = { TRADE: '../../assets/trade.png', TFT: '../../assets/tft.png', ANTIQUARY: '../../assets/antiquary.png', + CRAFTOFEXILE: '../../assets/craftofexile.png', GOTO: '../../assets/goto.png', } -const resultTypes = ['wiki', 'poedb', 'ninja', 'trade', 'tft', 'antiq', 'tool'] +const resultTypes = ['wiki', 'poedb', 'ninja', 'trade', 'tft', 'antiq', 'craft', 'tool'] const specialSearchPrefixes = resultTypes.map(e => `${e}:`) // register click handlers that hide the window when clicking outside of the palette area @@ -172,6 +173,14 @@ const makePalette = (searchInput, resultlist) => { ) { addResultNode(ICONS.ANTIQUARY, r.display_text, r.antiquary_url) } + if ( + enabledResultTypes.includes('craftofexile') + && [null, 'craft'].includes(targetedSearch) + && Object.prototype.hasOwnProperty.call(r, 'craftofexile_url') + && r.craftofexile_url !== null + ) { + addResultNode(ICONS.CRAFTOFEXILE, `Craft ${r.display_text}`, r.craftofexile_url) + } if ( enabledResultTypes.includes('tools') && [null, 'tool'].includes(targetedSearch) diff --git a/src/renderer/renderer.js b/src/renderer/renderer.js index 75318f9..e5ed5ee 100644 --- a/src/renderer/renderer.js +++ b/src/renderer/renderer.js @@ -10,6 +10,7 @@ const POEPALETTE_MINISEARCH = new MiniSearch({ 'trade_url', 'tft_url', 'antiquary_url', + 'craftofexile_url', 'tool_url', ], })