From 4693cd225a59b6b15b1ffb21ae5a763e5749cf51 Mon Sep 17 00:00:00 2001 From: flbraun Date: Sat, 16 Mar 2024 14:15:24 +0100 Subject: [PATCH] Craft of Exile integration This PR adds support for Craft of Exile, both fo the data scraper and the electron app. Searching for base items (e.g. "Fingerless Slilk Gloves") will now offer the addition result `Craft ` and open that base item in Craft of Exile's calculator. The Craft of Exile integration is disabled by default and must be activated in the integration selector. Resolves #40 --- assets/craftofexile.png | Bin 0 -> 6484 bytes data/craftofexile.py | 48 +++++++++++++++++++++++++++++++++++++++ data/utils.py | 1 + data/wiki.py | 7 ++++++ src/main/storage.js | 3 +++ src/main/tray.js | 9 ++++++++ src/renderer/palette.js | 11 ++++++++- src/renderer/renderer.js | 1 + 8 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 assets/craftofexile.png create mode 100644 data/craftofexile.py diff --git a/assets/craftofexile.png b/assets/craftofexile.png new file mode 100644 index 0000000000000000000000000000000000000000..77439470f16970c5a8ef53241485eb4897eeff7f GIT binary patch literal 6484 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 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', ], })