cs2tracker 2.1.11__py3-none-any.whl → 2.1.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cs2tracker might be problematic. Click here for more details.
- cs2tracker/_version.py +2 -2
- cs2tracker/app/application.py +12 -12
- cs2tracker/app/editor_frame.py +303 -76
- cs2tracker/app/scraper_frame.py +19 -2
- cs2tracker/constants.py +11 -133
- cs2tracker/data/config.ini +116 -116
- cs2tracker/data/convert_inventory.js +187 -0
- cs2tracker/data/get_inventory.js +148 -0
- cs2tracker/main.py +18 -3
- cs2tracker/scraper/background_task.py +4 -4
- cs2tracker/scraper/discord_notifier.py +4 -4
- cs2tracker/scraper/scraper.py +102 -70
- cs2tracker/util/__init__.py +2 -2
- cs2tracker/util/padded_console.py +13 -0
- cs2tracker/util/validated_config.py +55 -8
- cs2tracker-2.1.13.dist-info/METADATA +147 -0
- cs2tracker-2.1.13.dist-info/RECORD +27 -0
- cs2tracker-2.1.13.dist-info/licenses/LICENSE +402 -0
- cs2tracker-2.1.11.dist-info/METADATA +0 -82
- cs2tracker-2.1.11.dist-info/RECORD +0 -25
- cs2tracker-2.1.11.dist-info/licenses/LICENSE.md +0 -21
- {cs2tracker-2.1.11.dist-info → cs2tracker-2.1.13.dist-info}/WHEEL +0 -0
- {cs2tracker-2.1.11.dist-info → cs2tracker-2.1.13.dist-info}/entry_points.txt +0 -0
- {cs2tracker-2.1.11.dist-info → cs2tracker-2.1.13.dist-info}/top_level.txt +0 -0
cs2tracker/_version.py
CHANGED
cs2tracker/app/application.py
CHANGED
|
@@ -15,6 +15,7 @@ from cs2tracker.app.scraper_frame import ScraperFrame
|
|
|
15
15
|
from cs2tracker.constants import ICON_FILE, OS, OUTPUT_FILE, OSType
|
|
16
16
|
from cs2tracker.scraper import BackgroundTask, Scraper
|
|
17
17
|
from cs2tracker.util import PriceLogs
|
|
18
|
+
from cs2tracker.util.validated_config import get_config
|
|
18
19
|
|
|
19
20
|
APPLICATION_NAME = "CS2Tracker"
|
|
20
21
|
WINDOW_SIZE = "630x335"
|
|
@@ -24,7 +25,10 @@ SCRAPER_WINDOW_TITLE = "CS2Tracker Scraper"
|
|
|
24
25
|
SCRAPER_WINDOW_SIZE = "900x750"
|
|
25
26
|
|
|
26
27
|
CONFIG_EDITOR_TITLE = "Config Editor"
|
|
27
|
-
CONFIG_EDITOR_SIZE = "
|
|
28
|
+
CONFIG_EDITOR_SIZE = "900x750"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
config = get_config()
|
|
28
32
|
|
|
29
33
|
|
|
30
34
|
class Application:
|
|
@@ -92,9 +96,7 @@ class Application:
|
|
|
92
96
|
)
|
|
93
97
|
|
|
94
98
|
discord_webhook_checkbox_value = tk.BooleanVar(
|
|
95
|
-
value=
|
|
96
|
-
"App Settings", "discord_notifications", fallback=False
|
|
97
|
-
)
|
|
99
|
+
value=config.getboolean("App Settings", "discord_notifications", fallback=False)
|
|
98
100
|
)
|
|
99
101
|
self._add_checkbox(
|
|
100
102
|
checkbox_frame,
|
|
@@ -107,7 +109,7 @@ class Application:
|
|
|
107
109
|
)
|
|
108
110
|
|
|
109
111
|
use_proxy_checkbox_value = tk.BooleanVar(
|
|
110
|
-
value=
|
|
112
|
+
value=config.getboolean("App Settings", "use_proxy", fallback=False)
|
|
111
113
|
)
|
|
112
114
|
self._add_checkbox(
|
|
113
115
|
checkbox_frame,
|
|
@@ -181,7 +183,7 @@ class Application:
|
|
|
181
183
|
config_editor_window.geometry(CONFIG_EDITOR_SIZE)
|
|
182
184
|
config_editor_window.title(CONFIG_EDITOR_TITLE)
|
|
183
185
|
|
|
184
|
-
editor_frame = ConfigEditorFrame(config_editor_window
|
|
186
|
+
editor_frame = ConfigEditorFrame(config_editor_window)
|
|
185
187
|
editor_frame.pack(expand=True, fill="both")
|
|
186
188
|
|
|
187
189
|
def _draw_plot(self):
|
|
@@ -228,7 +230,7 @@ class Application:
|
|
|
228
230
|
|
|
229
231
|
def _toggle_use_proxy(self, enabled: bool):
|
|
230
232
|
"""Toggle whether the scraper should use proxy servers for requests."""
|
|
231
|
-
proxy_api_key =
|
|
233
|
+
proxy_api_key = config.get("User Settings", "proxy_api_key", fallback=None)
|
|
232
234
|
if not proxy_api_key and enabled:
|
|
233
235
|
messagebox.showerror(
|
|
234
236
|
"Config Error",
|
|
@@ -236,14 +238,12 @@ class Application:
|
|
|
236
238
|
)
|
|
237
239
|
return False
|
|
238
240
|
|
|
239
|
-
|
|
241
|
+
config.toggle_use_proxy(enabled)
|
|
240
242
|
return True
|
|
241
243
|
|
|
242
244
|
def _toggle_discord_webhook(self, enabled: bool):
|
|
243
245
|
"""Toggle whether the scraper should send notifications to a Discord webhook."""
|
|
244
|
-
discord_webhook_url =
|
|
245
|
-
"User Settings", "discord_webhook_url", fallback=None
|
|
246
|
-
)
|
|
246
|
+
discord_webhook_url = config.get("User Settings", "discord_webhook_url", fallback=None)
|
|
247
247
|
if not discord_webhook_url and enabled:
|
|
248
248
|
messagebox.showerror(
|
|
249
249
|
"Config Error",
|
|
@@ -251,5 +251,5 @@ class Application:
|
|
|
251
251
|
)
|
|
252
252
|
return False
|
|
253
253
|
|
|
254
|
-
|
|
254
|
+
config.toggle_discord_webhook(enabled)
|
|
255
255
|
return True
|
cs2tracker/app/editor_frame.py
CHANGED
|
@@ -1,42 +1,44 @@
|
|
|
1
1
|
import tkinter as tk
|
|
2
|
+
from queue import Empty, Queue
|
|
2
3
|
from shutil import copy
|
|
4
|
+
from subprocess import PIPE, STDOUT
|
|
5
|
+
from threading import Thread
|
|
3
6
|
from tkinter import messagebox, ttk
|
|
7
|
+
from urllib.parse import unquote
|
|
4
8
|
|
|
5
|
-
from
|
|
9
|
+
from nodejs import node
|
|
10
|
+
from ttk_text import ThemedText
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
from cs2tracker.constants import (
|
|
13
|
+
CONFIG_FILE,
|
|
14
|
+
CONFIG_FILE_BACKUP,
|
|
15
|
+
INVENTORY_IMPORT_FILE,
|
|
16
|
+
INVENTORY_IMPORT_SCRIPT,
|
|
17
|
+
)
|
|
18
|
+
from cs2tracker.util import get_config
|
|
8
19
|
|
|
9
|
-
|
|
10
|
-
|
|
20
|
+
ADD_CUSTOM_ITEM_TITLE = "Add Custom Item"
|
|
21
|
+
ADD_CUSTOM_ITEM_SIZE = "500x220"
|
|
22
|
+
|
|
23
|
+
IMPORT_INVENTORY_TITLE = "Import Steam Inventory"
|
|
24
|
+
IMPORT_INVENTORY_SIZE = "500x450"
|
|
25
|
+
|
|
26
|
+
IMPORT_INVENTORY_PROCESS_TITLE = "Importing Steam Inventory..."
|
|
27
|
+
IMPORT_INVENTORY_PROCESS_SIZE = "700x500"
|
|
28
|
+
|
|
29
|
+
config = get_config()
|
|
11
30
|
|
|
12
31
|
|
|
13
32
|
class ConfigEditorFrame(ttk.Frame):
|
|
14
|
-
def __init__(self, parent
|
|
33
|
+
def __init__(self, parent):
|
|
15
34
|
"""Initialize the configuration editor frame that allows users to view and edit
|
|
16
35
|
the configuration options.
|
|
17
36
|
"""
|
|
18
|
-
|
|
19
|
-
super().__init__(parent, style="Card.TFrame", padding=15)
|
|
37
|
+
super().__init__(parent, padding=15)
|
|
20
38
|
|
|
21
39
|
self.parent = parent
|
|
22
|
-
self.scraper = scraper
|
|
23
40
|
self._add_widgets()
|
|
24
41
|
|
|
25
|
-
def reload_config_into_tree(self):
|
|
26
|
-
"""Reload the configuration options into the treeview for display and
|
|
27
|
-
editing.
|
|
28
|
-
"""
|
|
29
|
-
for item in self.tree.get_children():
|
|
30
|
-
self.tree.delete(item)
|
|
31
|
-
|
|
32
|
-
for section in self.scraper.config.sections():
|
|
33
|
-
if section == "App Settings":
|
|
34
|
-
continue
|
|
35
|
-
section_level = self.tree.insert("", "end", iid=section, text=section)
|
|
36
|
-
for config_option, value in self.scraper.config.items(section):
|
|
37
|
-
title_option = config_option.replace("_", " ").title()
|
|
38
|
-
self.tree.insert(section_level, "end", text=title_option, values=[value])
|
|
39
|
-
|
|
40
42
|
def _add_widgets(self):
|
|
41
43
|
"""Configure the main editor frame which displays the configuration options in a
|
|
42
44
|
structured way.
|
|
@@ -44,12 +46,14 @@ class ConfigEditorFrame(ttk.Frame):
|
|
|
44
46
|
self._configure_treeview()
|
|
45
47
|
self.tree.pack(expand=True, fill="both")
|
|
46
48
|
|
|
47
|
-
button_frame = ConfigEditorButtonFrame(self, self.
|
|
49
|
+
button_frame = ConfigEditorButtonFrame(self, self.tree)
|
|
48
50
|
button_frame.pack(side="bottom", padx=10, pady=(0, 10))
|
|
49
51
|
|
|
50
52
|
def _set_cell_value(self, event):
|
|
51
|
-
"""
|
|
52
|
-
clicked.
|
|
53
|
+
"""
|
|
54
|
+
Set the value of a cell in the treeview to be editable when double- clicked.
|
|
55
|
+
|
|
56
|
+
Source: https://stackoverflow.com/questions/75787251/create-an-editable-tkinter-treeview-with-keyword-connection
|
|
53
57
|
"""
|
|
54
58
|
|
|
55
59
|
def save_edit(event):
|
|
@@ -63,7 +67,7 @@ class ConfigEditorFrame(ttk.Frame):
|
|
|
63
67
|
if item_text.strip() == "":
|
|
64
68
|
left_item_text = self.tree.item(row, "text")
|
|
65
69
|
# Don't allow editing of section headers
|
|
66
|
-
if any(left_item_text == section for section in
|
|
70
|
+
if any(left_item_text == section for section in config.sections()):
|
|
67
71
|
return
|
|
68
72
|
x, y, w, h = self.tree.bbox(row, column)
|
|
69
73
|
entryedit = ttk.Entry(self)
|
|
@@ -89,11 +93,8 @@ class ConfigEditorFrame(ttk.Frame):
|
|
|
89
93
|
event.widget.destroy()
|
|
90
94
|
|
|
91
95
|
def _make_tree_editable(self):
|
|
92
|
-
"""
|
|
93
|
-
|
|
94
|
-
value.
|
|
95
|
-
|
|
96
|
-
Source: https://stackoverflow.com/questions/75787251/create-an-editable-tkinter-treeview-with-keyword-connection
|
|
96
|
+
"""Add a binding to the treeview that allows double-clicking on a cell to edit
|
|
97
|
+
its value.
|
|
97
98
|
"""
|
|
98
99
|
self.tree.bind("<Double-1>", self._set_cell_value)
|
|
99
100
|
self.parent.bind("<MouseWheel>", self._destroy_entries) # type: ignore
|
|
@@ -101,13 +102,25 @@ class ConfigEditorFrame(ttk.Frame):
|
|
|
101
102
|
|
|
102
103
|
def _load_config_into_tree(self):
|
|
103
104
|
"""Load the configuration options into the treeview for display and editing."""
|
|
104
|
-
for section in
|
|
105
|
+
for section in config.sections():
|
|
105
106
|
if section == "App Settings":
|
|
106
107
|
continue
|
|
107
108
|
section_level = self.tree.insert("", "end", iid=section, text=section)
|
|
108
|
-
for config_option, value in
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
for config_option, value in config.items(section):
|
|
110
|
+
if section == "Custom Items":
|
|
111
|
+
custom_item_name = unquote(config_option.split("/")[-1])
|
|
112
|
+
self.tree.insert(section_level, "end", text=custom_item_name, values=[value])
|
|
113
|
+
else:
|
|
114
|
+
option_name = config_option.replace("_", " ").title()
|
|
115
|
+
self.tree.insert(section_level, "end", text=option_name, values=[value])
|
|
116
|
+
|
|
117
|
+
def reload_config_into_tree(self):
|
|
118
|
+
"""Reload the configuration options into the treeview for display and
|
|
119
|
+
editing.
|
|
120
|
+
"""
|
|
121
|
+
for item in self.tree.get_children():
|
|
122
|
+
self.tree.delete(item)
|
|
123
|
+
self._load_config_into_tree()
|
|
111
124
|
|
|
112
125
|
def _configure_treeview(self):
|
|
113
126
|
"""Configure a treeview to display and edit configuration options."""
|
|
@@ -124,7 +137,7 @@ class ConfigEditorFrame(ttk.Frame):
|
|
|
124
137
|
scrollbar.config(command=self.tree.yview)
|
|
125
138
|
|
|
126
139
|
self.tree.column("#0", anchor="w", width=200)
|
|
127
|
-
self.tree.column(1, anchor="
|
|
140
|
+
self.tree.column(1, anchor="center", width=25)
|
|
128
141
|
self.tree.heading("#0", text="Option")
|
|
129
142
|
self.tree.heading(1, text="Value")
|
|
130
143
|
|
|
@@ -133,15 +146,13 @@ class ConfigEditorFrame(ttk.Frame):
|
|
|
133
146
|
|
|
134
147
|
|
|
135
148
|
class ConfigEditorButtonFrame(ttk.Frame):
|
|
136
|
-
def __init__(self, parent,
|
|
149
|
+
def __init__(self, parent, tree):
|
|
137
150
|
"""Initialize the button frame that contains buttons for saving the updated
|
|
138
151
|
configuration and adding custom items.
|
|
139
152
|
"""
|
|
140
|
-
|
|
141
153
|
super().__init__(parent, padding=10)
|
|
142
154
|
|
|
143
155
|
self.parent = parent
|
|
144
|
-
self.scraper = scraper
|
|
145
156
|
self.tree = tree
|
|
146
157
|
self.custom_item_dialog = None
|
|
147
158
|
|
|
@@ -157,11 +168,14 @@ class ConfigEditorButtonFrame(ttk.Frame):
|
|
|
157
168
|
reset_button = ttk.Button(self, text="Reset", command=self._reset_config)
|
|
158
169
|
reset_button.pack(side="left", expand=True, padx=5)
|
|
159
170
|
|
|
160
|
-
custom_item_button = ttk.Button(
|
|
161
|
-
self, text="Add Custom Item", command=self._open_custom_item_dialog
|
|
162
|
-
)
|
|
171
|
+
custom_item_button = ttk.Button(self, text="Add Custom Item", command=self._add_custom_item)
|
|
163
172
|
custom_item_button.pack(side="left", expand=True, padx=5)
|
|
164
173
|
|
|
174
|
+
import_inventory_button = ttk.Button(
|
|
175
|
+
self, text="Import Steam Inventory", command=self._import_steam_inventory
|
|
176
|
+
)
|
|
177
|
+
import_inventory_button.pack(side="left", expand=True, padx=5)
|
|
178
|
+
|
|
165
179
|
def _save_config(self):
|
|
166
180
|
"""Save the current configuration from the treeview to the config file."""
|
|
167
181
|
for child in self.tree.get_children():
|
|
@@ -174,15 +188,17 @@ class ConfigEditorButtonFrame(ttk.Frame):
|
|
|
174
188
|
if section_name == "Custom Items":
|
|
175
189
|
# custom items are already saved upon creation (Saving them again would result in duplicates)
|
|
176
190
|
continue
|
|
177
|
-
|
|
191
|
+
config.set(section_name, config_option, value)
|
|
178
192
|
|
|
179
|
-
|
|
180
|
-
if
|
|
193
|
+
config.write_to_file()
|
|
194
|
+
if config.valid:
|
|
181
195
|
messagebox.showinfo("Config Saved", "The configuration has been saved successfully.")
|
|
182
196
|
else:
|
|
197
|
+
config.load()
|
|
198
|
+
self.parent.reload_config_into_tree()
|
|
183
199
|
messagebox.showerror(
|
|
184
200
|
"Config Error",
|
|
185
|
-
f"The configuration is invalid. ({
|
|
201
|
+
f"The configuration is invalid. ({config.last_error})",
|
|
186
202
|
)
|
|
187
203
|
|
|
188
204
|
def _reset_config(self):
|
|
@@ -192,15 +208,59 @@ class ConfigEditorButtonFrame(ttk.Frame):
|
|
|
192
208
|
)
|
|
193
209
|
if confirm:
|
|
194
210
|
copy(CONFIG_FILE_BACKUP, CONFIG_FILE)
|
|
195
|
-
|
|
211
|
+
config.load()
|
|
196
212
|
self.parent.reload_config_into_tree()
|
|
197
213
|
|
|
214
|
+
def _add_custom_item(self):
|
|
215
|
+
"""Open a window to add a new custom item."""
|
|
216
|
+
custom_item_window = tk.Toplevel(self.parent)
|
|
217
|
+
custom_item_window.title(ADD_CUSTOM_ITEM_TITLE)
|
|
218
|
+
custom_item_window.geometry(ADD_CUSTOM_ITEM_SIZE)
|
|
219
|
+
|
|
220
|
+
custom_item_frame = CustomItemFrame(custom_item_window, self.parent, self.tree)
|
|
221
|
+
custom_item_frame.pack(expand=True, fill="both", padx=15, pady=15)
|
|
222
|
+
|
|
223
|
+
def _import_steam_inventory(self):
|
|
224
|
+
"""Open a window to import the user's Steam inventory."""
|
|
225
|
+
steam_inventory_window = tk.Toplevel(self.parent)
|
|
226
|
+
steam_inventory_window.title(IMPORT_INVENTORY_TITLE)
|
|
227
|
+
steam_inventory_window.geometry(IMPORT_INVENTORY_SIZE)
|
|
228
|
+
|
|
229
|
+
steam_inventory_frame = InventoryImportFrame(steam_inventory_window, self)
|
|
230
|
+
steam_inventory_frame.pack(expand=True, fill="both", padx=15, pady=15)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class CustomItemFrame(ttk.Frame):
|
|
234
|
+
def __init__(self, parent, grandparent, tree):
|
|
235
|
+
"""Initialize the custom item frame that allows users to add custom items."""
|
|
236
|
+
super().__init__(parent, style="Card.TFrame", padding=15)
|
|
237
|
+
self.parent = parent
|
|
238
|
+
self.grandparent = grandparent
|
|
239
|
+
self.tree = tree
|
|
240
|
+
self._add_widgets()
|
|
241
|
+
|
|
242
|
+
def _add_widgets(self):
|
|
243
|
+
"""Add widgets to the custom item frame for entering item details."""
|
|
244
|
+
ttk.Label(self, text="Item URL:").pack(pady=5)
|
|
245
|
+
item_url_entry = ttk.Entry(self)
|
|
246
|
+
item_url_entry.pack(fill="x", padx=10)
|
|
247
|
+
|
|
248
|
+
ttk.Label(self, text="Owned Count:").pack(pady=5)
|
|
249
|
+
item_owned_entry = ttk.Entry(self)
|
|
250
|
+
item_owned_entry.pack(fill="x", padx=10)
|
|
251
|
+
|
|
252
|
+
add_button = ttk.Button(
|
|
253
|
+
self,
|
|
254
|
+
text="Add",
|
|
255
|
+
command=lambda: self._add_custom_item(item_url_entry.get(), item_owned_entry.get()),
|
|
256
|
+
)
|
|
257
|
+
add_button.pack(pady=10)
|
|
258
|
+
|
|
198
259
|
def _add_custom_item(self, item_url, item_owned):
|
|
199
260
|
"""Add a custom item to the configuration."""
|
|
200
261
|
if not item_url or not item_owned:
|
|
201
262
|
messagebox.showerror("Input Error", "All fields must be filled out.")
|
|
202
263
|
return
|
|
203
|
-
|
|
204
264
|
try:
|
|
205
265
|
if int(item_owned) < 0:
|
|
206
266
|
raise ValueError("Owned count must be a non-negative integer.")
|
|
@@ -208,40 +268,207 @@ class ConfigEditorButtonFrame(ttk.Frame):
|
|
|
208
268
|
messagebox.showerror("Input Error", f"Invalid owned count: {error}")
|
|
209
269
|
return
|
|
210
270
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
self.custom_item_dialog = None
|
|
271
|
+
config.set("Custom Items", item_url, item_owned)
|
|
272
|
+
config.write_to_file()
|
|
273
|
+
if config.valid:
|
|
274
|
+
config.load()
|
|
275
|
+
self.grandparent.reload_config_into_tree()
|
|
276
|
+
self.parent.destroy()
|
|
218
277
|
else:
|
|
219
|
-
|
|
278
|
+
config.remove_option("Custom Items", item_url)
|
|
220
279
|
messagebox.showerror(
|
|
221
280
|
"Config Error",
|
|
222
|
-
f"The configuration is invalid. ({
|
|
281
|
+
f"The configuration is invalid. ({config.last_error})",
|
|
223
282
|
)
|
|
224
283
|
|
|
225
|
-
def _open_custom_item_dialog(self):
|
|
226
|
-
"""Open a dialog to enter custom item details."""
|
|
227
|
-
self.custom_item_dialog = tk.Toplevel(self.parent)
|
|
228
|
-
self.custom_item_dialog.title(NEW_CUSTOM_ITEM_TITLE)
|
|
229
|
-
self.custom_item_dialog.geometry(NEW_CUSTOM_ITEM_SIZE)
|
|
230
284
|
|
|
231
|
-
|
|
232
|
-
|
|
285
|
+
class InventoryImportFrame(ttk.Frame):
|
|
286
|
+
# pylint: disable=too-many-instance-attributes
|
|
287
|
+
def __init__(self, parent, grandparent):
|
|
288
|
+
"""Initialize the inventory import frame that allows users to import their Steam
|
|
289
|
+
inventory.
|
|
290
|
+
"""
|
|
291
|
+
super().__init__(parent, style="Card.TFrame", padding=10)
|
|
292
|
+
self.parent = parent
|
|
293
|
+
self.grandparent = grandparent
|
|
294
|
+
self._add_widgets()
|
|
233
295
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
296
|
+
def _add_widgets(self):
|
|
297
|
+
"""Add widgets to the inventory import frame."""
|
|
298
|
+
self._configure_checkboxes()
|
|
299
|
+
self.import_cases_checkbox.pack(anchor="w", padx=10, pady=5)
|
|
300
|
+
self.import_sticker_capsules_checkbox.pack(anchor="w", padx=10, pady=5)
|
|
301
|
+
self.import_stickers_checkbox.pack(anchor="w", padx=10, pady=5)
|
|
302
|
+
self.import_others_checkbox.pack(anchor="w", padx=10, pady=5)
|
|
303
|
+
|
|
304
|
+
self._configure_entries()
|
|
305
|
+
self.user_name_label.pack(pady=10)
|
|
306
|
+
self.user_name_entry.pack(fill="x", padx=50)
|
|
307
|
+
self.password_label.pack(pady=10)
|
|
308
|
+
self.password_entry.pack(fill="x", padx=50)
|
|
309
|
+
self.two_factor_label.pack(pady=10)
|
|
310
|
+
self.two_factor_entry.pack(fill="x", padx=50)
|
|
311
|
+
|
|
312
|
+
self.import_button = ttk.Button(self, text="Import", command=self._import_inventory)
|
|
313
|
+
self.import_button.pack(pady=10)
|
|
314
|
+
|
|
315
|
+
def _configure_checkboxes(self):
|
|
316
|
+
# pylint: disable=attribute-defined-outside-init
|
|
317
|
+
"""Configure the checkboxes for selecting what to import from the Steam
|
|
318
|
+
inventory.
|
|
319
|
+
"""
|
|
320
|
+
self.import_cases_value = tk.BooleanVar(value=True)
|
|
321
|
+
self.import_cases_checkbox = ttk.Checkbutton(
|
|
322
|
+
self, text="Import Cases", variable=self.import_cases_value, style="Switch.TCheckbutton"
|
|
323
|
+
)
|
|
237
324
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
325
|
+
self.import_sticker_capsules_value = tk.BooleanVar(value=True)
|
|
326
|
+
self.import_sticker_capsules_checkbox = ttk.Checkbutton(
|
|
327
|
+
self,
|
|
328
|
+
text="Import Sticker Capsules",
|
|
329
|
+
variable=self.import_sticker_capsules_value,
|
|
330
|
+
style="Switch.TCheckbutton",
|
|
331
|
+
)
|
|
241
332
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
333
|
+
self.import_stickers_value = tk.BooleanVar(value=True)
|
|
334
|
+
self.import_stickers_checkbox = ttk.Checkbutton(
|
|
335
|
+
self,
|
|
336
|
+
text="Import Stickers",
|
|
337
|
+
variable=self.import_stickers_value,
|
|
338
|
+
style="Switch.TCheckbutton",
|
|
246
339
|
)
|
|
247
|
-
|
|
340
|
+
|
|
341
|
+
self.import_others_value = tk.BooleanVar(value=True)
|
|
342
|
+
self.import_others_checkbox = ttk.Checkbutton(
|
|
343
|
+
self,
|
|
344
|
+
text="Import Other Items",
|
|
345
|
+
variable=self.import_others_value,
|
|
346
|
+
style="Switch.TCheckbutton",
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
def _configure_entries(self):
|
|
350
|
+
# pylint: disable=attribute-defined-outside-init
|
|
351
|
+
"""Configure the entry fields for Steam username, password, and two-factor
|
|
352
|
+
code.
|
|
353
|
+
"""
|
|
354
|
+
self.user_name_label = ttk.Label(self, text="Steam Username:")
|
|
355
|
+
self.user_name_entry = ttk.Entry(self)
|
|
356
|
+
|
|
357
|
+
self.password_label = ttk.Label(self, text="Steam Password:")
|
|
358
|
+
self.password_entry = ttk.Entry(self, show="*")
|
|
359
|
+
|
|
360
|
+
self.two_factor_label = ttk.Label(self, text="Steam Guard Code (if enabled):")
|
|
361
|
+
self.two_factor_entry = ttk.Entry(self)
|
|
362
|
+
|
|
363
|
+
def _import_inventory(self):
|
|
364
|
+
"""
|
|
365
|
+
Call the node.js script to import the user's Steam inventory.
|
|
366
|
+
|
|
367
|
+
This will also install the necessary npm packages if they are not already
|
|
368
|
+
installed.
|
|
369
|
+
"""
|
|
370
|
+
import_cases = self.import_cases_value.get()
|
|
371
|
+
import_sticker_capsules = self.import_sticker_capsules_value.get()
|
|
372
|
+
import_stickers = self.import_stickers_value.get()
|
|
373
|
+
import_others = self.import_others_value.get()
|
|
374
|
+
|
|
375
|
+
username = self.user_name_entry.get().strip()
|
|
376
|
+
password = self.password_entry.get().strip()
|
|
377
|
+
two_factor_code = self.two_factor_entry.get().strip()
|
|
378
|
+
|
|
379
|
+
self._display_node_subprocess(
|
|
380
|
+
[
|
|
381
|
+
INVENTORY_IMPORT_SCRIPT,
|
|
382
|
+
INVENTORY_IMPORT_FILE,
|
|
383
|
+
str(import_cases),
|
|
384
|
+
str(import_sticker_capsules),
|
|
385
|
+
str(import_stickers),
|
|
386
|
+
str(import_others),
|
|
387
|
+
username,
|
|
388
|
+
password,
|
|
389
|
+
two_factor_code,
|
|
390
|
+
]
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
self.parent.destroy()
|
|
394
|
+
|
|
395
|
+
def _display_node_subprocess(self, node_cmd):
|
|
396
|
+
text_window = tk.Toplevel(self.grandparent)
|
|
397
|
+
text_window.title(IMPORT_INVENTORY_PROCESS_TITLE)
|
|
398
|
+
text_window.geometry(IMPORT_INVENTORY_PROCESS_SIZE)
|
|
399
|
+
|
|
400
|
+
process_frame = InventoryImportProcessFrame(text_window)
|
|
401
|
+
process_frame.pack(expand=True, fill="both", padx=15, pady=15)
|
|
402
|
+
process_frame.start(node_cmd)
|
|
403
|
+
process_frame.console.focus_set()
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
class InventoryImportProcessFrame(ttk.Frame):
|
|
407
|
+
# pylint: disable=attribute-defined-outside-init
|
|
408
|
+
# Source: https://stackoverflow.com/questions/27327886/issues-intercepting-subprocess-output-in-real-time
|
|
409
|
+
def __init__(self, parent):
|
|
410
|
+
"""Initialize the frame that displays the output of the subprocess."""
|
|
411
|
+
super().__init__(parent)
|
|
412
|
+
self.parent = parent
|
|
413
|
+
self._add_widgets()
|
|
414
|
+
|
|
415
|
+
def _add_widgets(self):
|
|
416
|
+
"""Add a text widget to display the output of the subprocess."""
|
|
417
|
+
self.scrollbar = ttk.Scrollbar(self)
|
|
418
|
+
self.scrollbar.pack(side="right", fill="y", padx=(5, 0))
|
|
419
|
+
|
|
420
|
+
self.console = ThemedText(self, wrap="word", yscrollcommand=self.scrollbar.set)
|
|
421
|
+
self.console.config(state="disabled")
|
|
422
|
+
self.console.pack(expand=True, fill="both", padx=10, pady=10)
|
|
423
|
+
|
|
424
|
+
self.scrollbar.config(command=self.console.yview)
|
|
425
|
+
|
|
426
|
+
def _read_lines(self, process, queue):
|
|
427
|
+
"""Read lines from the subprocess output and put them in a queue."""
|
|
428
|
+
while process.poll() is None:
|
|
429
|
+
line = process.stdout.readline()
|
|
430
|
+
if line:
|
|
431
|
+
queue.put(line)
|
|
432
|
+
|
|
433
|
+
def start(self, cmd):
|
|
434
|
+
"""Start the NodeJS subprocess with the given command and read its output."""
|
|
435
|
+
self.process = node.Popen(
|
|
436
|
+
cmd,
|
|
437
|
+
stdout=PIPE,
|
|
438
|
+
stdin=PIPE,
|
|
439
|
+
stderr=STDOUT,
|
|
440
|
+
text=True,
|
|
441
|
+
bufsize=1,
|
|
442
|
+
encoding="utf-8",
|
|
443
|
+
)
|
|
444
|
+
self.queue = Queue()
|
|
445
|
+
self.thread = Thread(target=self._read_lines, args=(self.process, self.queue), daemon=True)
|
|
446
|
+
self.thread.start()
|
|
447
|
+
|
|
448
|
+
self.after(100, self._update_lines)
|
|
449
|
+
|
|
450
|
+
def _update_lines(self):
|
|
451
|
+
"""Update the text widget with lines from the subprocess output."""
|
|
452
|
+
try:
|
|
453
|
+
line = self.queue.get(block=False)
|
|
454
|
+
self.console.config(state="normal")
|
|
455
|
+
self.console.insert("end", line)
|
|
456
|
+
self.console.config(state="disabled")
|
|
457
|
+
self.console.yview("end")
|
|
458
|
+
except Empty:
|
|
459
|
+
pass
|
|
460
|
+
|
|
461
|
+
if self.process.poll() is None or not self.queue.empty():
|
|
462
|
+
self.after(100, self._update_lines)
|
|
463
|
+
else:
|
|
464
|
+
self._cleanup()
|
|
465
|
+
|
|
466
|
+
def _cleanup(self):
|
|
467
|
+
"""Clean up the process and thread after completion and trigger a config update
|
|
468
|
+
from the newly written inventory file.
|
|
469
|
+
"""
|
|
470
|
+
config.read_from_inventory_file()
|
|
471
|
+
self.parent.master.master.reload_config_into_tree()
|
|
472
|
+
|
|
473
|
+
self.process.wait()
|
|
474
|
+
self.thread.join()
|
cs2tracker/app/scraper_frame.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import csv
|
|
2
|
-
from tkinter import ttk
|
|
2
|
+
from tkinter import messagebox, ttk
|
|
3
3
|
from tkinter.filedialog import asksaveasfilename
|
|
4
4
|
|
|
5
5
|
from tksheet import Sheet
|
|
@@ -23,12 +23,23 @@ class ScraperFrame(ttk.Frame):
|
|
|
23
23
|
self._configure_sheet()
|
|
24
24
|
self.sheet.pack()
|
|
25
25
|
|
|
26
|
+
def _readjust_sheet_size_with_window_size(self, event):
|
|
27
|
+
"""Ensures that the sheet resizes with the window."""
|
|
28
|
+
if event.widget == self.parent:
|
|
29
|
+
width, height = event.width, event.height
|
|
30
|
+
if width != self.sheet_width or height != self.sheet_height:
|
|
31
|
+
self.sheet_width = width
|
|
32
|
+
self.sheet_height = height
|
|
33
|
+
self.sheet.height_and_width(height, width)
|
|
34
|
+
self.parent.update()
|
|
35
|
+
self.parent.update_idletasks()
|
|
36
|
+
|
|
26
37
|
def _configure_sheet(self):
|
|
27
38
|
"""Configure the sheet widget with initial data and settings."""
|
|
28
39
|
self.sheet = Sheet( # pylint: disable=attribute-defined-outside-init
|
|
29
40
|
self,
|
|
30
41
|
data=[],
|
|
31
|
-
theme="light" if self.dark_theme else "dark",
|
|
42
|
+
theme="light" if self.dark_theme else "dark", # This is on purpose to add contrast
|
|
32
43
|
height=self.sheet_height,
|
|
33
44
|
width=self.sheet_width,
|
|
34
45
|
auto_resize_columns=150,
|
|
@@ -44,6 +55,8 @@ class ScraperFrame(ttk.Frame):
|
|
|
44
55
|
self.sheet.align_columns([1, 2, 3], "c")
|
|
45
56
|
self.sheet.popup_menu_add_command("Save Sheet", self._save_sheet)
|
|
46
57
|
|
|
58
|
+
self.parent.bind("<Configure>", self._readjust_sheet_size_with_window_size)
|
|
59
|
+
|
|
47
60
|
def _save_sheet(self):
|
|
48
61
|
"""Export the current sheet data to a CSV file."""
|
|
49
62
|
filepath = asksaveasfilename(
|
|
@@ -74,3 +87,7 @@ class ScraperFrame(ttk.Frame):
|
|
|
74
87
|
row_heights = self.sheet.get_row_heights()
|
|
75
88
|
last_row_index = len(row_heights) - 1
|
|
76
89
|
self.sheet.align_rows(last_row_index, "c")
|
|
90
|
+
|
|
91
|
+
if self.scraper.error_stack:
|
|
92
|
+
last_error = self.scraper.error_stack[-1]
|
|
93
|
+
messagebox.showerror("An Error Occurred", f"{last_error.message}")
|