diff --git a/.gitignore b/.gitignore index a71ef04..90b9f7e 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ __pycache__ /cache /*.jar log.txt +/dump.dat /lock.file settings.json /lang/English.json diff --git a/.vscode/launch.json b/.vscode/launch.json index d94eff8..716cda6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,24 +11,6 @@ "program": "${file}", "console": "integratedTerminal" }, - { - "name": "Run App (INFO)", - "type": "python", - "request": "launch", - "program": "main.py", - "args": ["-vv"], - "console": "integratedTerminal", - "justMyCode": false - }, - { - "name": "Run App (INFO+Tray)", - "type": "python", - "request": "launch", - "program": "main.py", - "args": ["-vv", "--tray"], - "console": "integratedTerminal", - "justMyCode": false - }, { "name": "Run App (CALL)", "type": "python", @@ -38,6 +20,15 @@ "console": "integratedTerminal", "justMyCode": false }, + { + "name": "Run App (DUMP)", + "type": "python", + "request": "launch", + "program": "main.py", + "args": ["--dump"], + "console": "integratedTerminal", + "justMyCode": false + }, { "name": "Run App (DEBUG+WS)", "type": "python", diff --git a/constants.py b/constants.py index e943054..2214974 100644 --- a/constants.py +++ b/constants.py @@ -23,7 +23,7 @@ if TYPE_CHECKING: IS_APPIMAGE = "APPIMAGE" in os.environ and os.path.exists(os.environ["APPIMAGE"]) IS_PACKAGED = hasattr(sys, "_MEIPASS") or IS_APPIMAGE # logging special levels -CALL = logging.INFO - 1 +CALL: int = logging.INFO - 1 logging.addLevelName(CALL, "CALL") # site-packages venv path changes depending on the system platform if sys.platform == "win32": @@ -94,8 +94,9 @@ SITE_PACKAGES_PATH = Path(VENV_PATH, SYS_SITE_PACKAGES) LANG_PATH = _resource_path("lang") # Other Paths LOG_PATH = Path(WORKING_DIR, "log.txt") -CACHE_PATH = Path(WORKING_DIR, "cache") +DUMP_PATH = Path(WORKING_DIR, "dump.dat") LOCK_PATH = Path(WORKING_DIR, "lock.file") +CACHE_PATH = Path(WORKING_DIR, "cache") CACHE_DB = Path(CACHE_PATH, "mapping.json") COOKIES_PATH = Path(WORKING_DIR, "cookies.jar") SETTINGS_PATH = Path(WORKING_DIR, "settings.json") diff --git a/main.py b/main.py index f87c60d..cf4011c 100644 --- a/main.py +++ b/main.py @@ -53,7 +53,7 @@ if __name__ == "__main__": _debug_gql: bool log: bool tray: bool - no_run_check: bool + dump: bool # TODO: replace int with union of literal values once typeshed updates @property @@ -103,6 +103,7 @@ if __name__ == "__main__": parser.add_argument("-v", dest="_verbose", action="count", default=0) parser.add_argument("--tray", action="store_true") parser.add_argument("--log", action="store_true") + parser.add_argument("--dump", action="store_true") # undocumented debug args parser.add_argument( "--debug-ws", dest="_debug_ws", action="store_true", help=argparse.SUPPRESS diff --git a/manual.txt b/manual.txt index 7fefdbf..3a59dcb 100644 --- a/manual.txt +++ b/manual.txt @@ -5,15 +5,20 @@ Available command line arguments: • --tray - Start application as minimised into tray. + Start the application as minimised into tray. • -v Increase verbosity level. Can be stacked up several times (-vv, -vvv, etc.) to show increasingly more information during application runtime. • --log Enables logging of runtime information into a 'log.txt' file. Verbosity level of this logging matches the level set by `-v`. +• --dump + Start the application in a data-dump mode, where a 'dump.dat' file is created. + The file contains anonymous raw Twitch API data, studying of which can help troubleshoot + issues with the application. The application automatically closes shortly after launching, + once dumping is finished. • --version - Show application version information + Show application version information. Note: Additional settings are available within the application GUI. diff --git a/settings.py b/settings.py index 1e0ee1a..4bd6870 100644 --- a/settings.py +++ b/settings.py @@ -40,7 +40,7 @@ class Settings: # from args log: bool tray: bool - no_run_check: bool + dump: bool # args properties debug_ws: int debug_gql: int diff --git a/twitch.py b/twitch.py index df829d5..1548156 100644 --- a/twitch.py +++ b/twitch.py @@ -4,6 +4,7 @@ import json import asyncio import logging from time import time +from copy import deepcopy from itertools import chain from functools import partial from collections import abc, deque, OrderedDict @@ -39,6 +40,7 @@ from utils import ( ) from constants import ( CALL, + DUMP_PATH, COOKIES_PATH, GQL_OPERATIONS, MAX_CHANNELS, @@ -559,6 +561,10 @@ class Twitch: return -1 async def run(self): + if self.settings.dump: + with open(DUMP_PATH, 'w', encoding="utf8"): + # replace the existing file with an empty one + pass while True: try: await self._run() @@ -599,6 +605,9 @@ class Twitch: self.change_state(State.INVENTORY_FETCH) while True: if self._state is State.IDLE: + if self.settings.dump: + self.gui.close() + continue self.gui.status.update(_("gui", "status", "idle")) self.stop_watching() # clear the flag and wait until it's set again @@ -787,6 +796,9 @@ class Twitch: watching_channel, ) elif self._state is State.CHANNEL_SWITCH: + if self.settings.dump: + self.gui.close() + continue self.gui.status.update(_("gui", "status", "switching")) # Change into the selected channel, stay in the watching channel, # or select a new channel that meets the required conditions @@ -1412,6 +1424,26 @@ class Twitch: chunk_campaigns_data = await chunk_coro # merge the inventory and campaigns datas together inventory_data = self._merge_data(inventory_data, chunk_campaigns_data) + + if self.settings.dump: + # dump the campaigns data to the dump file + with open(DUMP_PATH, 'a', encoding="utf8") as file: + # pre-process a little, so the dump file isn't overly bloated + dump_data: JsonType = deepcopy(inventory_data) + for campaign_data in dump_data.values(): + if ( + campaign_data["allow"] + and campaign_data["allow"].get("isEnabled", True) + and campaign_data["allow"]["channels"] + ): + # simply count the channels included in the ACL + campaign_data["allow"]["channels"] = ( + f"{len(campaign_data['allow']['channels'])} channels" + ) + json.dump(dump_data, file, indent=4, sort_keys=True) + file.write("\n\n") # add a new line spacer + json.dump(claimed_benefits, file, indent=4, sort_keys=True, default=str) + # use the merged data to create campaign objects campaigns: list[DropsCampaign] = [ DropsCampaign(self, campaign_data, claimed_benefits)