mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-04-21 11:27:38 +03:00
Merge pull request #1261 from EDCD/enhancement/945/CAPI-threaded-queries
Change all CAPI queries over to a queue/thread model
This commit is contained in:
commit
4482da007f
@ -247,10 +247,10 @@ from edmc_data import DEBUG_WEBSERVER_HOST, DEBUG_WEBSERVER_PORT
|
||||
|
||||
TARGET_URL = 'https://www.edsm.net/api-journal-v1'
|
||||
if 'edsm' in debug_senders:
|
||||
TARGET_URL = f'http://{DEBUG_WEBSERVER_HOST}:{DEBUG_WEBSERVER_PORT}/edsm'
|
||||
TARGET_URL = f'http://{DEBUG_WEBSERVER_HOST}:{DEBUG_WEBSERVER_PORT}/edsm'
|
||||
|
||||
...
|
||||
r = this.session.post(TARGET_URL, data=data, timeout=_TIMEOUT)
|
||||
r = this.requests_session.post(TARGET_URL, data=data, timeout=_TIMEOUT)
|
||||
```
|
||||
|
||||
Be sure to set a URL path in the `TARGET_URL` that denotes where the data
|
||||
|
91
EDMC.py
91
EDMC.py
@ -6,6 +6,7 @@ import argparse
|
||||
import json
|
||||
import locale
|
||||
import os
|
||||
import queue
|
||||
import re
|
||||
import sys
|
||||
from os.path import getmtime, join
|
||||
@ -270,8 +271,38 @@ sys.path: {sys.path}'''
|
||||
|
||||
companion.session.login(monitor.cmdr, monitor.is_beta)
|
||||
|
||||
###################################################################
|
||||
# Initiate CAPI queries
|
||||
querytime = int(time())
|
||||
data = companion.session.station()
|
||||
companion.session.station(query_time=querytime)
|
||||
|
||||
# Wait for the response
|
||||
_capi_request_timeout = 60
|
||||
try:
|
||||
capi_response = companion.session.capi_response_queue.get(
|
||||
block=True, timeout=_capi_request_timeout
|
||||
)
|
||||
|
||||
except queue.Empty:
|
||||
logger.error(f'CAPI requests timed out after {_capi_request_timeout} seconds')
|
||||
sys.exit(EXIT_SERVER)
|
||||
|
||||
###################################################################
|
||||
|
||||
# noinspection DuplicatedCode
|
||||
if isinstance(capi_response, companion.EDMCCAPIFailedRequest):
|
||||
logger.trace_if('capi.worker', f'Failed Request: {capi_response.message}')
|
||||
if capi_response.exception:
|
||||
raise capi_response.exception
|
||||
|
||||
else:
|
||||
raise ValueError(capi_response.message)
|
||||
|
||||
logger.trace_if('capi.worker', 'Answer is not a Failure')
|
||||
if not isinstance(capi_response, companion.EDMCCAPIResponse):
|
||||
raise ValueError(f"Response was neither CAPIFailedRequest nor EDMCAPIResponse: {type(capi_response)}")
|
||||
|
||||
data = capi_response.capi_data
|
||||
config.set('querytime', querytime)
|
||||
|
||||
# Validation
|
||||
@ -279,10 +310,10 @@ sys.path: {sys.path}'''
|
||||
logger.error("No data['command']['name'] from CAPI")
|
||||
sys.exit(EXIT_SERVER)
|
||||
|
||||
elif not deep_get(data, 'lastSystem', 'name') or \
|
||||
data['commander'].get('docked') and not \
|
||||
deep_get(data, 'lastStarport', 'name'): # Only care if docked
|
||||
|
||||
elif (
|
||||
not deep_get(data, 'lastSystem', 'name') or
|
||||
data['commander'].get('docked') and not deep_get(data, 'lastStarport', 'name')
|
||||
): # Only care if docked
|
||||
logger.error("No data['lastSystem']['name'] from CAPI")
|
||||
sys.exit(EXIT_SERVER)
|
||||
|
||||
@ -294,21 +325,20 @@ sys.path: {sys.path}'''
|
||||
pass # Skip further validation
|
||||
|
||||
elif data['commander']['name'] != monitor.cmdr:
|
||||
logger.error(f'Commander "{data["commander"]["name"]}" from CAPI doesn\'t match "{monitor.cmdr}" from Journal') # noqa: E501
|
||||
sys.exit(EXIT_CREDENTIALS)
|
||||
raise companion.CmdrError()
|
||||
|
||||
elif data['lastSystem']['name'] != monitor.system or \
|
||||
((data['commander']['docked'] and data['lastStarport']['name'] or None) != monitor.station) or \
|
||||
data['ship']['id'] != monitor.state['ShipID'] or \
|
||||
data['ship']['name'].lower() != monitor.state['ShipType']:
|
||||
|
||||
logger.error('Mismatch(es) between CAPI and Journal for at least one of: StarSystem, Last Star Port, Ship ID or Ship Name/Type') # noqa: E501
|
||||
sys.exit(EXIT_LAGGING)
|
||||
elif (
|
||||
data['lastSystem']['name'] != monitor.system or
|
||||
((data['commander']['docked'] and data['lastStarport']['name'] or None) != monitor.station) or
|
||||
data['ship']['id'] != monitor.state['ShipID'] or
|
||||
data['ship']['name'].lower() != monitor.state['ShipType']
|
||||
):
|
||||
raise companion.ServerLagging()
|
||||
|
||||
# stuff we can do when not docked
|
||||
if args.d:
|
||||
logger.debug(f'Writing raw JSON data to "{args.d}"')
|
||||
out = json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True, separators=(',', ': '))
|
||||
out = json.dumps(dict(data), ensure_ascii=False, indent=2, sort_keys=True, separators=(',', ': '))
|
||||
with open(args.d, 'wb') as f:
|
||||
f.write(out.encode("utf-8"))
|
||||
|
||||
@ -408,21 +438,44 @@ sys.path: {sys.path}'''
|
||||
try:
|
||||
eddn_sender = eddn.EDDN(None)
|
||||
logger.debug('Sending Market, Outfitting and Shipyard data to EDDN...')
|
||||
eddn_sender.export_commodities(data, monitor.is_beta)
|
||||
eddn_sender.export_outfitting(data, monitor.is_beta)
|
||||
eddn_sender.export_shipyard(data, monitor.is_beta)
|
||||
eddn_sender.export_commodities(data, monitor.is_beta, monitor.state['Odyssey'])
|
||||
eddn_sender.export_outfitting(data, monitor.is_beta, monitor.state['Odyssey'])
|
||||
eddn_sender.export_shipyard(data, monitor.is_beta, monitor.state['Odyssey'])
|
||||
|
||||
except Exception:
|
||||
logger.exception('Failed to send data to EDDN')
|
||||
|
||||
except companion.ServerError:
|
||||
logger.error('Frontier CAPI Server returned an error')
|
||||
logger.exception('Frontier CAPI Server returned an error')
|
||||
sys.exit(EXIT_SERVER)
|
||||
|
||||
except companion.ServerConnectionError:
|
||||
logger.exception('Exception while contacting server')
|
||||
sys.exit(EXIT_SERVER)
|
||||
|
||||
except companion.CredentialsError:
|
||||
logger.error('Frontier CAPI Server: Invalid Credentials')
|
||||
sys.exit(EXIT_CREDENTIALS)
|
||||
|
||||
# Companion API problem
|
||||
except companion.ServerLagging:
|
||||
logger.error(
|
||||
'Mismatch(es) between CAPI and Journal for at least one of: '
|
||||
'StarSystem, Last Star Port, Ship ID or Ship Name/Type'
|
||||
)
|
||||
sys.exit(EXIT_SERVER)
|
||||
|
||||
except companion.CmdrError: # Companion API return doesn't match Journal
|
||||
logger.error(
|
||||
f'Commander "{data["commander"]["name"]}" from CAPI doesn\'t match '
|
||||
f'"{monitor.cmdr}" from Journal'
|
||||
)
|
||||
sys.exit(EXIT_SERVER)
|
||||
|
||||
except Exception:
|
||||
logger.exception('"other" exception')
|
||||
sys.exit(EXIT_SERVER)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
@ -4,9 +4,9 @@
|
||||
|
||||
import argparse
|
||||
import html
|
||||
import json
|
||||
import locale
|
||||
import pathlib
|
||||
import queue
|
||||
import re
|
||||
import sys
|
||||
# import threading
|
||||
@ -16,7 +16,7 @@ from os import chdir, environ
|
||||
from os.path import dirname, join
|
||||
from sys import platform
|
||||
from time import localtime, strftime, time
|
||||
from typing import TYPE_CHECKING, Optional, Tuple
|
||||
from typing import TYPE_CHECKING, Optional, Tuple, Union
|
||||
|
||||
# Have this as early as possible for people running EDMarketConnector.exe
|
||||
# from cmd.exe or a bat file or similar. Else they might not be in the correct
|
||||
@ -374,6 +374,7 @@ SHIPYARD_HTML_TEMPLATE = """
|
||||
class AppWindow(object):
|
||||
"""Define the main application window."""
|
||||
|
||||
_CAPI_RESPONSE_TK_EVENT_NAME = '<<CAPIResponse>>'
|
||||
# Tkinter Event types
|
||||
EVENT_KEYPRESS = 2
|
||||
EVENT_BUTTON = 4
|
||||
@ -383,13 +384,16 @@ class AppWindow(object):
|
||||
|
||||
def __init__(self, master: tk.Tk): # noqa: C901, CCR001 # TODO - can possibly factor something out
|
||||
|
||||
self.holdofftime = config.get_int('querytime', default=0) + companion.holdoff
|
||||
self.capi_query_holdoff_time = config.get_int('querytime', default=0) + companion.capi_query_cooldown
|
||||
|
||||
self.w = master
|
||||
self.w.title(applongname)
|
||||
self.w.rowconfigure(0, weight=1)
|
||||
self.w.columnconfigure(0, weight=1)
|
||||
|
||||
# companion needs to be able to send <<CAPIResponse>> events
|
||||
companion.session.set_tk_master(self.w)
|
||||
|
||||
self.prefsdialog = None
|
||||
|
||||
# if platform == 'win32':
|
||||
@ -485,8 +489,8 @@ class AppWindow(object):
|
||||
theme.register_alternate((self.button, self.theme_button, self.theme_button),
|
||||
{'row': ui_row, 'columnspan': 2, 'sticky': tk.NSEW})
|
||||
self.status.grid(columnspan=2, sticky=tk.EW)
|
||||
self.button.bind('<Button-1>', self.getandsend)
|
||||
theme.button_bind(self.theme_button, self.getandsend)
|
||||
self.button.bind('<Button-1>', self.capi_request_data)
|
||||
theme.button_bind(self.theme_button, self.capi_request_data)
|
||||
|
||||
for child in frame.winfo_children():
|
||||
child.grid_configure(padx=self.PADX, pady=(platform != 'win32' or isinstance(child, tk.Frame)) and 2 or 0)
|
||||
@ -646,9 +650,10 @@ class AppWindow(object):
|
||||
self.w.bind('<FocusIn>', self.onenter) # Special handling for transparency
|
||||
self.w.bind('<Leave>', self.onleave) # Special handling for transparency
|
||||
self.w.bind('<FocusOut>', self.onleave) # Special handling for transparency
|
||||
self.w.bind('<Return>', self.getandsend)
|
||||
self.w.bind('<KP_Enter>', self.getandsend)
|
||||
self.w.bind_all('<<Invoke>>', self.getandsend) # Hotkey monitoring
|
||||
self.w.bind('<Return>', self.capi_request_data)
|
||||
self.w.bind('<KP_Enter>', self.capi_request_data)
|
||||
self.w.bind_all('<<Invoke>>', self.capi_request_data) # Ask for CAPI queries to be performed
|
||||
self.w.bind_all(self._CAPI_RESPONSE_TK_EVENT_NAME, self.capi_handle_response)
|
||||
self.w.bind_all('<<JournalEvent>>', self.journal_event) # Journal monitoring
|
||||
self.w.bind_all('<<DashboardEvent>>', self.dashboard_event) # Dashboard monitoring
|
||||
self.w.bind_all('<<PluginError>>', self.plugin_error) # Statusbar
|
||||
@ -886,32 +891,58 @@ class AppWindow(object):
|
||||
|
||||
return True
|
||||
|
||||
def getandsend(self, event=None, retrying: bool = False): # noqa: C901, CCR001
|
||||
def capi_request_data(self, event=None) -> None:
|
||||
"""
|
||||
Perform CAPI data retrieval and associated actions.
|
||||
|
||||
This can be triggered by hitting the main UI 'Update' button,
|
||||
automatically on docking, or due to a retry.
|
||||
|
||||
:param event: Tk generated event details.
|
||||
"""
|
||||
logger.trace_if('capi.worker', 'Begin')
|
||||
auto_update = not event
|
||||
play_sound = (auto_update or int(event.type) == self.EVENT_VIRTUAL) and not config.get_int('hotkey_mute')
|
||||
play_bad = False
|
||||
err: Optional[str] = None
|
||||
|
||||
if (
|
||||
not monitor.cmdr or not monitor.mode or monitor.state['Captain']
|
||||
or not monitor.system or monitor.mode == 'CQC'
|
||||
):
|
||||
return # In CQC or on crew - do nothing
|
||||
if not monitor.cmdr:
|
||||
logger.trace_if('capi.worker', 'Aborting Query: Cmdr unknown')
|
||||
# LANG: CAPI queries aborted because Cmdr name is unknown
|
||||
self.status['text'] = _('CAPI query aborted: Cmdr name unknown')
|
||||
return
|
||||
|
||||
if not monitor.mode:
|
||||
logger.trace_if('capi.worker', 'Aborting Query: Game Mode unknown')
|
||||
# LANG: CAPI queries aborted because game mode unknown
|
||||
self.status['text'] = _('CAPI query aborted: Game mode unknown')
|
||||
return
|
||||
|
||||
if not monitor.system:
|
||||
logger.trace_if('capi.worker', 'Aborting Query: Current star system unknown')
|
||||
# LANG: CAPI queries aborted because current star system name unknown
|
||||
self.status['text'] = _('CAPI query aborted: Current system unknown')
|
||||
return
|
||||
|
||||
if monitor.state['Captain']:
|
||||
logger.trace_if('capi.worker', 'Aborting Query: In multi-crew')
|
||||
# LANG: CAPI queries aborted because player is in multi-crew on other Cmdr's ship
|
||||
self.status['text'] = _('CAPI query aborted: In other-ship multi-crew')
|
||||
return
|
||||
|
||||
if monitor.mode == 'CQC':
|
||||
logger.trace_if('capi.worker', 'Aborting Query: In CQC')
|
||||
# LANG: CAPI queries aborted because player is in CQC (Arena)
|
||||
self.status['text'] = _('CAPI query aborted: CQC (Arena) detected')
|
||||
return
|
||||
|
||||
if companion.session.state == companion.Session.STATE_AUTH:
|
||||
logger.trace_if('capi.worker', 'Auth in progress? Aborting query')
|
||||
# Attempt another Auth
|
||||
self.login()
|
||||
return
|
||||
|
||||
if not retrying:
|
||||
if time() < self.holdofftime: # Was invoked by key while in cooldown
|
||||
if play_sound and (self.holdofftime - time()) < companion.holdoff * 0.75:
|
||||
if not companion.session.retrying:
|
||||
if time() < self.capi_query_holdoff_time: # Was invoked by key while in cooldown
|
||||
if play_sound and (self.capi_query_holdoff_time - time()) < companion.capi_query_cooldown * 0.75:
|
||||
self.status['text'] = ''
|
||||
hotkeymgr.play_bad() # Don't play sound in first few seconds to prevent repeats
|
||||
|
||||
@ -925,56 +956,91 @@ class AppWindow(object):
|
||||
self.button['state'] = self.theme_button['state'] = tk.DISABLED
|
||||
self.w.update_idletasks()
|
||||
|
||||
query_time = int(time())
|
||||
logger.trace_if('capi.worker', 'Requesting full station data')
|
||||
config.set('querytime', query_time)
|
||||
logger.trace_if('capi.worker', 'Calling companion.session.station')
|
||||
companion.session.station(
|
||||
query_time=query_time, tk_response_event=self._CAPI_RESPONSE_TK_EVENT_NAME,
|
||||
play_sound=play_sound
|
||||
)
|
||||
|
||||
def capi_handle_response(self, event=None): # noqa: C901, CCR001
|
||||
"""Handle the resulting data from a CAPI query."""
|
||||
logger.trace_if('capi.worker', 'Handling response')
|
||||
play_bad: bool = False
|
||||
err: Optional[str] = None
|
||||
|
||||
capi_response: Union[companion.EDMCCAPIFailedRequest, companion.EDMCCAPIResponse]
|
||||
try:
|
||||
querytime = int(time())
|
||||
data = companion.session.station()
|
||||
config.set('querytime', querytime)
|
||||
logger.trace_if('capi.worker', 'Pulling answer off queue')
|
||||
capi_response = companion.session.capi_response_queue.get(block=False)
|
||||
if isinstance(capi_response, companion.EDMCCAPIFailedRequest):
|
||||
logger.trace_if('capi.worker', f'Failed Request: {capi_response.message}')
|
||||
if capi_response.exception:
|
||||
raise capi_response.exception
|
||||
|
||||
else:
|
||||
raise ValueError(capi_response.message)
|
||||
|
||||
logger.trace_if('capi.worker', 'Answer is not a Failure')
|
||||
if not isinstance(capi_response, companion.EDMCCAPIResponse):
|
||||
msg = f'Response was neither CAPIFailedRequest nor EDMCAPIResponse: {type(capi_response)}'
|
||||
logger.error(msg)
|
||||
raise ValueError(msg)
|
||||
|
||||
# Validation
|
||||
if 'commander' not in data:
|
||||
if 'commander' not in capi_response.capi_data:
|
||||
# This can happen with EGS Auth if no commander created yet
|
||||
# LANG: No data was returned for the commander from the Frontier CAPI
|
||||
err = self.status['text'] = _('CAPI: No commander data returned')
|
||||
|
||||
elif not data.get('commander', {}).get('name'):
|
||||
elif not capi_response.capi_data.get('commander', {}).get('name'):
|
||||
# LANG: We didn't have the commander name when we should have
|
||||
err = self.status['text'] = _("Who are you?!") # Shouldn't happen
|
||||
|
||||
elif (not data.get('lastSystem', {}).get('name')
|
||||
or (data['commander'].get('docked')
|
||||
and not data.get('lastStarport', {}).get('name'))):
|
||||
elif (not capi_response.capi_data.get('lastSystem', {}).get('name')
|
||||
or (capi_response.capi_data['commander'].get('docked')
|
||||
and not capi_response.capi_data.get('lastStarport', {}).get('name'))):
|
||||
# LANG: We don't know where the commander is, when we should
|
||||
err = self.status['text'] = _("Where are you?!") # Shouldn't happen
|
||||
|
||||
elif not data.get('ship', {}).get('name') or not data.get('ship', {}).get('modules'):
|
||||
elif (
|
||||
not capi_response.capi_data.get('ship', {}).get('name')
|
||||
or not capi_response.capi_data.get('ship', {}).get('modules')
|
||||
):
|
||||
# LANG: We don't know what ship the commander is in, when we should
|
||||
err = self.status['text'] = _("What are you flying?!") # Shouldn't happen
|
||||
|
||||
elif monitor.cmdr and data['commander']['name'] != monitor.cmdr:
|
||||
elif monitor.cmdr and capi_response.capi_data['commander']['name'] != monitor.cmdr:
|
||||
# Companion API Commander doesn't match Journal
|
||||
logger.trace_if('capi.worker', 'Raising CmdrError()')
|
||||
raise companion.CmdrError()
|
||||
|
||||
elif auto_update and not monitor.state['OnFoot'] and not data['commander'].get('docked'):
|
||||
elif (
|
||||
capi_response.auto_update and not monitor.state['OnFoot']
|
||||
and not capi_response.capi_data['commander'].get('docked')
|
||||
):
|
||||
# auto update is only when just docked
|
||||
logger.warning(f"{auto_update!r} and not {monitor.state['OnFoot']!r} and "
|
||||
f"not {data['commander'].get('docked')!r}")
|
||||
logger.warning(f"{capi_response.auto_update!r} and not {monitor.state['OnFoot']!r} and "
|
||||
f"not {capi_response.capi_data['commander'].get('docked')!r}")
|
||||
raise companion.ServerLagging()
|
||||
|
||||
elif data['lastSystem']['name'] != monitor.system:
|
||||
elif capi_response.capi_data['lastSystem']['name'] != monitor.system:
|
||||
# CAPI system must match last journal one
|
||||
logger.warning(f"{data['lastSystem']['name']!r} != {monitor.system!r}")
|
||||
logger.warning(f"{capi_response.capi_data['lastSystem']['name']!r} != {monitor.system!r}")
|
||||
raise companion.ServerLagging()
|
||||
|
||||
elif data['lastStarport']['name'] != monitor.station:
|
||||
elif capi_response.capi_data['lastStarport']['name'] != monitor.station:
|
||||
if monitor.state['OnFoot'] and monitor.station:
|
||||
logger.warning(f"({data['lastStarport']['name']!r} != {monitor.station!r}) AND "
|
||||
logger.warning(f"({capi_response.capi_data['lastStarport']['name']!r} != {monitor.station!r}) AND "
|
||||
f"{monitor.state['OnFoot']!r} and {monitor.station!r}")
|
||||
raise companion.ServerLagging()
|
||||
|
||||
else:
|
||||
last_station = None
|
||||
if data['commander']['docked']:
|
||||
last_station = data['lastStarport']['name']
|
||||
if capi_response.capi_data['commander']['docked']:
|
||||
last_station = capi_response.capi_data['lastStarport']['name']
|
||||
|
||||
if monitor.station is None:
|
||||
# Likely (re-)Embarked on ship docked at an EDO settlement.
|
||||
@ -986,32 +1052,40 @@ class AppWindow(object):
|
||||
|
||||
if last_station != monitor.station:
|
||||
# CAPI lastStarport must match
|
||||
logger.warning(f"({data['lastStarport']['name']!r} != {monitor.station!r}) AND "
|
||||
f"{last_station!r} != {monitor.station!r}")
|
||||
logger.warning(f"({capi_response.capi_data['lastStarport']['name']!r} != {monitor.station!r})"
|
||||
f" AND {last_station!r} != {monitor.station!r}")
|
||||
raise companion.ServerLagging()
|
||||
|
||||
self.holdofftime = querytime + companion.holdoff
|
||||
self.capi_query_holdoff_time = capi_response.query_time + companion.capi_query_cooldown
|
||||
|
||||
elif not monitor.state['OnFoot'] and data['ship']['id'] != monitor.state['ShipID']:
|
||||
elif not monitor.state['OnFoot'] and capi_response.capi_data['ship']['id'] != monitor.state['ShipID']:
|
||||
# CAPI ship must match
|
||||
logger.warning(f"not {monitor.state['OnFoot']!r} and "
|
||||
f"{data['ship']['id']!r} != {monitor.state['ShipID']!r}")
|
||||
f"{capi_response.capi_data['ship']['id']!r} != {monitor.state['ShipID']!r}")
|
||||
raise companion.ServerLagging()
|
||||
|
||||
elif not monitor.state['OnFoot'] and data['ship']['name'].lower() != monitor.state['ShipType']:
|
||||
elif (
|
||||
not monitor.state['OnFoot']
|
||||
and capi_response.capi_data['ship']['name'].lower() != monitor.state['ShipType']
|
||||
):
|
||||
# CAPI ship type must match
|
||||
logger.warning(f"not {monitor.state['OnFoot']!r} and "
|
||||
f"{data['ship']['name'].lower()!r} != {monitor.state['ShipType']!r}")
|
||||
f"{capi_response.capi_data['ship']['name'].lower()!r} != "
|
||||
f"{monitor.state['ShipType']!r}")
|
||||
raise companion.ServerLagging()
|
||||
|
||||
else:
|
||||
# TODO: Change to depend on its own CL arg
|
||||
if __debug__: # Recording
|
||||
companion.session.dump_capi_data(data)
|
||||
companion.session.dump_capi_data(capi_response.capi_data)
|
||||
|
||||
if not monitor.state['ShipType']: # Started game in SRV or fighter
|
||||
self.ship['text'] = ship_name_map.get(data['ship']['name'].lower(), data['ship']['name'])
|
||||
monitor.state['ShipID'] = data['ship']['id']
|
||||
monitor.state['ShipType'] = data['ship']['name'].lower()
|
||||
self.ship['text'] = ship_name_map.get(
|
||||
capi_response.capi_data['ship']['name'].lower(),
|
||||
capi_response.capi_data['ship']['name']
|
||||
)
|
||||
monitor.state['ShipID'] = capi_response.capi_data['ship']['id']
|
||||
monitor.state['ShipType'] = capi_response.capi_data['ship']['name'].lower()
|
||||
|
||||
if not monitor.state['Modules']:
|
||||
self.ship.configure(state=tk.DISABLED)
|
||||
@ -1021,45 +1095,62 @@ class AppWindow(object):
|
||||
self.ship.configure(state=True)
|
||||
|
||||
if monitor.state.get('SuitCurrent') is not None:
|
||||
if (loadout := data.get('loadout')) is not None:
|
||||
if (loadout := capi_response.capi_data.get('loadout')) is not None:
|
||||
if (suit := loadout.get('suit')) is not None:
|
||||
if (suitname := suit.get('edmcName')) is not None:
|
||||
# We've been paranoid about loadout->suit->suitname, now just assume loadouts is there
|
||||
loadout_name = index_possibly_sparse_list(
|
||||
data['loadouts'], loadout['loadoutSlotId']
|
||||
capi_response.capi_data['loadouts'], loadout['loadoutSlotId']
|
||||
)['name']
|
||||
|
||||
self.suit['text'] = f'{suitname} ({loadout_name})'
|
||||
|
||||
self.suit_show_if_set()
|
||||
# Update Odyssey Suit data
|
||||
companion.session.suit_update(capi_response.capi_data)
|
||||
|
||||
if data['commander'].get('credits') is not None:
|
||||
monitor.state['Credits'] = data['commander']['credits']
|
||||
monitor.state['Loan'] = data['commander'].get('debt', 0)
|
||||
if capi_response.capi_data['commander'].get('credits') is not None:
|
||||
monitor.state['Credits'] = capi_response.capi_data['commander']['credits']
|
||||
monitor.state['Loan'] = capi_response.capi_data['commander'].get('debt', 0)
|
||||
|
||||
# stuff we can do when not docked
|
||||
err = plug.notify_newdata(data, monitor.is_beta)
|
||||
err = plug.notify_newdata(capi_response.capi_data, monitor.is_beta)
|
||||
self.status['text'] = err and err or ''
|
||||
if err:
|
||||
play_bad = True
|
||||
|
||||
# Export market data
|
||||
if not self.export_market_data(data):
|
||||
if not self.export_market_data(capi_response.capi_data):
|
||||
err = 'Error: Exporting Market data'
|
||||
play_bad = True
|
||||
|
||||
self.holdofftime = querytime + companion.holdoff
|
||||
self.capi_query_holdoff_time = capi_response.query_time + companion.capi_query_cooldown
|
||||
|
||||
except queue.Empty:
|
||||
logger.error('There was no response in the queue!')
|
||||
# TODO: Set status text
|
||||
return
|
||||
|
||||
except companion.ServerConnectionError:
|
||||
self.status['text'] = _('Frontier CAPI server error')
|
||||
|
||||
except companion.CredentialsError:
|
||||
companion.session.retrying = False
|
||||
companion.session.invalidate()
|
||||
companion.session.login()
|
||||
return # We need to give Auth time to complete, so can't set a timed retry
|
||||
|
||||
# Companion API problem
|
||||
except companion.ServerLagging as e:
|
||||
err = str(e)
|
||||
if retrying:
|
||||
if companion.session.retrying:
|
||||
self.status['text'] = err
|
||||
play_bad = True
|
||||
|
||||
else:
|
||||
# Retry once if Companion server is unresponsive
|
||||
self.w.after(int(SERVER_RETRY * 1000), lambda: self.getandsend(event, True))
|
||||
companion.session.retrying = True
|
||||
self.w.after(int(SERVER_RETRY * 1000), lambda: self.capi_request_data(event))
|
||||
return # early exit to avoid starting cooldown count
|
||||
|
||||
except companion.CmdrError as e: # Companion API return doesn't match Journal
|
||||
@ -1080,14 +1171,16 @@ class AppWindow(object):
|
||||
|
||||
if not err: # not self.status['text']: # no errors
|
||||
# LANG: Time when we last obtained Frontier CAPI data
|
||||
self.status['text'] = strftime(_('Last updated at %H:%M:%S'), localtime(querytime))
|
||||
self.status['text'] = strftime(_('Last updated at %H:%M:%S'), localtime(capi_response.query_time))
|
||||
|
||||
if play_sound and play_bad:
|
||||
if capi_response.play_sound and play_bad:
|
||||
hotkeymgr.play_bad()
|
||||
|
||||
logger.trace_if('capi.worker', 'Updating suit and cooldown...')
|
||||
self.update_suit_text()
|
||||
self.suit_show_if_set()
|
||||
self.cooldown()
|
||||
logger.trace_if('capi.worker', '...done')
|
||||
|
||||
def journal_event(self, event): # noqa: C901, CCR001 # Currently not easily broken up.
|
||||
"""
|
||||
@ -1222,7 +1315,7 @@ class AppWindow(object):
|
||||
logger.info('Monitor: Disable WinSparkle automatic update checks')
|
||||
|
||||
# Can't start dashboard monitoring
|
||||
if not dashboard.start(self.w, monitor.started):
|
||||
if not dashboard.start_frontier_auth(self.w, monitor.started):
|
||||
logger.info("Can't start Status monitoring")
|
||||
|
||||
# Export loadout
|
||||
@ -1264,7 +1357,7 @@ class AppWindow(object):
|
||||
auto_update = True
|
||||
|
||||
if auto_update:
|
||||
self.w.after(int(SERVER_RETRY * 1000), self.getandsend)
|
||||
self.w.after(int(SERVER_RETRY * 1000), self.capi_request_data)
|
||||
|
||||
if entry['event'] == 'ShutDown':
|
||||
# Enable WinSparkle automatic update checks
|
||||
@ -1363,10 +1456,12 @@ class AppWindow(object):
|
||||
|
||||
def cooldown(self) -> None:
|
||||
"""Display and update the cooldown timer for 'Update' button."""
|
||||
if time() < self.holdofftime:
|
||||
if time() < self.capi_query_holdoff_time:
|
||||
# Update button in main window
|
||||
self.button['text'] = self.theme_button['text'] \
|
||||
= _('cooldown {SS}s').format(SS=int(self.holdofftime - time())) # LANG: Cooldown on 'Update' button
|
||||
= _('cooldown {SS}s').format( # LANG: Cooldown on 'Update' button
|
||||
SS=int(self.capi_query_holdoff_time - time())
|
||||
)
|
||||
self.w.after(1000, self.cooldown)
|
||||
|
||||
else:
|
||||
@ -1494,47 +1589,32 @@ class AppWindow(object):
|
||||
self.destroy()
|
||||
self.__class__.showing = False
|
||||
|
||||
def save_raw(self) -> None: # noqa: CCR001 # Not easily broken up.
|
||||
"""Save newly acquired CAPI data in the configured file."""
|
||||
# LANG: Status - Attempting to retrieve data from Frontier CAPI to save to file
|
||||
self.status['text'] = _('Fetching data...')
|
||||
self.w.update_idletasks()
|
||||
def save_raw(self) -> None:
|
||||
"""
|
||||
Save any CAPI data already acquired to a file.
|
||||
|
||||
try:
|
||||
data: CAPIData = companion.session.station()
|
||||
self.status['text'] = ''
|
||||
default_extension: str = ''
|
||||
This specifically does *not* cause new queries to be performed, as the
|
||||
purpose is to aid in diagnosing any issues that occurred during 'normal'
|
||||
queries.
|
||||
"""
|
||||
default_extension: str = ''
|
||||
|
||||
if platform == 'darwin':
|
||||
default_extension = '.json'
|
||||
if platform == 'darwin':
|
||||
default_extension = '.json'
|
||||
|
||||
last_system: str = data.get("lastSystem", {}).get("name", "Unknown")
|
||||
last_starport: str = ''
|
||||
timestamp: str = strftime('%Y-%m-%dT%H.%M.%S', localtime())
|
||||
f = tkinter.filedialog.asksaveasfilename(
|
||||
parent=self.w,
|
||||
defaultextension=default_extension,
|
||||
filetypes=[('JSON', '.json'), ('All Files', '*')],
|
||||
initialdir=config.get_str('outdir'),
|
||||
initialfile=f'{monitor.system}{monitor.station}.{timestamp}'
|
||||
)
|
||||
if not f:
|
||||
return
|
||||
|
||||
if data['commander'].get('docked'):
|
||||
last_starport = '.' + data.get('lastStarport', {}).get('name', 'Unknown')
|
||||
|
||||
timestamp: str = strftime('%Y-%m-%dT%H.%M.%S', localtime())
|
||||
f = tkinter.filedialog.asksaveasfilename(
|
||||
parent=self.w,
|
||||
defaultextension=default_extension,
|
||||
filetypes=[('JSON', '.json'), ('All Files', '*')],
|
||||
initialdir=config.get_str('outdir'),
|
||||
initialfile=f'{last_system}{last_starport}.{timestamp}'
|
||||
)
|
||||
if f:
|
||||
with open(f, 'wb') as h:
|
||||
h.write(json.dumps(dict(data),
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
sort_keys=True,
|
||||
separators=(',', ': ')).encode('utf-8'))
|
||||
except companion.ServerError as e:
|
||||
self.status['text'] = str(e)
|
||||
|
||||
except Exception as e:
|
||||
logger.debug('"other" exception', exc_info=e)
|
||||
self.status['text'] = str(e)
|
||||
with open(f, 'wb') as h:
|
||||
h.write(str(companion.session.capi_raw_data).encode(encoding='utf-8'))
|
||||
|
||||
# def exit_tray(self, systray: 'SysTrayIcon') -> None:
|
||||
# """Tray icon is shutting down."""
|
||||
@ -1577,6 +1657,10 @@ class AppWindow(object):
|
||||
logger.info('Unregistering hotkey manager...')
|
||||
hotkeymgr.unregister()
|
||||
|
||||
# Now the CAPI query thread
|
||||
logger.info('Closing CAPI query thread...')
|
||||
companion.session.capi_query_close_worker()
|
||||
|
||||
# Now the main programmatic input methods
|
||||
logger.info('Closing dashboard...')
|
||||
dashboard.close()
|
||||
@ -1617,7 +1701,7 @@ class AppWindow(object):
|
||||
|
||||
def oniconify(self, event=None) -> None:
|
||||
"""Handle minimization of the application."""
|
||||
self.w.overrideredirect(0) # Can't iconize while overrideredirect
|
||||
self.w.overrideredirect(False) # Can't iconize while overrideredirect
|
||||
self.w.iconify()
|
||||
self.w.update_idletasks() # Size and windows styles get recalculated here
|
||||
self.w.wait_visibility() # Need main window to be re-created before returning
|
||||
|
269
L10n/en.template
269
L10n/en.template
@ -1,172 +1,175 @@
|
||||
/* Language name */
|
||||
"!Language" = "English";
|
||||
|
||||
/* companion.py: Frontier CAPI didn't respond; In files: companion.py:171; */
|
||||
/* companion.py: Frontier CAPI didn't respond; In files: companion.py:212; */
|
||||
"Error: Frontier CAPI didn't respond" = "Error: Frontier CAPI didn't respond";
|
||||
|
||||
/* companion.py: Frontier CAPI data doesn't agree with latest Journal game location; In files: companion.py:190; */
|
||||
/* companion.py: Frontier CAPI data doesn't agree with latest Journal game location; In files: companion.py:231; */
|
||||
"Error: Frontier server is lagging" = "Error: Frontier server is lagging";
|
||||
|
||||
/* companion.py: Commander is docked at an EDO settlement, got out and back in, we forgot the station; In files: companion.py:205; */
|
||||
/* companion.py: Commander is docked at an EDO settlement, got out and back in, we forgot the station; In files: companion.py:247; */
|
||||
"Docked but unknown station: EDO Settlement?" = "Docked but unknown station: EDO Settlement?";
|
||||
|
||||
/* companion.py: Generic "something went wrong with Frontier Auth" error; In files: companion.py:214; */
|
||||
/* companion.py: Generic "something went wrong with Frontier Auth" error; In files: companion.py:257; */
|
||||
"Error: Invalid Credentials" = "Error: Invalid Credentials";
|
||||
|
||||
/* companion.py: Frontier CAPI authorisation not for currently game-active commander; In files: companion.py:230; */
|
||||
/* companion.py: Frontier CAPI authorisation not for currently game-active commander; In files: companion.py:273; */
|
||||
"Error: Wrong Cmdr" = "Error: Wrong Cmdr";
|
||||
|
||||
/* companion.py: Generic error prefix - following text is from Frontier auth service; In files: companion.py:341; companion.py:422; */
|
||||
/* companion.py: Generic error prefix - following text is from Frontier auth service; In files: companion.py:397; companion.py:482; */
|
||||
"Error" = "Error";
|
||||
|
||||
/* companion.py: Frontier auth, no 'usr' section in returned data; companion.py: Frontier auth, no 'customer_id' in 'usr' section in returned data; In files: companion.py:380; companion.py:385; */
|
||||
/* companion.py: Frontier auth, no 'usr' section in returned data; companion.py: Frontier auth, no 'customer_id' in 'usr' section in returned data; In files: companion.py:440; companion.py:445; */
|
||||
"Error: Couldn't check token customer_id" = "Error: Couldn't check token customer_id";
|
||||
|
||||
/* companion.py: Frontier auth customer_id doesn't match game session FID; In files: companion.py:391; */
|
||||
/* companion.py: Frontier auth customer_id doesn't match game session FID; In files: companion.py:451; */
|
||||
"Error: customer_id doesn't match!" = "Error: customer_id doesn't match!";
|
||||
|
||||
/* companion.py: Failed to get Access Token from Frontier Auth service; In files: companion.py:413; */
|
||||
/* companion.py: Failed to get Access Token from Frontier Auth service; In files: companion.py:473; */
|
||||
"Error: unable to get token" = "Error: unable to get token";
|
||||
|
||||
/* companion.py: Frontier CAPI data retrieval failed; In files: companion.py:575; */
|
||||
/* companion.py: Frontier CAPI data retrieval failed; In files: companion.py:774; */
|
||||
"Frontier CAPI query failure" = "Frontier CAPI query failure";
|
||||
|
||||
/* companion.py: Frontier CAPI data retrieval failed with 5XX code; In files: companion.py:591; */
|
||||
/* companion.py: Frontier CAPI data retrieval failed with 5XX code; In files: companion.py:786; */
|
||||
"Frontier CAPI server error" = "Frontier CAPI server error";
|
||||
|
||||
/* EDMarketConnector.py: Update button in main window; In files: EDMarketConnector.py:424; EDMarketConnector.py:718; EDMarketConnector.py:1302; */
|
||||
/* EDMarketConnector.py: Update button in main window; In files: EDMarketConnector.py:485; EDMarketConnector.py:780; EDMarketConnector.py:1440; */
|
||||
"Update" = "Update";
|
||||
|
||||
/* EDMarketConnector.py: Appearance - Label for checkbox to select if application always on top; prefs.py: Appearance - Label for checkbox to select if application always on top; In files: EDMarketConnector.py:507; prefs.py:843; */
|
||||
/* EDMarketConnector.py: Appearance - Label for checkbox to select if application always on top; prefs.py: Appearance - Label for checkbox to select if application always on top; In files: EDMarketConnector.py:568; prefs.py:866; */
|
||||
"Always on top" = "Always on top";
|
||||
|
||||
/* EDMarketConnector.py: Unknown suit; In files: EDMarketConnector.py:636; */
|
||||
/* EDMarketConnector.py: Unknown suit; In files: EDMarketConnector.py:698; */
|
||||
"Unknown" = "Unknown";
|
||||
|
||||
/* EDMarketConnector.py: ED Journal file location appears to be in error; In files: EDMarketConnector.py:705; */
|
||||
/* EDMarketConnector.py: ED Journal file location appears to be in error; In files: EDMarketConnector.py:767; */
|
||||
"Error: Check E:D journal file location" = "Error: Check E:D journal file location";
|
||||
|
||||
/* EDMarketConnector.py: Label for commander name in main window; edsm.py: Game Commander name label in EDSM settings; stats.py: Cmdr stats; theme.py: Label for commander name in main window; In files: EDMarketConnector.py:712; edsm.py:219; stats.py:50; theme.py:227; */
|
||||
/* EDMarketConnector.py: Label for commander name in main window; edsm.py: Game Commander name label in EDSM settings; stats.py: Cmdr stats; theme.py: Label for commander name in main window; In files: EDMarketConnector.py:774; edsm.py:257; stats.py:50; theme.py:227; */
|
||||
"Cmdr" = "Cmdr";
|
||||
|
||||
/* EDMarketConnector.py: 'Ship' or multi-crew role label in main window, as applicable; EDMarketConnector.py: Multicrew role label in main window; In files: EDMarketConnector.py:714; EDMarketConnector.py:1072; */
|
||||
/* EDMarketConnector.py: 'Ship' or multi-crew role label in main window, as applicable; EDMarketConnector.py: Multicrew role label in main window; In files: EDMarketConnector.py:776; EDMarketConnector.py:1199; */
|
||||
"Role" = "Role";
|
||||
|
||||
/* EDMarketConnector.py: 'Ship' or multi-crew role label in main window, as applicable; EDMarketConnector.py: 'Ship' label in main UI; stats.py: Status dialog subtitle; In files: EDMarketConnector.py:714; EDMarketConnector.py:1082; EDMarketConnector.py:1105; stats.py:363; */
|
||||
/* EDMarketConnector.py: 'Ship' or multi-crew role label in main window, as applicable; EDMarketConnector.py: 'Ship' label in main UI; stats.py: Status dialog subtitle; In files: EDMarketConnector.py:776; EDMarketConnector.py:1209; EDMarketConnector.py:1232; stats.py:365; */
|
||||
"Ship" = "Ship";
|
||||
|
||||
/* EDMarketConnector.py: Label for 'Suit' line in main UI; In files: EDMarketConnector.py:715; */
|
||||
/* EDMarketConnector.py: Label for 'Suit' line in main UI; In files: EDMarketConnector.py:777; */
|
||||
"Suit" = "Suit";
|
||||
|
||||
/* EDMarketConnector.py: Label for 'System' line in main UI; prefs.py: Configuration - Label for selection of 'System' provider website; stats.py: Main window; In files: EDMarketConnector.py:716; prefs.py:605; stats.py:365; */
|
||||
/* EDMarketConnector.py: Label for 'System' line in main UI; prefs.py: Configuration - Label for selection of 'System' provider website; stats.py: Main window; In files: EDMarketConnector.py:778; prefs.py:606; stats.py:367; */
|
||||
"System" = "System";
|
||||
|
||||
/* EDMarketConnector.py: Label for 'Station' line in main UI; prefs.py: Configuration - Label for selection of 'Station' provider website; prefs.py: Appearance - Example 'Normal' text; stats.py: Status dialog subtitle; In files: EDMarketConnector.py:717; prefs.py:623; prefs.py:738; stats.py:366; */
|
||||
/* EDMarketConnector.py: Label for 'Station' line in main UI; prefs.py: Configuration - Label for selection of 'Station' provider website; prefs.py: Appearance - Example 'Normal' text; stats.py: Status dialog subtitle; In files: EDMarketConnector.py:779; prefs.py:624; prefs.py:761; stats.py:368; */
|
||||
"Station" = "Station";
|
||||
|
||||
/* EDMarketConnector.py: 'File' menu title on OSX; EDMarketConnector.py: 'File' menu title; EDMarketConnector.py: 'File' menu; In files: EDMarketConnector.py:720; EDMarketConnector.py:735; EDMarketConnector.py:738; EDMarketConnector.py:1791; */
|
||||
/* EDMarketConnector.py: 'File' menu title on OSX; EDMarketConnector.py: 'File' menu title; EDMarketConnector.py: 'File' menu; In files: EDMarketConnector.py:782; EDMarketConnector.py:797; EDMarketConnector.py:800; EDMarketConnector.py:1917; */
|
||||
"File" = "File";
|
||||
|
||||
/* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:721; EDMarketConnector.py:736; EDMarketConnector.py:739; */
|
||||
/* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:783; EDMarketConnector.py:798; EDMarketConnector.py:801; */
|
||||
"Edit" = "Edit";
|
||||
|
||||
/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:722; */
|
||||
/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:784; */
|
||||
"View" = "View";
|
||||
|
||||
/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:723; */
|
||||
/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:785; */
|
||||
"Window" = "Window";
|
||||
|
||||
/* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:724; EDMarketConnector.py:737; EDMarketConnector.py:740; */
|
||||
/* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:786; EDMarketConnector.py:799; EDMarketConnector.py:802; */
|
||||
"Help" = "Help";
|
||||
|
||||
/* EDMarketConnector.py: App menu entry on OSX; EDMarketConnector.py: Help > About App; In files: EDMarketConnector.py:727; EDMarketConnector.py:753; EDMarketConnector.py:1347; */
|
||||
/* EDMarketConnector.py: App menu entry on OSX; EDMarketConnector.py: Help > About App; In files: EDMarketConnector.py:789; EDMarketConnector.py:815; EDMarketConnector.py:1486; */
|
||||
"About {APP}" = "About {APP}";
|
||||
|
||||
/* EDMarketConnector.py: Help > Check for Updates...; In files: EDMarketConnector.py:729; EDMarketConnector.py:752; */
|
||||
/* EDMarketConnector.py: Help > Check for Updates...; In files: EDMarketConnector.py:791; EDMarketConnector.py:814; */
|
||||
"Check for Updates..." = "Check for Updates...";
|
||||
|
||||
/* EDMarketConnector.py: File > Save Raw Data...; In files: EDMarketConnector.py:730; EDMarketConnector.py:744; */
|
||||
/* EDMarketConnector.py: File > Save Raw Data...; In files: EDMarketConnector.py:792; EDMarketConnector.py:806; */
|
||||
"Save Raw Data..." = "Save Raw Data...";
|
||||
|
||||
/* EDMarketConnector.py: File > Status; stats.py: Status dialog title; In files: EDMarketConnector.py:731; EDMarketConnector.py:743; stats.py:360; */
|
||||
/* EDMarketConnector.py: File > Status; stats.py: Status dialog title; In files: EDMarketConnector.py:793; EDMarketConnector.py:805; stats.py:362; */
|
||||
"Status" = "Status";
|
||||
|
||||
/* EDMarketConnector.py: Help > Privacy Policy; In files: EDMarketConnector.py:732; EDMarketConnector.py:750; */
|
||||
/* EDMarketConnector.py: Help > Privacy Policy; In files: EDMarketConnector.py:794; EDMarketConnector.py:812; */
|
||||
"Privacy Policy" = "Privacy Policy";
|
||||
|
||||
/* EDMarketConnector.py: Help > Release Notes; In files: EDMarketConnector.py:733; EDMarketConnector.py:751; EDMarketConnector.py:1381; */
|
||||
/* EDMarketConnector.py: Help > Release Notes; In files: EDMarketConnector.py:795; EDMarketConnector.py:813; EDMarketConnector.py:1520; */
|
||||
"Release Notes" = "Release Notes";
|
||||
|
||||
/* EDMarketConnector.py: File > Settings; prefs.py: File > Settings (macOS); In files: EDMarketConnector.py:745; EDMarketConnector.py:1792; prefs.py:254; */
|
||||
/* EDMarketConnector.py: File > Settings; prefs.py: File > Settings (macOS); In files: EDMarketConnector.py:807; EDMarketConnector.py:1918; prefs.py:254; */
|
||||
"Settings" = "Settings";
|
||||
|
||||
/* EDMarketConnector.py: File > Exit; In files: EDMarketConnector.py:746; */
|
||||
/* EDMarketConnector.py: File > Exit; In files: EDMarketConnector.py:808; */
|
||||
"Exit" = "Exit";
|
||||
|
||||
/* EDMarketConnector.py: Help > Documentation; In files: EDMarketConnector.py:749; */
|
||||
/* EDMarketConnector.py: Help > Documentation; In files: EDMarketConnector.py:811; */
|
||||
"Documentation" = "Documentation";
|
||||
|
||||
/* EDMarketConnector.py: Label for 'Copy' as in 'Copy and Paste'; ttkHyperlinkLabel.py: Label for 'Copy' as in 'Copy and Paste'; In files: EDMarketConnector.py:756; ttkHyperlinkLabel.py:42; */
|
||||
/* EDMarketConnector.py: Label for 'Copy' as in 'Copy and Paste'; ttkHyperlinkLabel.py: Label for 'Copy' as in 'Copy and Paste'; In files: EDMarketConnector.py:818; ttkHyperlinkLabel.py:42; */
|
||||
"Copy" = "Copy";
|
||||
|
||||
/* EDMarketConnector.py: Status - Attempting to get a Frontier Auth Access Token; In files: EDMarketConnector.py:762; */
|
||||
/* EDMarketConnector.py: Status - Attempting to get a Frontier Auth Access Token; In files: EDMarketConnector.py:824; */
|
||||
"Logging in..." = "Logging in...";
|
||||
|
||||
/* EDMarketConnector.py: Successfully authenticated with the Frontier website; In files: EDMarketConnector.py:778; EDMarketConnector.py:1215; */
|
||||
/* EDMarketConnector.py: Successfully authenticated with the Frontier website; In files: EDMarketConnector.py:840; EDMarketConnector.py:1351; */
|
||||
"Authentication successful" = "Authentication successful";
|
||||
|
||||
/* EDMarketConnector.py: Player is not docked at a station, when we expect them to be; In files: EDMarketConnector.py:809; */
|
||||
/* EDMarketConnector.py: Player is not docked at a station, when we expect them to be; In files: EDMarketConnector.py:871; */
|
||||
"You're not docked at a station!" = "You're not docked at a station!";
|
||||
|
||||
/* EDMarketConnector.py: Status - Either no market or no modules data for station from Frontier CAPI; In files: EDMarketConnector.py:817; */
|
||||
/* EDMarketConnector.py: Status - Either no market or no modules data for station from Frontier CAPI; In files: EDMarketConnector.py:879; */
|
||||
"Station doesn't have anything!" = "Station doesn't have anything!";
|
||||
|
||||
/* EDMarketConnector.py: Status - No station market data from Frontier CAPI; In files: EDMarketConnector.py:822; */
|
||||
/* EDMarketConnector.py: Status - No station market data from Frontier CAPI; In files: EDMarketConnector.py:884; */
|
||||
"Station doesn't have a market!" = "Station doesn't have a market!";
|
||||
|
||||
/* EDMarketConnector.py: Status - Attempting to retrieve data from Frontier CAPI; EDMarketConnector.py: Status - Attempting to retrieve data from Frontier CAPI to save to file; stats.py: Fetching data from Frontier CAPI in order to display on File > Status; In files: EDMarketConnector.py:867; EDMarketConnector.py:1428; stats.py:279; */
|
||||
/* EDMarketConnector.py: CAPI queries aborted because player is in CQC (Arena); In files: EDMarketConnector.py:913; */
|
||||
"CQC detected, aborting CAPI query" = "CQC detected, aborting CAPI query";
|
||||
|
||||
/* EDMarketConnector.py: Status - Attempting to retrieve data from Frontier CAPI; stats.py: Fetching data from Frontier CAPI in order to display on File > Status; In files: EDMarketConnector.py:934; stats.py:280; */
|
||||
"Fetching data..." = "Fetching data...";
|
||||
|
||||
/* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:880; */
|
||||
/* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:973; */
|
||||
"CAPI: No commander data returned" = "CAPI: No commander data returned";
|
||||
|
||||
/* EDMarketConnector.py: We didn't have the commander name when we should have; stats.py: Unknown commander; In files: EDMarketConnector.py:884; stats.py:297; */
|
||||
/* EDMarketConnector.py: We didn't have the commander name when we should have; stats.py: Unknown commander; In files: EDMarketConnector.py:977; stats.py:299; */
|
||||
"Who are you?!" = "Who are you?!";
|
||||
|
||||
/* EDMarketConnector.py: We don't know where the commander is, when we should; stats.py: Unknown location; In files: EDMarketConnector.py:890; stats.py:307; */
|
||||
/* EDMarketConnector.py: We don't know where the commander is, when we should; stats.py: Unknown location; In files: EDMarketConnector.py:983; stats.py:309; */
|
||||
"Where are you?!" = "Where are you?!";
|
||||
|
||||
/* EDMarketConnector.py: We don't know what ship the commander is in, when we should; stats.py: Unknown ship; In files: EDMarketConnector.py:894; stats.py:312; */
|
||||
/* EDMarketConnector.py: We don't know what ship the commander is in, when we should; stats.py: Unknown ship; In files: EDMarketConnector.py:990; stats.py:314; */
|
||||
"What are you flying?!" = "What are you flying?!";
|
||||
|
||||
/* EDMarketConnector.py: Time when we last obtained Frontier CAPI data; In files: EDMarketConnector.py:1026; */
|
||||
/* EDMarketConnector.py: Time when we last obtained Frontier CAPI data; In files: EDMarketConnector.py:1145; */
|
||||
"Last updated at %H:%M:%S" = "Last updated at %H:%M:%S";
|
||||
|
||||
/* EDMarketConnector.py: Multicrew role; In files: EDMarketConnector.py:1052; */
|
||||
/* EDMarketConnector.py: Multicrew role; In files: EDMarketConnector.py:1174; */
|
||||
"Fighter" = "Fighter";
|
||||
|
||||
/* EDMarketConnector.py: Multicrew role; In files: EDMarketConnector.py:1053; */
|
||||
/* EDMarketConnector.py: Multicrew role; In files: EDMarketConnector.py:1175; */
|
||||
"Gunner" = "Gunner";
|
||||
|
||||
/* EDMarketConnector.py: Multicrew role; In files: EDMarketConnector.py:1054; */
|
||||
/* EDMarketConnector.py: Multicrew role; In files: EDMarketConnector.py:1176; */
|
||||
"Helm" = "Helm";
|
||||
|
||||
/* EDMarketConnector.py: Cooldown on 'Update' button; In files: EDMarketConnector.py:1298; */
|
||||
/* EDMarketConnector.py: Cooldown on 'Update' button; In files: EDMarketConnector.py:1434; */
|
||||
"cooldown {SS}s" = "cooldown {SS}s";
|
||||
|
||||
/* EDMarketConnector.py: Generic 'OK' button label; prefs.py: 'OK' button on Settings/Preferences window; In files: EDMarketConnector.py:1407; prefs.py:304; */
|
||||
/* EDMarketConnector.py: Generic 'OK' button label; prefs.py: 'OK' button on Settings/Preferences window; In files: EDMarketConnector.py:1546; prefs.py:305; */
|
||||
"OK" = "OK";
|
||||
|
||||
/* EDMarketConnector.py: The application is shutting down; In files: EDMarketConnector.py:1489; */
|
||||
/* EDMarketConnector.py: The application is shutting down; In files: EDMarketConnector.py:1611; */
|
||||
"Shutting down..." = "Shutting down...";
|
||||
|
||||
/* EDMarketConnector.py: Popup-text about 'active' plugins without Python 3.x support; In files: EDMarketConnector.py:1780:1786; */
|
||||
/* EDMarketConnector.py: Popup-text about 'active' plugins without Python 3.x support; In files: EDMarketConnector.py:1906:1912; */
|
||||
"One or more of your enabled plugins do not yet have support for Python 3.x. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an updated version available, else alert the developer that they need to update the code for Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "One or more of your enabled plugins do not yet have support for Python 3.x. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an updated version available, else alert the developer that they need to update the code for Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name.";
|
||||
|
||||
/* EDMarketConnector.py: Settings > Plugins tab; prefs.py: Label on Settings > Plugins tab; In files: EDMarketConnector.py:1790; prefs.py:953; */
|
||||
/* EDMarketConnector.py: Settings > Plugins tab; prefs.py: Label on Settings > Plugins tab; In files: EDMarketConnector.py:1916; prefs.py:976; */
|
||||
"Plugins" = "Plugins";
|
||||
|
||||
/* EDMarketConnector.py: Popup window title for list of 'enabled' plugins that don't work with Python 3.x; In files: EDMarketConnector.py:1801; */
|
||||
/* EDMarketConnector.py: Popup window title for list of 'enabled' plugins that don't work with Python 3.x; In files: EDMarketConnector.py:1927; */
|
||||
"EDMC: Plugins Without Python 3.x Support" = "EDMC: Plugins Without Python 3.x Support";
|
||||
|
||||
/* journal_lock.py: Title text on popup when Journal directory already locked; In files: journal_lock.py:206; */
|
||||
@ -181,7 +184,7 @@
|
||||
/* journal_lock.py: Generic 'Ignore' button label; In files: journal_lock.py:232; */
|
||||
"Ignore" = "Ignore";
|
||||
|
||||
/* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:194; prefs.py:466; prefs.py:678; prefs.py:711; */
|
||||
/* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:194; prefs.py:467; prefs.py:701; prefs.py:734; */
|
||||
"Default" = "Default";
|
||||
|
||||
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:52; coriolis.py:55; coriolis.py:101; coriolis.py:117; coriolis.py:123; */
|
||||
@ -214,193 +217,205 @@
|
||||
/* eddb.py: Journal Processing disabled due to an active killswitch; In files: eddb.py:100; */
|
||||
"EDDB Journal processing disabled. See Log." = "EDDB Journal processing disabled. See Log.";
|
||||
|
||||
/* eddn.py: Status text shown while attempting to send data; In files: eddn.py:215; eddn.py:619; eddn.py:969; */
|
||||
/* eddn.py: Status text shown while attempting to send data; In files: eddn.py:225; eddn.py:630; eddn.py:982; */
|
||||
"Sending data to EDDN..." = "Sending data to EDDN...";
|
||||
|
||||
/* eddn.py: Error while trying to send data to EDDN; In files: eddn.py:264; eddn.py:907; eddn.py:942; eddn.py:981; */
|
||||
/* eddn.py: Error while trying to send data to EDDN; In files: eddn.py:274; eddn.py:920; eddn.py:955; eddn.py:994; */
|
||||
"Error: Can't connect to EDDN" = "Error: Can't connect to EDDN";
|
||||
|
||||
/* eddn.py: EDDN has banned this version of our client; In files: eddn.py:282; */
|
||||
/* eddn.py: EDDN has banned this version of our client; In files: eddn.py:292; */
|
||||
"EDDN Error: EDMC is too old for EDDN. Please update." = "EDDN Error: EDMC is too old for EDDN. Please update.";
|
||||
|
||||
/* eddn.py: EDDN returned an error that indicates something about what we sent it was wrong; In files: eddn.py:288; */
|
||||
/* eddn.py: EDDN returned an error that indicates something about what we sent it was wrong; In files: eddn.py:298; */
|
||||
"EDDN Error: Validation Failed (EDMC Too Old?). See Log" = "EDDN Error: Validation Failed (EDMC Too Old?). See Log";
|
||||
|
||||
/* eddn.py: EDDN returned some sort of HTTP error, one we didn't expect. {STATUS} contains a number; In files: eddn.py:293; */
|
||||
/* eddn.py: EDDN returned some sort of HTTP error, one we didn't expect. {STATUS} contains a number; In files: eddn.py:303; */
|
||||
"EDDN Error: Returned {STATUS} status code" = "EDDN Error: Returned {STATUS} status code";
|
||||
|
||||
/* eddn.py: Enable EDDN support for station data checkbox label; In files: eddn.py:699; */
|
||||
/* eddn.py: Enable EDDN support for station data checkbox label; In files: eddn.py:710; */
|
||||
"Send station data to the Elite Dangerous Data Network" = "Send station data to the Elite Dangerous Data Network";
|
||||
|
||||
/* eddn.py: Enable EDDN support for system and other scan data checkbox label; In files: eddn.py:710; */
|
||||
/* eddn.py: Enable EDDN support for system and other scan data checkbox label; In files: eddn.py:721; */
|
||||
"Send system and scan data to the Elite Dangerous Data Network" = "Send system and scan data to the Elite Dangerous Data Network";
|
||||
|
||||
/* eddn.py: EDDN delay sending until docked option is on, this message notes that a send was skipped due to this; In files: eddn.py:721; */
|
||||
/* eddn.py: EDDN delay sending until docked option is on, this message notes that a send was skipped due to this; In files: eddn.py:732; */
|
||||
"Delay sending until docked" = "Delay sending until docked";
|
||||
|
||||
/* eddn.py: Killswitch disabled EDDN; In files: eddn.py:785; */
|
||||
/* eddn.py: Killswitch disabled EDDN; In files: eddn.py:796; */
|
||||
"EDDN journal handler disabled. See Log." = "EDDN journal handler disabled. See Log.";
|
||||
|
||||
/* edsm.py: Settings>EDSM - Label on checkbox for 'send data'; In files: edsm.py:198; */
|
||||
/* edsm.py: Settings>EDSM - Label on checkbox for 'send data'; In files: edsm.py:236; */
|
||||
"Send flight log and Cmdr status to EDSM" = "Send flight log and Cmdr status to EDSM";
|
||||
|
||||
/* edsm.py: Settings>EDSM - Label on header/URL to EDSM API key page; In files: edsm.py:208; */
|
||||
/* edsm.py: Settings>EDSM - Label on header/URL to EDSM API key page; In files: edsm.py:246; */
|
||||
"Elite Dangerous Star Map credentials" = "Elite Dangerous Star Map credentials";
|
||||
|
||||
/* edsm.py: EDSM Commander name label in EDSM settings; In files: edsm.py:227; */
|
||||
/* edsm.py: EDSM Commander name label in EDSM settings; In files: edsm.py:265; */
|
||||
"Commander Name" = "Commander Name";
|
||||
|
||||
/* edsm.py: EDSM API key label; inara.py: Inara API key label; In files: edsm.py:235; inara.py:237; */
|
||||
/* edsm.py: EDSM API key label; inara.py: Inara API key label; In files: edsm.py:273; inara.py:243; */
|
||||
"API Key" = "API Key";
|
||||
|
||||
/* edsm.py: We have no data on the current commander; prefs.py: No hotkey/shortcut set; stats.py: No rank; In files: edsm.py:262; prefs.py:518; prefs.py:1156; prefs.py:1189; stats.py:117; stats.py:136; stats.py:155; stats.py:172; */
|
||||
/* edsm.py: We have no data on the current commander; prefs.py: No hotkey/shortcut set; stats.py: No rank; In files: edsm.py:300; prefs.py:519; prefs.py:1173; prefs.py:1206; stats.py:117; stats.py:136; stats.py:155; stats.py:172; */
|
||||
"None" = "None";
|
||||
|
||||
/* edsm.py: EDSM plugin - Journal handling disabled by killswitch; In files: edsm.py:358; */
|
||||
/* edsm.py: EDSM plugin - Journal handling disabled by killswitch; In files: edsm.py:401; */
|
||||
"EDSM Handler disabled. See Log." = "EDSM Handler disabled. See Log.";
|
||||
|
||||
/* edsm.py: EDSM Plugin - Error message from EDSM API; In files: edsm.py:640; edsm.py:745; */
|
||||
/* edsm.py: EDSM Plugin - Error message from EDSM API; In files: edsm.py:746; edsm.py:874; */
|
||||
"Error: EDSM {MSG}" = "Error: EDSM {MSG}";
|
||||
|
||||
/* edsm.py: EDSM Plugin - Error connecting to EDSM API; In files: edsm.py:677; edsm.py:740; */
|
||||
/* edsm.py: EDSM Plugin - Error connecting to EDSM API; In files: edsm.py:783; edsm.py:869; */
|
||||
"Error: Can't connect to EDSM" = "Error: Can't connect to EDSM";
|
||||
|
||||
/* inara.py: Checkbox to enable INARA API Usage; In files: inara.py:216; */
|
||||
/* inara.py: Checkbox to enable INARA API Usage; In files: inara.py:222; */
|
||||
"Send flight log and Cmdr status to Inara" = "Send flight log and Cmdr status to Inara";
|
||||
|
||||
/* inara.py: Text for INARA API keys link ( goes to https://inara.cz/settings-api ); In files: inara.py:228; */
|
||||
/* inara.py: Text for INARA API keys link ( goes to https://inara.cz/settings-api ); In files: inara.py:234; */
|
||||
"Inara credentials" = "Inara credentials";
|
||||
|
||||
/* inara.py: INARA support disabled via killswitch; In files: inara.py:335; */
|
||||
/* inara.py: INARA support disabled via killswitch; In files: inara.py:341; */
|
||||
"Inara disabled. See Log." = "Inara disabled. See Log.";
|
||||
|
||||
/* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1549; inara.py:1562; */
|
||||
/* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1580; inara.py:1593; */
|
||||
"Error: Inara {MSG}" = "Error: Inara {MSG}";
|
||||
|
||||
/* prefs.py: File > Preferences menu entry for macOS; In files: prefs.py:250; */
|
||||
"Preferences" = "Preferences";
|
||||
|
||||
/* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:346; */
|
||||
/* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:347; */
|
||||
"Please choose what data to save" = "Please choose what data to save";
|
||||
|
||||
/* prefs.py: Settings > Output option; In files: prefs.py:352; */
|
||||
/* prefs.py: Settings > Output option; In files: prefs.py:353; */
|
||||
"Market data in CSV format file" = "Market data in CSV format file";
|
||||
|
||||
/* prefs.py: Settings > Output option; In files: prefs.py:361; */
|
||||
/* prefs.py: Settings > Output option; In files: prefs.py:362; */
|
||||
"Market data in Trade Dangerous format file" = "Market data in Trade Dangerous format file";
|
||||
|
||||
/* prefs.py: Settings > Output option; In files: prefs.py:371; */
|
||||
/* prefs.py: Settings > Output option; In files: prefs.py:372; */
|
||||
"Ship loadout" = "Ship loadout";
|
||||
|
||||
/* prefs.py: Settings > Output option; In files: prefs.py:381; */
|
||||
/* prefs.py: Settings > Output option; In files: prefs.py:382; */
|
||||
"Automatically update on docking" = "Automatically update on docking";
|
||||
|
||||
/* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:390; prefs.py:409; */
|
||||
/* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:391; prefs.py:410; */
|
||||
"File location" = "File location";
|
||||
|
||||
/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:398; prefs.py:448; */
|
||||
/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:399; prefs.py:449; */
|
||||
"Change..." = "Change...";
|
||||
|
||||
/* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:401; prefs.py:451; */
|
||||
/* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:402; prefs.py:452; */
|
||||
"Browse..." = "Browse...";
|
||||
|
||||
/* prefs.py: Label for 'Output' Settings/Preferences tab; In files: prefs.py:416; */
|
||||
/* prefs.py: Label for 'Output' Settings/Preferences tab; In files: prefs.py:417; */
|
||||
"Output" = "Output";
|
||||
|
||||
/* prefs.py: Settings > Configuration - Label for Journal files location; In files: prefs.py:442; prefs.py:457; */
|
||||
/* prefs.py: Settings > Configuration - Label for Journal files location; In files: prefs.py:443; prefs.py:458; */
|
||||
"E:D journal file location" = "E:D journal file location";
|
||||
|
||||
/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:482; */
|
||||
/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:483; */
|
||||
"Keyboard shortcut" = "Keyboard shortcut";
|
||||
|
||||
/* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:484; */
|
||||
/* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:485; */
|
||||
"Hotkey" = "Hotkey";
|
||||
|
||||
/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:493; */
|
||||
/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:494; */
|
||||
"Re-start {APP} to use shortcuts" = "Re-start {APP} to use shortcuts";
|
||||
|
||||
/* prefs.py: macOS - Configuration - need to grant the app permission for keyboard shortcuts; In files: prefs.py:502; */
|
||||
/* prefs.py: macOS - Configuration - need to grant the app permission for keyboard shortcuts; In files: prefs.py:503; */
|
||||
"{APP} needs permission to use shortcuts" = "{APP} needs permission to use shortcuts";
|
||||
|
||||
/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:507; */
|
||||
/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:508; */
|
||||
"Open System Preferences" = "Open System Preferences";
|
||||
|
||||
/* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:529; */
|
||||
/* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:530; */
|
||||
"Only when Elite: Dangerous is the active app" = "Only when Elite: Dangerous is the active app";
|
||||
|
||||
/* prefs.py: Configuration - play sound when hotkey used; In files: prefs.py:540; */
|
||||
/* prefs.py: Configuration - play sound when hotkey used; In files: prefs.py:541; */
|
||||
"Play sound" = "Play sound";
|
||||
|
||||
/* prefs.py: Configuration - disable checks for app updates when in-game; In files: prefs.py:555; */
|
||||
/* prefs.py: Configuration - disable checks for app updates when in-game; In files: prefs.py:556; */
|
||||
"Disable Automatic Application Updates Check when in-game" = "Disable Automatic Application Updates Check when in-game";
|
||||
|
||||
/* prefs.py: Label for preferred shipyard, system and station 'providers'; In files: prefs.py:568; */
|
||||
/* prefs.py: Label for preferred shipyard, system and station 'providers'; In files: prefs.py:569; */
|
||||
"Preferred websites" = "Preferred websites";
|
||||
|
||||
/* prefs.py: Label for Shipyard provider selection; In files: prefs.py:579; */
|
||||
/* prefs.py: Label for Shipyard provider selection; In files: prefs.py:580; */
|
||||
"Shipyard" = "Shipyard";
|
||||
|
||||
/* prefs.py: Label for checkbox to utilise alternative Coriolis URL method; In files: prefs.py:591; */
|
||||
/* prefs.py: Label for checkbox to utilise alternative Coriolis URL method; In files: prefs.py:592; */
|
||||
"Use alternate URL method" = "Use alternate URL method";
|
||||
|
||||
/* prefs.py: Configuration - Label for selection of Log Level; In files: prefs.py:644; */
|
||||
/* prefs.py: Configuration - Label for selection of Log Level; In files: prefs.py:645; */
|
||||
"Log Level" = "Log Level";
|
||||
|
||||
/* prefs.py: Label for 'Configuration' tab in Settings; In files: prefs.py:672; */
|
||||
/* prefs.py: Label for 'Configuration' tab in Settings; In files: prefs.py:673; */
|
||||
"Configuration" = "Configuration";
|
||||
|
||||
/* prefs.py: Label for Settings > Appeareance > selection of 'normal' text colour; In files: prefs.py:685; */
|
||||
/* prefs.py: UI elements privacy section header in privacy tab of preferences; In files: prefs.py:682; */
|
||||
"Main UI privacy options" = "Main UI privacy options";
|
||||
|
||||
/* prefs.py: Hide private group owner name from UI checkbox; In files: prefs.py:687; */
|
||||
"Hide private group name in UI" = "Hide private group name in UI";
|
||||
|
||||
/* prefs.py: Hide multicrew captain name from main UI checkbox; In files: prefs.py:691; */
|
||||
"Hide multi-crew captain name" = "Hide multi-crew captain name";
|
||||
|
||||
/* prefs.py: Preferences privacy tab title; In files: prefs.py:695; */
|
||||
"Privacy" = "Privacy";
|
||||
|
||||
/* prefs.py: Label for Settings > Appeareance > selection of 'normal' text colour; In files: prefs.py:708; */
|
||||
"Normal text" = "Normal text";
|
||||
|
||||
/* prefs.py: Label for Settings > Appeareance > selection of 'highlightes' text colour; In files: prefs.py:687; */
|
||||
/* prefs.py: Label for Settings > Appeareance > selection of 'highlightes' text colour; In files: prefs.py:710; */
|
||||
"Highlighted text" = "Highlighted text";
|
||||
|
||||
/* prefs.py: Appearance - Label for selection of application display language; In files: prefs.py:696; */
|
||||
/* prefs.py: Appearance - Label for selection of application display language; In files: prefs.py:719; */
|
||||
"Language" = "Language";
|
||||
|
||||
/* prefs.py: Label for Settings > Appearance > Theme selection; In files: prefs.py:706; */
|
||||
/* prefs.py: Label for Settings > Appearance > Theme selection; In files: prefs.py:729; */
|
||||
"Theme" = "Theme";
|
||||
|
||||
/* prefs.py: Label for 'Dark' theme radio button; In files: prefs.py:717; */
|
||||
/* prefs.py: Label for 'Dark' theme radio button; In files: prefs.py:740; */
|
||||
"Dark" = "Dark";
|
||||
|
||||
/* prefs.py: Label for 'Transparent' theme radio button; In files: prefs.py:724; */
|
||||
/* prefs.py: Label for 'Transparent' theme radio button; In files: prefs.py:747; */
|
||||
"Transparent" = "Transparent";
|
||||
|
||||
/* prefs.py: Appearance - Label for selection of UI scaling; In files: prefs.py:770; */
|
||||
/* prefs.py: Appearance - Label for selection of UI scaling; In files: prefs.py:793; */
|
||||
"UI Scale Percentage" = "UI Scale Percentage";
|
||||
|
||||
/* prefs.py: Appearance - Help/hint text for UI scaling selection; In files: prefs.py:791; */
|
||||
/* prefs.py: Appearance - Help/hint text for UI scaling selection; In files: prefs.py:814; */
|
||||
"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 means Default{CR}Restart Required for{CR}changes to take effect!";
|
||||
|
||||
/* prefs.py: Appearance - Label for selection of main window transparency; In files: prefs.py:801; */
|
||||
/* prefs.py: Appearance - Label for selection of main window transparency; In files: prefs.py:824; */
|
||||
"Main window transparency" = "Main window transparency";
|
||||
|
||||
/* prefs.py: Appearance - Help/hint text for Main window transparency selection; In files: prefs.py:821:824; */
|
||||
/* prefs.py: Appearance - Help/hint text for Main window transparency selection; In files: prefs.py:844:847; */
|
||||
"100 means fully opaque.{CR}Window is updated in real time" = "100 means fully opaque.{CR}Window is updated in real time";
|
||||
|
||||
/* prefs.py: Label for Settings > Appearance tab; In files: prefs.py:860; */
|
||||
/* prefs.py: Label for Settings > Appearance tab; In files: prefs.py:883; */
|
||||
"Appearance" = "Appearance";
|
||||
|
||||
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:875; */
|
||||
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:898; */
|
||||
"Plugins folder" = "Plugins folder";
|
||||
|
||||
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:882; */
|
||||
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:905; */
|
||||
"Open" = "Open";
|
||||
|
||||
/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:890; */
|
||||
/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:913; */
|
||||
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name";
|
||||
|
||||
/* prefs.py: Label on list of enabled plugins; In files: prefs.py:901; */
|
||||
/* prefs.py: Label on list of enabled plugins; In files: prefs.py:924; */
|
||||
"Enabled Plugins" = "Enabled Plugins";
|
||||
|
||||
/* prefs.py: Plugins - Label for list of 'enabled' plugins that don't work with Python 3.x; In files: prefs.py:921; */
|
||||
/* prefs.py: Plugins - Label for list of 'enabled' plugins that don't work with Python 3.x; In files: prefs.py:944; */
|
||||
"Plugins Without Python 3.x Support:" = "Plugins Without Python 3.x Support:";
|
||||
|
||||
/* prefs.py: Plugins - Label on URL to documentation about migrating plugins from Python 2.7; In files: prefs.py:929; */
|
||||
/* prefs.py: Plugins - Label on URL to documentation about migrating plugins from Python 2.7; In files: prefs.py:952; */
|
||||
"Information on migrating plugins" = "Information on migrating plugins";
|
||||
|
||||
/* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:944; */
|
||||
/* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:967; */
|
||||
"Disabled Plugins" = "Disabled Plugins";
|
||||
|
||||
/* stats.py: Cmdr stats; In files: stats.py:51; */
|
||||
@ -625,20 +640,8 @@
|
||||
/* stats.py: Power rank; In files: stats.py:160; */
|
||||
"Rating 5" = "Rating 5";
|
||||
|
||||
/* stats.py: Status dialog subtitle - CR value of ship; In files: stats.py:367; */
|
||||
/* stats.py: Status dialog subtitle - CR value of ship; In files: stats.py:369; */
|
||||
"Value" = "Value";
|
||||
|
||||
/* stats.py: Status dialog title; In files: stats.py:376; */
|
||||
/* stats.py: Status dialog title; In files: stats.py:378; */
|
||||
"Ships" = "Ships";
|
||||
|
||||
/* prefs.py: UI elements privacy section header in privacy tab of preferences; In files: prefs.py:682; */
|
||||
"Main UI privacy options" = "Main UI privacy options";
|
||||
|
||||
/* prefs.py: Hide private group owner name from UI checkbox; In files: prefs.py:687; */
|
||||
"Hide private group name in UI" = "Hide private group name in UI";
|
||||
|
||||
/* prefs.py: Hide multicrew captain name from main UI checkbox; In files: prefs.py:691; */
|
||||
"Hide multi-crew captain name" = "Hide multi-crew captain name";
|
||||
|
||||
/* prefs.py: Preferences privacy tab title; In files: prefs.py:695; */
|
||||
"Privacy" = "Privacy";
|
||||
|
639
companion.py
639
companion.py
@ -9,17 +9,21 @@ protocol used for the callback.
|
||||
import base64
|
||||
import collections
|
||||
import csv
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import numbers
|
||||
import os
|
||||
import random
|
||||
import threading
|
||||
import time
|
||||
import tkinter as tk
|
||||
import urllib.parse
|
||||
import webbrowser
|
||||
from builtins import object, range, str
|
||||
from email.utils import parsedate
|
||||
from os.path import join
|
||||
from queue import Queue
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, OrderedDict, TypeVar, Union
|
||||
|
||||
import requests
|
||||
@ -44,26 +48,16 @@ else:
|
||||
# Define custom type for the dicts that hold CAPI data
|
||||
# CAPIData = NewType('CAPIData', Dict)
|
||||
|
||||
holdoff = 60 # be nice
|
||||
timeout = 10 # requests timeout
|
||||
capi_query_cooldown = 60 # Minimum time between (sets of) CAPI queries
|
||||
capi_default_requests_timeout = 10
|
||||
auth_timeout = 30 # timeout for initial auth
|
||||
|
||||
# Currently the "Elite Dangerous Market Connector (EDCD/Athanasius)" one in
|
||||
# Athanasius' Frontier account
|
||||
# Obtain from https://auth.frontierstore.net/client/signup
|
||||
CLIENT_ID = os.getenv('CLIENT_ID') or 'fb88d428-9110-475f-a3d2-dc151c2b9c7a'
|
||||
SERVER_AUTH = 'https://auth.frontierstore.net'
|
||||
URL_AUTH = '/auth'
|
||||
URL_TOKEN = '/token'
|
||||
URL_DECODE = '/decode'
|
||||
|
||||
# Used by both class Auth and Session
|
||||
FRONTIER_AUTH_SERVER = 'https://auth.frontierstore.net'
|
||||
USER_AGENT = f'EDCD-{appname}-{appversion()}'
|
||||
|
||||
SERVER_LIVE = 'https://companion.orerve.net'
|
||||
SERVER_BETA = 'https://pts-companion.orerve.net'
|
||||
URL_QUERY = '/profile'
|
||||
URL_MARKET = '/market'
|
||||
URL_SHIPYARD = '/shipyard'
|
||||
|
||||
commodity_map: Dict = {}
|
||||
|
||||
@ -71,7 +65,10 @@ commodity_map: Dict = {}
|
||||
class CAPIData(UserDict):
|
||||
"""CAPI Response."""
|
||||
|
||||
def __init__(self, data: Union[str, Dict[str, Any], 'CAPIData', None] = None, source_endpoint: str = None) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
data: Union[str, Dict[str, Any], 'CAPIData', None] = None, source_endpoint: str = None
|
||||
) -> None:
|
||||
if data is None:
|
||||
super().__init__()
|
||||
elif isinstance(data, str):
|
||||
@ -86,7 +83,7 @@ class CAPIData(UserDict):
|
||||
if source_endpoint is None:
|
||||
return
|
||||
|
||||
if source_endpoint == URL_SHIPYARD and self.data.get('lastStarport'):
|
||||
if source_endpoint == Session.FRONTIER_CAPI_PATH_SHIPYARD and self.data.get('lastStarport'):
|
||||
# All the other endpoints may or may not have a lastStarport, but definitely wont have valid data
|
||||
# for this check, which means it'll just make noise for no reason while we're working on other things
|
||||
self.check_modules_ships()
|
||||
@ -128,6 +125,57 @@ class CAPIData(UserDict):
|
||||
self.data['lastStarport']['ships'] = {'shipyard_list': {}, 'unavailable_list': []}
|
||||
|
||||
|
||||
class CAPIDataEncoder(json.JSONEncoder):
|
||||
"""Allow for json dumping via specified encoder."""
|
||||
|
||||
def default(self, o):
|
||||
"""Tell JSON encoder that we're actually just a dict."""
|
||||
return o.__dict__
|
||||
|
||||
|
||||
class CAPIDataRawEndpoint:
|
||||
"""Last received CAPI response for a specific endpoint."""
|
||||
|
||||
def __init__(self, raw_data: str, query_time: datetime.datetime):
|
||||
self.query_time = query_time
|
||||
self.raw_data = raw_data
|
||||
# TODO: Maybe requests.response status ?
|
||||
|
||||
|
||||
class CAPIDataRaw:
|
||||
"""The last obtained raw CAPI response for each endpoint."""
|
||||
|
||||
raw_data: Dict[str, CAPIDataRawEndpoint] = {}
|
||||
|
||||
def record_endpoint(
|
||||
self, endpoint: str,
|
||||
raw_data: str,
|
||||
query_time: datetime.datetime
|
||||
):
|
||||
"""Record the latest raw data for the given endpoint."""
|
||||
self.raw_data[endpoint] = CAPIDataRawEndpoint(raw_data, query_time)
|
||||
|
||||
def __str__(self):
|
||||
"""Return a more readable string form of the data."""
|
||||
capi_data_str = '{'
|
||||
for k, v in self.raw_data.items():
|
||||
capi_data_str += f'"{k}":\n{{\n\t"query_time": "{v.query_time}",\n\t' \
|
||||
f'"raw_data": {v.raw_data}\n}},\n\n'
|
||||
|
||||
capi_data_str = capi_data_str.removesuffix(',\n\n')
|
||||
capi_data_str += '\n\n}'
|
||||
|
||||
return capi_data_str
|
||||
|
||||
def __iter__(self):
|
||||
"""Make this iterable on its raw_data dict."""
|
||||
yield from self.raw_data
|
||||
|
||||
def __getitem__(self, item):
|
||||
"""Make the raw_data dict's items get'able."""
|
||||
return self.raw_data.__getitem__(item)
|
||||
|
||||
|
||||
def listify(thing: Union[List, Dict]) -> List:
|
||||
"""
|
||||
Convert actual JSON array or int-indexed dict into a Python list.
|
||||
@ -236,17 +284,26 @@ class CmdrError(Exception):
|
||||
class Auth(object):
|
||||
"""Handles authentication with the Frontier CAPI service via oAuth2."""
|
||||
|
||||
# Currently the "Elite Dangerous Market Connector (EDCD/Athanasius)" one in
|
||||
# Athanasius' Frontier account
|
||||
# Obtain from https://auth.frontierstore.net/client/signup
|
||||
CLIENT_ID = os.getenv('CLIENT_ID') or 'fb88d428-9110-475f-a3d2-dc151c2b9c7a'
|
||||
|
||||
FRONTIER_AUTH_PATH_AUTH = '/auth'
|
||||
FRONTIER_AUTH_PATH_TOKEN = '/token'
|
||||
FRONTIER_AUTH_PATH_DECODE = '/decode'
|
||||
|
||||
def __init__(self, cmdr: str) -> None:
|
||||
self.cmdr: str = cmdr
|
||||
self.session = requests.Session()
|
||||
self.session.headers['User-Agent'] = USER_AGENT
|
||||
self.requests_session = requests.Session()
|
||||
self.requests_session.headers['User-Agent'] = USER_AGENT
|
||||
self.verifier: Union[bytes, None] = None
|
||||
self.state: Union[str, None] = None
|
||||
|
||||
def __del__(self) -> None:
|
||||
"""Ensure our Session is closed if we're being deleted."""
|
||||
if self.session:
|
||||
self.session.close()
|
||||
if self.requests_session:
|
||||
self.requests_session.close()
|
||||
|
||||
def refresh(self) -> Optional[str]:
|
||||
"""
|
||||
@ -271,13 +328,17 @@ class Auth(object):
|
||||
logger.debug('We have a refresh token for that idx')
|
||||
data = {
|
||||
'grant_type': 'refresh_token',
|
||||
'client_id': CLIENT_ID,
|
||||
'client_id': self.CLIENT_ID,
|
||||
'refresh_token': tokens[idx],
|
||||
}
|
||||
|
||||
logger.debug('Attempting refresh with Frontier...')
|
||||
try:
|
||||
r = self.session.post(SERVER_AUTH + URL_TOKEN, data=data, timeout=auth_timeout)
|
||||
r = self.requests_session.post(
|
||||
FRONTIER_AUTH_SERVER + self.FRONTIER_AUTH_PATH_TOKEN,
|
||||
data=data,
|
||||
timeout=auth_timeout
|
||||
)
|
||||
if r.status_code == requests.codes.ok:
|
||||
data = r.json()
|
||||
tokens[idx] = data.get('refresh_token', '')
|
||||
@ -307,10 +368,10 @@ class Auth(object):
|
||||
logger.info(f'Trying auth from scratch for Commander "{self.cmdr}"')
|
||||
challenge = self.base64_url_encode(hashlib.sha256(self.verifier).digest())
|
||||
webbrowser.open(
|
||||
f'{SERVER_AUTH}{URL_AUTH}?response_type=code'
|
||||
f'{FRONTIER_AUTH_SERVER}{self.FRONTIER_AUTH_PATH_AUTH}?response_type=code'
|
||||
f'&audience=frontier,steam,epic'
|
||||
f'&scope=auth capi'
|
||||
f'&client_id={CLIENT_ID}'
|
||||
f'&client_id={self.CLIENT_ID}'
|
||||
f'&code_challenge={challenge}'
|
||||
f'&code_challenge_method=S256'
|
||||
f'&state={self.state}'
|
||||
@ -348,7 +409,7 @@ class Auth(object):
|
||||
logger.debug('Got code, posting it back...')
|
||||
request_data = {
|
||||
'grant_type': 'authorization_code',
|
||||
'client_id': CLIENT_ID,
|
||||
'client_id': self.CLIENT_ID,
|
||||
'code_verifier': self.verifier,
|
||||
'code': data['code'][0],
|
||||
'redirect_uri': protocolhandler.redirect,
|
||||
@ -361,12 +422,16 @@ class Auth(object):
|
||||
# requests_log.setLevel(logging.DEBUG)
|
||||
# requests_log.propagate = True
|
||||
|
||||
r = self.session.post(SERVER_AUTH + URL_TOKEN, data=request_data, timeout=auth_timeout)
|
||||
r = self.requests_session.post(
|
||||
FRONTIER_AUTH_SERVER + self.FRONTIER_AUTH_PATH_TOKEN,
|
||||
data=request_data,
|
||||
timeout=auth_timeout
|
||||
)
|
||||
data_token = r.json()
|
||||
if r.status_code == requests.codes.ok:
|
||||
# Now we need to /decode the token to check the customer_id against FID
|
||||
r = self.session.get(
|
||||
SERVER_AUTH + URL_DECODE,
|
||||
r = self.requests_session.get(
|
||||
FRONTIER_AUTH_SERVER + self.FRONTIER_AUTH_PATH_DECODE,
|
||||
headers={
|
||||
'Authorization': f'Bearer {data_token.get("access_token", "")}',
|
||||
'Content-Type': 'application/json',
|
||||
@ -463,30 +528,127 @@ class Auth(object):
|
||||
return base64.urlsafe_b64encode(text).decode().replace('=', '')
|
||||
|
||||
|
||||
class EDMCCAPIReturn:
|
||||
"""Base class for Request, Failure or Response."""
|
||||
|
||||
def __init__(
|
||||
self, query_time: int, tk_response_event: Optional[str] = None,
|
||||
play_sound: bool = False, auto_update: bool = False
|
||||
):
|
||||
self.tk_response_event = tk_response_event # Name of tk event to generate when response queued.
|
||||
self.query_time: int = query_time # When this query is considered to have started (time_t).
|
||||
self.play_sound: bool = play_sound # Whether to play good/bad sounds for success/failure.
|
||||
self.auto_update: bool = auto_update # Whether this was automatically triggered.
|
||||
|
||||
|
||||
class EDMCCAPIRequest(EDMCCAPIReturn):
|
||||
"""Encapsulates a request for CAPI data."""
|
||||
|
||||
REQUEST_WORKER_SHUTDOWN = '__EDMC_WORKER_SHUTDOWN'
|
||||
|
||||
def __init__(
|
||||
self, endpoint: str, query_time: int,
|
||||
tk_response_event: Optional[str] = None,
|
||||
play_sound: bool = False, auto_update: bool = False
|
||||
):
|
||||
super().__init__(
|
||||
query_time=query_time, tk_response_event=tk_response_event,
|
||||
play_sound=play_sound, auto_update=auto_update
|
||||
)
|
||||
self.endpoint: str = endpoint # The CAPI query to perform.
|
||||
|
||||
|
||||
class EDMCCAPIResponse(EDMCCAPIReturn):
|
||||
"""Encapsulates a response from CAPI quer(y|ies)."""
|
||||
|
||||
def __init__(
|
||||
self, capi_data: CAPIData,
|
||||
query_time: int, play_sound: bool = False, auto_update: bool = False
|
||||
):
|
||||
super().__init__(query_time=query_time, play_sound=play_sound, auto_update=auto_update)
|
||||
self.capi_data: CAPIData = capi_data # Frontier CAPI response, possibly augmented (station query)
|
||||
|
||||
|
||||
class EDMCCAPIFailedRequest(EDMCCAPIReturn):
|
||||
"""CAPI failed query error class."""
|
||||
|
||||
def __init__(
|
||||
self, message: str,
|
||||
query_time: int, play_sound: bool = False, auto_update: bool = False,
|
||||
exception=None
|
||||
):
|
||||
super().__init__(query_time=query_time, play_sound=play_sound, auto_update=auto_update)
|
||||
self.message: str = message # User-friendly reason for failure.
|
||||
self.exception: int = exception # Exception that recipient should raise.
|
||||
|
||||
|
||||
class Session(object):
|
||||
"""Methods for handling an oAuth2 session."""
|
||||
"""Methods for handling Frontier Auth and CAPI queries."""
|
||||
|
||||
STATE_INIT, STATE_AUTH, STATE_OK = list(range(3))
|
||||
|
||||
FRONTIER_CAPI_PATH_PROFILE = '/profile'
|
||||
FRONTIER_CAPI_PATH_MARKET = '/market'
|
||||
FRONTIER_CAPI_PATH_SHIPYARD = '/shipyard'
|
||||
# This is a dummy value, to signal to Session.capi_query_worker that we
|
||||
# the 'station' triplet of queries.
|
||||
_CAPI_PATH_STATION = '_edmc_station'
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.state = Session.STATE_INIT
|
||||
self.server: Optional[str] = None
|
||||
self.credentials: Optional[Dict[str, Any]] = None
|
||||
self.session: Optional[requests.Session] = None
|
||||
self.requests_session: Optional[requests.Session] = None
|
||||
self.auth: Optional[Auth] = None
|
||||
self.retrying = False # Avoid infinite loop when successful auth / unsuccessful query
|
||||
self.tk_master: Optional[tk.Tk] = None
|
||||
|
||||
def login(self, cmdr: str = None, is_beta: Union[None, bool] = None) -> bool:
|
||||
self.capi_raw_data = CAPIDataRaw() # Cache of raw replies from CAPI service
|
||||
# Queue that holds requests for CAPI queries, the items should always
|
||||
# be EDMCCAPIRequest objects.
|
||||
self.capi_request_queue: Queue[EDMCCAPIRequest] = Queue()
|
||||
# This queue is used to pass the result, possibly a failure, of CAPI
|
||||
# queries back to the requesting code (technically anything checking
|
||||
# this queue, but it should be either EDMarketConnector.AppWindow or
|
||||
# EDMC.py). Items may be EDMCCAPIResponse or EDMCCAPIFailedRequest.
|
||||
self.capi_response_queue: Queue[Union[EDMCCAPIResponse, EDMCCAPIFailedRequest]] = Queue()
|
||||
logger.debug('Starting CAPI queries thread...')
|
||||
self.capi_query_thread = threading.Thread(
|
||||
target=self.capi_query_worker,
|
||||
daemon=True,
|
||||
name='CAPI worker'
|
||||
)
|
||||
self.capi_query_thread.start()
|
||||
logger.debug('Done')
|
||||
|
||||
def set_tk_master(self, master: tk.Tk) -> None:
|
||||
"""Set a reference to main UI Tk root window."""
|
||||
self.tk_master = master
|
||||
|
||||
######################################################################
|
||||
# Frontier Authorization
|
||||
######################################################################
|
||||
def start_frontier_auth(self, access_token: str) -> None:
|
||||
"""Start an oAuth2 session."""
|
||||
logger.debug('Starting session')
|
||||
self.requests_session = requests.Session()
|
||||
self.requests_session.headers['Authorization'] = f'Bearer {access_token}'
|
||||
self.requests_session.headers['User-Agent'] = USER_AGENT
|
||||
self.state = Session.STATE_OK
|
||||
|
||||
def login(self, cmdr: str = None, is_beta: Optional[bool] = None) -> bool:
|
||||
"""
|
||||
Attempt oAuth2 login.
|
||||
|
||||
:return: True if login succeeded, False if re-authorization initiated.
|
||||
"""
|
||||
if not CLIENT_ID:
|
||||
logger.error('CLIENT_ID is None')
|
||||
if not Auth.CLIENT_ID:
|
||||
logger.error('self.CLIENT_ID is None')
|
||||
raise CredentialsError('cannot login without a valid Client ID')
|
||||
|
||||
# TODO: WTF is the intent behind this logic ?
|
||||
# Perhaps to do with not even trying to auth if we're not sure if
|
||||
# it's beta, but that's moot for *auth* since oAuth2.
|
||||
if not cmdr or is_beta is None:
|
||||
# Use existing credentials
|
||||
if not self.credentials:
|
||||
@ -516,7 +678,7 @@ class Session(object):
|
||||
if access_token:
|
||||
logger.debug('We have an access_token')
|
||||
self.auth = None
|
||||
self.start(access_token)
|
||||
self.start_frontier_auth(access_token)
|
||||
return True
|
||||
|
||||
else:
|
||||
@ -536,7 +698,7 @@ class Session(object):
|
||||
|
||||
try:
|
||||
logger.debug('Trying authorize with payload from handler')
|
||||
self.start(self.auth.authorize(protocolhandler.lastpayload)) # type: ignore
|
||||
self.start_frontier_auth(self.auth.authorize(protocolhandler.lastpayload)) # type: ignore
|
||||
self.auth = None
|
||||
|
||||
except Exception:
|
||||
@ -545,176 +707,263 @@ class Session(object):
|
||||
self.auth = None
|
||||
raise # Bad thing happened
|
||||
|
||||
def start(self, access_token: str) -> None:
|
||||
"""Start an oAuth2 session."""
|
||||
logger.debug('Starting session')
|
||||
self.session = requests.Session()
|
||||
self.session.headers['Authorization'] = f'Bearer {access_token}'
|
||||
self.session.headers['User-Agent'] = USER_AGENT
|
||||
self.state = Session.STATE_OK
|
||||
def close(self) -> None:
|
||||
"""Close Frontier authorization session."""
|
||||
self.state = Session.STATE_INIT
|
||||
if self.requests_session:
|
||||
try:
|
||||
self.requests_session.close()
|
||||
|
||||
def query(self, endpoint: str) -> CAPIData: # noqa: CCR001
|
||||
"""Perform a query against the specified CAPI endpoint."""
|
||||
logger.trace_if('capi.query', f'Performing query for endpoint "{endpoint}"')
|
||||
if self.state == Session.STATE_INIT:
|
||||
if self.login():
|
||||
return self.query(endpoint)
|
||||
except Exception as e:
|
||||
logger.debug('Frontier Auth: closing', exc_info=e)
|
||||
|
||||
elif self.state == Session.STATE_AUTH:
|
||||
logger.error('cannot make a query when unauthorized')
|
||||
raise CredentialsError('cannot make a query when unauthorized')
|
||||
self.requests_session = None
|
||||
|
||||
logger.trace_if('capi.query', 'Trying...')
|
||||
if conf_module.capi_pretend_down:
|
||||
raise ServerConnectionError(f'Pretending CAPI down: {endpoint}')
|
||||
def invalidate(self) -> None:
|
||||
"""Invalidate Frontier authorization credentials."""
|
||||
logger.debug('Forcing a full re-authentication')
|
||||
# Force a full re-authentication
|
||||
self.close()
|
||||
Auth.invalidate(self.credentials['cmdr']) # type: ignore
|
||||
######################################################################
|
||||
|
||||
try:
|
||||
r = self.session.get(self.server + endpoint, timeout=timeout) # type: ignore
|
||||
######################################################################
|
||||
# CAPI queries
|
||||
######################################################################
|
||||
def capi_query_worker(self): # noqa: C901, CCR001
|
||||
"""Worker thread that performs actual CAPI queries."""
|
||||
logger.debug('CAPI worker thread starting')
|
||||
|
||||
except requests.ConnectionError as e:
|
||||
logger.warning(f'Unable to resolve name for CAPI: {e} (for request: {endpoint})')
|
||||
raise ServerConnectionError(f'Unable to connect to endpoint {endpoint}') from e
|
||||
def capi_single_query( # noqa: CCR001
|
||||
capi_endpoint: str, timeout: int = capi_default_requests_timeout
|
||||
) -> CAPIData:
|
||||
"""
|
||||
Perform a *single* CAPI endpoint query within the thread worker.
|
||||
|
||||
except Exception as e:
|
||||
logger.debug('Attempting GET', exc_info=e)
|
||||
# LANG: Frontier CAPI data retrieval failed
|
||||
raise ServerError(f'{_("Frontier CAPI query failure")}: {endpoint}') from e
|
||||
:param capi_endpoint: An actual Frontier CAPI endpoint to query.
|
||||
:param timeout: requests query timeout to use.
|
||||
:return: The resulting CAPI data, of type CAPIData.
|
||||
"""
|
||||
capi_data: CAPIData
|
||||
try:
|
||||
logger.trace_if('capi.worker', 'Sending HTTP request...')
|
||||
if conf_module.capi_pretend_down:
|
||||
raise ServerConnectionError(f'Pretending CAPI down: {capi_endpoint}')
|
||||
|
||||
if r.url.startswith(SERVER_AUTH):
|
||||
logger.info('Redirected back to Auth Server')
|
||||
# Redirected back to Auth server - force full re-authentication
|
||||
self.dump(r)
|
||||
self.invalidate()
|
||||
self.retrying = False
|
||||
self.login()
|
||||
raise CredentialsError()
|
||||
r = self.requests_session.get(self.server + capi_endpoint, timeout=timeout) # type: ignore
|
||||
logger.trace_if('capi.worker', '... got result...')
|
||||
r.raise_for_status() # Typically 403 "Forbidden" on token expiry
|
||||
# May also fail here if token expired since response is empty
|
||||
# r.status_code = 401
|
||||
# raise requests.HTTPError
|
||||
capi_json = r.json()
|
||||
capi_data = CAPIData(capi_json, capi_endpoint)
|
||||
self.capi_raw_data.record_endpoint(
|
||||
capi_endpoint, r.content.decode(encoding='utf-8'),
|
||||
datetime.datetime.utcnow()
|
||||
)
|
||||
|
||||
elif 500 <= r.status_code < 600:
|
||||
# Server error. Typically 500 "Internal Server Error" if server is down
|
||||
logger.debug('500 status back from CAPI')
|
||||
self.dump(r)
|
||||
# LANG: Frontier CAPI data retrieval failed with 5XX code
|
||||
raise ServerError(f'{_("Frontier CAPI server error")}: {r.status_code}')
|
||||
except requests.ConnectionError as e:
|
||||
logger.warning(f'Request {capi_endpoint}: {e}')
|
||||
raise ServerConnectionError(f'Unable to connect to endpoint: {capi_endpoint}') from e
|
||||
|
||||
try:
|
||||
r.raise_for_status() # Typically 403 "Forbidden" on token expiry
|
||||
data = CAPIData(r.json(), endpoint) # May also fail here if token expired since response is empty
|
||||
except requests.HTTPError as e: # In response to raise_for_status()
|
||||
logger.exception(f'Frontier CAPI Auth: GET {capi_endpoint}')
|
||||
self.dump(r)
|
||||
|
||||
except (requests.HTTPError, ValueError) as e:
|
||||
logger.exception('Frontier CAPI Auth: GET ')
|
||||
self.dump(r)
|
||||
self.close()
|
||||
if r.status_code == 401: # CAPI doesn't think we're Auth'd
|
||||
# No need for translation, we'll go straight into trying new Auth
|
||||
# and thus any message would be overwritten.
|
||||
raise CredentialsError('Frontier CAPI said Auth required') from e
|
||||
|
||||
if self.retrying: # Refresh just succeeded but this query failed! Force full re-authentication
|
||||
logger.error('Frontier CAPI Auth: query failed after refresh')
|
||||
self.invalidate()
|
||||
self.retrying = False
|
||||
self.login()
|
||||
raise CredentialsError('query failed after refresh') from e
|
||||
if r.status_code == 418: # "I'm a teapot" - used to signal maintenance
|
||||
# LANG: Frontier CAPI returned 418, meaning down for maintenance
|
||||
raise ServerError(_("Frontier CAPI down for maintenance")) from e
|
||||
|
||||
elif self.login(): # Maybe our token expired. Re-authorize in any case
|
||||
logger.debug('Initial query failed, but login() just worked, trying again...')
|
||||
self.retrying = True
|
||||
return self.query(endpoint)
|
||||
logger.exception('Frontier CAPI: Misc. Error')
|
||||
raise ServerError('Frontier CAPI: Misc. Error') from e
|
||||
|
||||
except ValueError as e:
|
||||
logger.exception(f'decoding CAPI response content:\n{r.content.decode(encoding="utf-8")}\n')
|
||||
raise ServerError("Frontier CAPI response: couldn't decode JSON") from e
|
||||
|
||||
except Exception as e:
|
||||
logger.debug('Attempting GET', exc_info=e)
|
||||
# LANG: Frontier CAPI data retrieval failed
|
||||
raise ServerError(f'{_("Frontier CAPI query failure")}: {capi_endpoint}') from e
|
||||
|
||||
if capi_endpoint == self.FRONTIER_CAPI_PATH_PROFILE and 'commander' not in capi_data:
|
||||
logger.error('No commander in returned data')
|
||||
|
||||
if 'timestamp' not in capi_data:
|
||||
capi_data['timestamp'] = time.strftime(
|
||||
'%Y-%m-%dT%H:%M:%SZ', parsedate(r.headers['Date']) # type: ignore
|
||||
)
|
||||
|
||||
return capi_data
|
||||
|
||||
def capi_station_queries(timeout: int = capi_default_requests_timeout) -> CAPIData: # noqa: CCR001
|
||||
"""
|
||||
Perform all 'station' queries for the caller.
|
||||
|
||||
A /profile query is performed to check that we are docked (or on foot)
|
||||
and the station name and marketid match the prior Docked event.
|
||||
If they do match, and the services list says they're present, also
|
||||
retrieve CAPI market and/or shipyard/outfitting data and merge into
|
||||
the /profile data.
|
||||
|
||||
:param timeout: requests timeout to use.
|
||||
:return: CAPIData instance with what we retrieved.
|
||||
"""
|
||||
station_data = capi_single_query(self.FRONTIER_CAPI_PATH_PROFILE, timeout=timeout)
|
||||
|
||||
if not station_data['commander'].get('docked') and not monitor.state['OnFoot']:
|
||||
return station_data
|
||||
|
||||
# Sanity checks in case data isn't as we expect, and maybe 'docked' flag
|
||||
# is also lagging.
|
||||
if (last_starport := station_data.get('lastStarport')) is None:
|
||||
logger.error("No lastStarport in data!")
|
||||
return station_data
|
||||
|
||||
if (
|
||||
(last_starport_name := last_starport.get('name')) is None
|
||||
or last_starport_name == ''
|
||||
):
|
||||
# This could well be valid if you've been out exploring for a long
|
||||
# time.
|
||||
logger.warning("No lastStarport name!")
|
||||
return station_data
|
||||
|
||||
# WORKAROUND: n/a | 06-08-2021: Issue 1198 and https://issues.frontierstore.net/issue-detail/40706
|
||||
# -- strip "+" chars off star port names returned by the CAPI
|
||||
last_starport_name = last_starport["name"] = last_starport_name.rstrip(" +")
|
||||
|
||||
services = last_starport.get('services', {})
|
||||
if not isinstance(services, dict):
|
||||
# Odyssey Alpha Phase 3 4.0.0.20 has been observed having
|
||||
# this be an empty list when you've jumped to another system
|
||||
# and not yet docked. As opposed to no services key at all
|
||||
# or an empty dict.
|
||||
logger.error(f'services is "{type(services)}", not dict !')
|
||||
# TODO: Change this to be dependent on its own CL arg
|
||||
if __debug__:
|
||||
self.dump_capi_data(station_data)
|
||||
|
||||
# Set an empty dict so as to not have to retest below.
|
||||
services = {}
|
||||
|
||||
last_starport_id = int(last_starport.get('id'))
|
||||
|
||||
if services.get('commodities'):
|
||||
market_data = capi_single_query(self.FRONTIER_CAPI_PATH_MARKET, timeout=timeout)
|
||||
if last_starport_id != int(market_data['id']):
|
||||
logger.warning(f"{last_starport_id!r} != {int(market_data['id'])!r}")
|
||||
raise ServerLagging()
|
||||
|
||||
else:
|
||||
market_data['name'] = last_starport_name
|
||||
station_data['lastStarport'].update(market_data)
|
||||
|
||||
if services.get('outfitting') or services.get('shipyard'):
|
||||
shipyard_data = capi_single_query(self.FRONTIER_CAPI_PATH_SHIPYARD, timeout=timeout)
|
||||
if last_starport_id != int(shipyard_data['id']):
|
||||
logger.warning(f"{last_starport_id!r} != {int(shipyard_data['id'])!r}")
|
||||
raise ServerLagging()
|
||||
|
||||
else:
|
||||
shipyard_data['name'] = last_starport_name
|
||||
station_data['lastStarport'].update(shipyard_data)
|
||||
# WORKAROUND END
|
||||
|
||||
return station_data
|
||||
|
||||
while True:
|
||||
query = self.capi_request_queue.get()
|
||||
logger.trace_if('capi.worker', 'De-queued request')
|
||||
if not isinstance(query, EDMCCAPIRequest):
|
||||
logger.error("Item from queue wasn't an EDMCCAPIRequest")
|
||||
break
|
||||
|
||||
if query.endpoint == query.REQUEST_WORKER_SHUTDOWN:
|
||||
logger.info(f'endpoint {query.REQUEST_WORKER_SHUTDOWN}, exiting...')
|
||||
break
|
||||
|
||||
logger.trace_if('capi.worker', f'Processing query: {query.endpoint}')
|
||||
capi_data: CAPIData
|
||||
try:
|
||||
if query.endpoint == self._CAPI_PATH_STATION:
|
||||
capi_data = capi_station_queries()
|
||||
|
||||
else:
|
||||
capi_data = capi_single_query(self.FRONTIER_CAPI_PATH_PROFILE)
|
||||
|
||||
except Exception as e:
|
||||
self.capi_response_queue.put(
|
||||
EDMCCAPIFailedRequest(
|
||||
message=str(e.args),
|
||||
exception=e,
|
||||
query_time=query.query_time,
|
||||
play_sound=query.play_sound,
|
||||
auto_update=query.auto_update
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
self.retrying = False
|
||||
logger.error('Frontier CAPI Auth: HTTP error or invalid JSON')
|
||||
raise CredentialsError('HTTP error or invalid JSON') from e
|
||||
self.capi_response_queue.put(
|
||||
EDMCCAPIResponse(
|
||||
capi_data=capi_data,
|
||||
query_time=query.query_time,
|
||||
play_sound=query.play_sound,
|
||||
auto_update=query.auto_update
|
||||
)
|
||||
)
|
||||
|
||||
self.retrying = False
|
||||
if 'timestamp' not in data:
|
||||
data['timestamp'] = time.strftime('%Y-%m-%dT%H:%M:%SZ', parsedate(r.headers['Date'])) # type: ignore
|
||||
# If the query came from EDMC.(py|exe) there's no tk to send an
|
||||
# event too, so assume it will be polling there response queue.
|
||||
if query.tk_response_event is not None:
|
||||
logger.trace_if('capi.worker', 'Sending <<CAPIResponse>>')
|
||||
self.tk_master.event_generate('<<CAPIResponse>>')
|
||||
|
||||
# Update Odyssey Suit data
|
||||
if endpoint == URL_QUERY:
|
||||
self.suit_update(data)
|
||||
logger.info('CAPI worker thread DONE')
|
||||
|
||||
return data
|
||||
def capi_query_close_worker(self) -> None:
|
||||
"""Ask the CAPI query thread to finish."""
|
||||
self.capi_request_queue.put(
|
||||
EDMCCAPIRequest(
|
||||
endpoint=EDMCCAPIRequest.REQUEST_WORKER_SHUTDOWN,
|
||||
query_time=int(time.time())
|
||||
)
|
||||
)
|
||||
|
||||
def profile(self) -> CAPIData:
|
||||
"""Perform general CAPI /profile endpoint query."""
|
||||
data = self.query(URL_QUERY)
|
||||
if 'commander' not in data:
|
||||
logger.error('No commander in returned data')
|
||||
|
||||
return data
|
||||
|
||||
def station(self) -> CAPIData: # noqa: CCR001
|
||||
def station(
|
||||
self, query_time: int, tk_response_event: Optional[str] = None,
|
||||
play_sound: bool = False, auto_update: bool = False
|
||||
) -> None:
|
||||
"""
|
||||
Perform CAPI quer(y|ies) for station data.
|
||||
|
||||
A /profile query is performed to check that we are docked (or on foot)
|
||||
and the station name and marketid match the prior Docked event.
|
||||
If they do match, and the services list says they're present, also
|
||||
retrieve CAPI market and/or shipyard/outfitting data and merge into
|
||||
the /profile data.
|
||||
|
||||
:return: Possibly augmented CAPI data.
|
||||
:param query_time: When this query was initiated.
|
||||
:param tk_response_event: Name of tk event to generate when response queued.
|
||||
:param play_sound: Whether the app should play a sound on error.
|
||||
:param auto_update: Whether this request was triggered automatically.
|
||||
"""
|
||||
data = self.query(URL_QUERY)
|
||||
if 'commander' not in data:
|
||||
logger.error('No commander in returned data')
|
||||
return data
|
||||
|
||||
if not data['commander'].get('docked') and not monitor.state['OnFoot']:
|
||||
return data
|
||||
|
||||
# Sanity checks in case data isn't as we expect, and maybe 'docked' flag
|
||||
# is also lagging.
|
||||
if (last_starport := data.get('lastStarport')) is None:
|
||||
logger.error("No lastStarport in data!")
|
||||
return data
|
||||
|
||||
if ((last_starport_name := last_starport.get('name')) is None
|
||||
or last_starport_name == ''):
|
||||
# This could well be valid if you've been out exploring for a long
|
||||
# time.
|
||||
logger.warning("No lastStarport name!")
|
||||
return data
|
||||
|
||||
# WORKAROUND: n/a | 06-08-2021: Issue 1198 and https://issues.frontierstore.net/issue-detail/40706
|
||||
# -- strip "+" chars off star port names returned by the CAPI
|
||||
last_starport_name = last_starport["name"] = last_starport_name.rstrip(" +")
|
||||
|
||||
services = last_starport.get('services', {})
|
||||
if not isinstance(services, dict):
|
||||
# Odyssey Alpha Phase 3 4.0.0.20 has been observed having
|
||||
# this be an empty list when you've jumped to another system
|
||||
# and not yet docked. As opposed to no services key at all
|
||||
# or an empty dict.
|
||||
logger.error(f'services is "{type(services)}", not dict !')
|
||||
if __debug__:
|
||||
self.dump_capi_data(data)
|
||||
|
||||
# Set an empty dict so as to not have to retest below.
|
||||
services = {}
|
||||
|
||||
last_starport_id = int(last_starport.get('id'))
|
||||
|
||||
if services.get('commodities'):
|
||||
marketdata = self.query(URL_MARKET)
|
||||
if last_starport_id != int(marketdata['id']):
|
||||
logger.warning(f"{last_starport_id!r} != {int(marketdata['id'])!r}")
|
||||
raise ServerLagging()
|
||||
|
||||
else:
|
||||
marketdata['name'] = last_starport_name
|
||||
data['lastStarport'].update(marketdata)
|
||||
|
||||
if services.get('outfitting') or services.get('shipyard'):
|
||||
shipdata = self.query(URL_SHIPYARD)
|
||||
if last_starport_id != int(shipdata['id']):
|
||||
logger.warning(f"{last_starport_id!r} != {int(shipdata['id'])!r}")
|
||||
raise ServerLagging()
|
||||
|
||||
else:
|
||||
shipdata['name'] = last_starport_name
|
||||
data['lastStarport'].update(shipdata)
|
||||
# WORKAROUND END
|
||||
|
||||
return data
|
||||
# Ask the thread worker to perform all three queries
|
||||
logger.trace_if('capi.worker', 'Enqueueing request')
|
||||
self.capi_request_queue.put(
|
||||
EDMCCAPIRequest(
|
||||
endpoint=self._CAPI_PATH_STATION,
|
||||
tk_response_event=tk_response_event,
|
||||
query_time=query_time,
|
||||
play_sound=play_sound,
|
||||
auto_update=auto_update
|
||||
)
|
||||
)
|
||||
######################################################################
|
||||
|
||||
######################################################################
|
||||
# Utility functions
|
||||
######################################################################
|
||||
def suit_update(self, data: CAPIData) -> None:
|
||||
"""
|
||||
Update monitor.state suit data.
|
||||
@ -752,25 +1001,6 @@ class Session(object):
|
||||
else:
|
||||
monitor.state['SuitLoadouts'] = suit_loadouts
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close CAPI authorization session."""
|
||||
self.state = Session.STATE_INIT
|
||||
if self.session:
|
||||
try:
|
||||
self.session.close()
|
||||
|
||||
except Exception as e:
|
||||
logger.debug('Frontier CAPI Auth: closing', exc_info=e)
|
||||
|
||||
self.session = None
|
||||
|
||||
def invalidate(self) -> None:
|
||||
"""Invalidate oAuth2 credentials."""
|
||||
logger.debug('Forcing a full re-authentication')
|
||||
# Force a full re-authentication
|
||||
self.close()
|
||||
Auth.invalidate(self.credentials['cmdr']) # type: ignore
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def dump(self, r: requests.Response) -> None:
|
||||
"""Log, as error, status of requests.Response from CAPI request."""
|
||||
@ -797,13 +1027,17 @@ class Session(object):
|
||||
|
||||
timestamp = time.strftime('%Y-%m-%dT%H.%M.%S', time.localtime())
|
||||
with open(f'dump/{system}{station}.{timestamp}.json', 'wb') as h:
|
||||
h.write(json.dumps(dict(data),
|
||||
h.write(json.dumps(data, cls=CAPIDataEncoder,
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
sort_keys=True,
|
||||
separators=(',', ': ')).encode('utf-8'))
|
||||
######################################################################
|
||||
|
||||
|
||||
######################################################################
|
||||
# Non-class utility functions
|
||||
######################################################################
|
||||
def fixup(data: CAPIData) -> CAPIData: # noqa: C901, CCR001 # Can't be usefully simplified
|
||||
"""
|
||||
Fix up commodity names to English & miscellaneous anomalies fixes.
|
||||
@ -940,6 +1174,7 @@ def index_possibly_sparse_list(data: Union[Mapping[str, V], List[V]], key: int)
|
||||
|
||||
else:
|
||||
raise ValueError(f'Unexpected data type {type(data)}')
|
||||
######################################################################
|
||||
|
||||
|
||||
# singleton
|
||||
|
@ -13,7 +13,10 @@ import util_ships
|
||||
|
||||
def export(data, filename=None):
|
||||
|
||||
string = json.dumps(companion.ship(data), ensure_ascii=False, indent=2, sort_keys=True, separators=(',', ': ')) # pretty print
|
||||
string = json.dumps(
|
||||
companion.ship(data), cls=companion.CAPIDataEncoder,
|
||||
ensure_ascii=False, indent=2, sort_keys=True, separators=(',', ': ')
|
||||
) # pretty print
|
||||
|
||||
if filename:
|
||||
with open(filename, 'wt') as h:
|
||||
|
@ -310,6 +310,7 @@ Msg:\n{msg}'''
|
||||
|
||||
:param data: a dict containing the starport data
|
||||
:param is_beta: whether or not we're currently in beta mode
|
||||
:param is_odyssey: whether the account shows as having Odyssey expansion.
|
||||
"""
|
||||
commodities: List[OrderedDictT[str, Any]] = []
|
||||
for commodity in data['lastStarport'].get('commodities') or []:
|
||||
@ -790,7 +791,6 @@ def journal_entry( # noqa: C901, CCR001
|
||||
:param state: `dict` - Current `monitor.state` data.
|
||||
:return: `str` - Error message, or `None` if no errors.
|
||||
"""
|
||||
|
||||
should_return, new_data = killswitch.check_killswitch('plugins.eddn.journal', entry)
|
||||
if should_return:
|
||||
plug.show_error(_('EDDN journal handler disabled. See Log.')) # LANG: Killswitch disabled EDDN
|
||||
|
37
stats.py
37
stats.py
@ -1,5 +1,6 @@
|
||||
"""CMDR Status information."""
|
||||
import csv
|
||||
import json
|
||||
import tkinter
|
||||
import tkinter as tk
|
||||
from sys import platform
|
||||
@ -275,45 +276,41 @@ class StatsDialog():
|
||||
if not monitor.cmdr:
|
||||
return
|
||||
|
||||
# LANG: Fetching data from Frontier CAPI in order to display on File > Status
|
||||
self.status['text'] = _('Fetching data...')
|
||||
self.parent.update_idletasks()
|
||||
|
||||
try:
|
||||
data = companion.session.profile()
|
||||
|
||||
except companion.ServerError as e:
|
||||
self.status['text'] = str(e)
|
||||
# TODO: This needs to use cached data
|
||||
if companion.session.FRONTIER_CAPI_PATH_PROFILE not in companion.session.capi_raw_data:
|
||||
logger.info('No cached data, aborting...')
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("error while attempting to show status")
|
||||
self.status['text'] = str(e)
|
||||
return
|
||||
capi_data = json.loads(
|
||||
companion.session.capi_raw_data[companion.session.FRONTIER_CAPI_PATH_PROFILE].raw_data
|
||||
)
|
||||
|
||||
if not data.get('commander') or not data['commander'].get('name', '').strip():
|
||||
if not capi_data.get('commander') or not capi_data['commander'].get('name', '').strip():
|
||||
# Shouldn't happen
|
||||
# LANG: Unknown commander
|
||||
self.status['text'] = _("Who are you?!")
|
||||
|
||||
elif (
|
||||
not data.get('lastSystem')
|
||||
or not data['lastSystem'].get('name', '').strip()
|
||||
or not data.get('lastStarport')
|
||||
or not data['lastStarport'].get('name', '').strip()
|
||||
not capi_data.get('lastSystem')
|
||||
or not capi_data['lastSystem'].get('name', '').strip()
|
||||
or not capi_data.get('lastStarport')
|
||||
or not capi_data['lastStarport'].get('name', '').strip()
|
||||
):
|
||||
# Shouldn't happen
|
||||
# LANG: Unknown location
|
||||
self.status['text'] = _("Where are you?!")
|
||||
|
||||
elif not data.get('ship') or not data['ship'].get('modules') or not data['ship'].get('name', '').strip():
|
||||
elif (
|
||||
not capi_data.get('ship') or not capi_data['ship'].get('modules')
|
||||
or not capi_data['ship'].get('name', '').strip()
|
||||
):
|
||||
# Shouldn't happen
|
||||
# LANG: Unknown ship
|
||||
self.status['text'] = _("What are you flying?!")
|
||||
|
||||
else:
|
||||
self.status['text'] = ''
|
||||
StatsResults(self.parent, data)
|
||||
StatsResults(self.parent, capi_data)
|
||||
|
||||
|
||||
class StatsResults(tk.Toplevel):
|
||||
|
Loading…
x
Reference in New Issue
Block a user