cs2tracker 2.1.14__py3-none-any.whl → 2.1.15__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/__init__.py +0 -3
- cs2tracker/app/{application.py → app.py} +77 -45
- cs2tracker/app/editor_frame.py +223 -108
- cs2tracker/app/history_frame.py +61 -0
- cs2tracker/app/scraper_frame.py +13 -7
- cs2tracker/{util/validated_config.py → config.py} +97 -52
- cs2tracker/data/config.ini +24 -21
- cs2tracker/data/convert_inventory.js +108 -28
- cs2tracker/data/get_inventory.js +34 -13
- cs2tracker/logs.py +143 -0
- cs2tracker/main.py +3 -3
- cs2tracker/scraper/__init__.py +0 -9
- cs2tracker/scraper/background_task.py +1 -1
- cs2tracker/scraper/discord_notifier.py +34 -32
- cs2tracker/scraper/{parsers.py → parser.py} +29 -32
- cs2tracker/scraper/scraper.py +113 -83
- cs2tracker/util/__init__.py +0 -9
- cs2tracker/util/currency_conversion.py +84 -0
- cs2tracker/util/padded_console.py +5 -0
- cs2tracker/util/tkinter.py +55 -0
- {cs2tracker-2.1.14.dist-info → cs2tracker-2.1.15.dist-info}/METADATA +4 -5
- cs2tracker-2.1.15.dist-info/RECORD +31 -0
- cs2tracker/util/price_logs.py +0 -100
- cs2tracker-2.1.14.dist-info/RECORD +0 -28
- {cs2tracker-2.1.14.dist-info → cs2tracker-2.1.15.dist-info}/WHEEL +0 -0
- {cs2tracker-2.1.14.dist-info → cs2tracker-2.1.15.dist-info}/entry_points.txt +0 -0
- {cs2tracker-2.1.14.dist-info → cs2tracker-2.1.15.dist-info}/licenses/LICENSE +0 -0
- {cs2tracker-2.1.14.dist-info → cs2tracker-2.1.15.dist-info}/top_level.txt +0 -0
cs2tracker/app/scraper_frame.py
CHANGED
|
@@ -4,7 +4,9 @@ from tkinter.filedialog import asksaveasfilename
|
|
|
4
4
|
|
|
5
5
|
from tksheet import Sheet
|
|
6
6
|
|
|
7
|
-
from cs2tracker.scraper.
|
|
7
|
+
from cs2tracker.scraper.parser import Parser
|
|
8
|
+
from cs2tracker.scraper.scraper import ParsingError, SheetNotFoundError
|
|
9
|
+
from cs2tracker.util.tkinter import centered
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
class ScraperFrame(ttk.Frame):
|
|
@@ -51,10 +53,10 @@ class ScraperFrame(ttk.Frame):
|
|
|
51
53
|
self.sheet.enable_bindings()
|
|
52
54
|
|
|
53
55
|
source_titles = []
|
|
54
|
-
for price_source in
|
|
56
|
+
for price_source in Parser.SOURCES:
|
|
55
57
|
source_titles += [
|
|
56
|
-
f"{price_source.
|
|
57
|
-
f"{price_source.
|
|
58
|
+
f"{price_source.name.title()} (USD)",
|
|
59
|
+
f"{price_source.name.title()} Owned (USD)",
|
|
58
60
|
]
|
|
59
61
|
self.sheet.insert_row(
|
|
60
62
|
[
|
|
@@ -65,14 +67,16 @@ class ScraperFrame(ttk.Frame):
|
|
|
65
67
|
)
|
|
66
68
|
self.sheet.align_rows([0], "c")
|
|
67
69
|
|
|
68
|
-
price_columns = list(range(2 * len(
|
|
70
|
+
price_columns = list(range(2 * len(Parser.SOURCES)))
|
|
69
71
|
price_columns = [1] + [column_index + 2 for column_index in price_columns]
|
|
70
72
|
self.sheet.align_columns(price_columns, "c")
|
|
71
73
|
self.sheet.column_width(0, 220)
|
|
72
74
|
|
|
73
75
|
required_window_width = 220 + 150 * len(price_columns)
|
|
74
76
|
if int(self.sheet_width) < required_window_width:
|
|
75
|
-
self.parent.geometry(
|
|
77
|
+
self.parent.geometry(
|
|
78
|
+
centered(self.parent, f"{required_window_width}x{self.sheet_height}")
|
|
79
|
+
)
|
|
76
80
|
|
|
77
81
|
self.sheet.popup_menu_add_command("Save Sheet", self._save_sheet)
|
|
78
82
|
self.parent.bind("<Configure>", self._readjust_sheet_size_with_window_size)
|
|
@@ -104,7 +108,9 @@ class ScraperFrame(ttk.Frame):
|
|
|
104
108
|
|
|
105
109
|
self.scraper.scrape_prices(update_sheet_callback)
|
|
106
110
|
|
|
107
|
-
if self.scraper.error_stack
|
|
111
|
+
if self.scraper.error_stack and not isinstance(
|
|
112
|
+
self.scraper.error_stack[-1], SheetNotFoundError
|
|
113
|
+
):
|
|
108
114
|
last_error = self.scraper.error_stack[-1]
|
|
109
115
|
if not isinstance(last_error, ParsingError):
|
|
110
116
|
messagebox.showerror("An Error Occurred", f"{last_error.message}", parent=self)
|
|
@@ -36,10 +36,13 @@ class ValidatedConfig(ConfigParser):
|
|
|
36
36
|
discord_notifications = self.getboolean(
|
|
37
37
|
"App Settings", "discord_notifications", fallback=False
|
|
38
38
|
)
|
|
39
|
+
conversion_currency = self.get("App Settings", "conversion_currency", fallback="EUR")
|
|
40
|
+
|
|
39
41
|
self.clear()
|
|
40
42
|
self.add_section("App Settings")
|
|
41
43
|
self.set("App Settings", "use_proxy", str(use_proxy))
|
|
42
44
|
self.set("App Settings", "discord_notifications", str(discord_notifications))
|
|
45
|
+
self.set("App Settings", "conversion_currency", conversion_currency)
|
|
43
46
|
|
|
44
47
|
def _validate_config_sections(self):
|
|
45
48
|
"""Validate that the configuration file has all required sections."""
|
|
@@ -47,47 +50,49 @@ class ValidatedConfig(ConfigParser):
|
|
|
47
50
|
raise ValueError("Missing 'User Settings' section in the configuration file.")
|
|
48
51
|
if not self.has_section("App Settings"):
|
|
49
52
|
raise ValueError("Missing 'App Settings' section in the configuration file.")
|
|
50
|
-
if not self.has_section("
|
|
51
|
-
raise ValueError("Missing '
|
|
53
|
+
if not self.has_section("Stickers"):
|
|
54
|
+
raise ValueError("Missing 'Stickers' section in the configuration file.")
|
|
52
55
|
if not self.has_section("Cases"):
|
|
53
56
|
raise ValueError("Missing 'Cases' section in the configuration file.")
|
|
57
|
+
if not self.has_section("Skins"):
|
|
58
|
+
raise ValueError("Missing 'Skins' section in the configuration file.")
|
|
54
59
|
for capsule_section in CAPSULE_PAGES:
|
|
55
60
|
if not self.has_section(capsule_section):
|
|
56
61
|
raise ValueError(f"Missing '{capsule_section}' section in the configuration file.")
|
|
57
62
|
|
|
58
63
|
def _validate_config_values(self):
|
|
64
|
+
# pylint: disable=too-many-branches
|
|
59
65
|
"""Validate that the configuration file has valid values for all sections."""
|
|
60
66
|
try:
|
|
61
|
-
for
|
|
62
|
-
if
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
raise ValueError(
|
|
85
|
-
f"Invalid value in '{capsule_section}' section: {capsule_href} = {capsule_owned}"
|
|
86
|
-
)
|
|
67
|
+
for section in self.sections():
|
|
68
|
+
if section == "App Settings":
|
|
69
|
+
for option in ("use_proxy", "discord_notifications", "conversion_currency"):
|
|
70
|
+
if not self.has_option(section, option):
|
|
71
|
+
raise ValueError(f"Reason: Missing '{option}' in '{section}' section.")
|
|
72
|
+
if option in ("use_proxy", "discord_notifications") and self.get(
|
|
73
|
+
section, option, fallback=False
|
|
74
|
+
) not in ("True", "False"):
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"Reason: Invalid value for '{option}' in '{section}' section."
|
|
77
|
+
)
|
|
78
|
+
elif section == "User Settings":
|
|
79
|
+
for option in ("proxy_api_key", "discord_webhook_url"):
|
|
80
|
+
if not self.has_option(section, option):
|
|
81
|
+
raise ValueError(f"Reason: Missing '{option}' in '{section}' section.")
|
|
82
|
+
else:
|
|
83
|
+
for item_href, item_owned in self.items(section):
|
|
84
|
+
if not re.match(STEAM_MARKET_LISTING_REGEX, item_href):
|
|
85
|
+
raise ValueError("Reason: Invalid Steam market listing URL.")
|
|
86
|
+
if int(item_owned) < 0:
|
|
87
|
+
raise ValueError("Reason: Negative values are not allowed.")
|
|
88
|
+
if int(item_owned) > 1000000:
|
|
89
|
+
raise ValueError("Reason: Value exceeds maximum limit of 1,000,000.")
|
|
87
90
|
except ValueError as error:
|
|
88
|
-
if "
|
|
91
|
+
# Re-raise the error if it contains "Reason: " to maintain the original message
|
|
92
|
+
# and raise a ValueError if the conversion of a value to an integer fails.
|
|
93
|
+
if "Reason: " in str(error):
|
|
89
94
|
raise
|
|
90
|
-
raise ValueError("Invalid value type. All values must be integers.") from error
|
|
95
|
+
raise ValueError("Reason: Invalid value type. All values must be integers.") from error
|
|
91
96
|
|
|
92
97
|
def _validate_config(self):
|
|
93
98
|
"""
|
|
@@ -131,20 +136,23 @@ class ValidatedConfig(ConfigParser):
|
|
|
131
136
|
try:
|
|
132
137
|
with open(INVENTORY_IMPORT_FILE, "r", encoding="utf-8") as inventory_file:
|
|
133
138
|
inventory_data = json.load(inventory_file)
|
|
134
|
-
|
|
139
|
+
sorted_inventory_data = dict(sorted(inventory_data.items()))
|
|
135
140
|
|
|
136
|
-
|
|
137
|
-
|
|
141
|
+
added_to_config = set()
|
|
142
|
+
for item_name, item_owned in sorted_inventory_data.items():
|
|
143
|
+
option = self.name_to_option(item_name, href=True)
|
|
138
144
|
for section in self.sections():
|
|
139
|
-
if
|
|
140
|
-
self.set(section,
|
|
145
|
+
if option in self.options(section):
|
|
146
|
+
self.set(section, option, str(item_owned))
|
|
141
147
|
added_to_config.add(item_name)
|
|
142
148
|
|
|
143
|
-
for item_name, item_owned in
|
|
149
|
+
for item_name, item_owned in sorted_inventory_data.items():
|
|
144
150
|
if item_name not in added_to_config:
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
151
|
+
option = self.name_to_option(item_name, href=True)
|
|
152
|
+
if item_name.startswith("Sticker"):
|
|
153
|
+
self.set("Stickers", option, str(item_owned))
|
|
154
|
+
else:
|
|
155
|
+
self.set("Skins", option, str(item_owned))
|
|
148
156
|
|
|
149
157
|
self.write_to_file()
|
|
150
158
|
except (FileNotFoundError, json.JSONDecodeError) as error:
|
|
@@ -161,6 +169,9 @@ class ValidatedConfig(ConfigParser):
|
|
|
161
169
|
:return: The reader-friendly name.
|
|
162
170
|
"""
|
|
163
171
|
if href:
|
|
172
|
+
if not re.match(STEAM_MARKET_LISTING_REGEX, option):
|
|
173
|
+
raise ValueError(f"Invalid Steam market listing URL: {option}")
|
|
174
|
+
|
|
164
175
|
converted_option = unquote(option.split("/")[-1])
|
|
165
176
|
else:
|
|
166
177
|
converted_option = option.replace("_", " ").title()
|
|
@@ -182,32 +193,66 @@ class ValidatedConfig(ConfigParser):
|
|
|
182
193
|
|
|
183
194
|
return converted_name
|
|
184
195
|
|
|
185
|
-
def
|
|
196
|
+
def toggle_app_option(self, option, enabled):
|
|
186
197
|
"""
|
|
187
198
|
Toggle the use of proxies for requests. This will update the configuration file.
|
|
188
199
|
|
|
189
200
|
:param enabled: If True, proxies will be used; if False, they will not be used.
|
|
190
201
|
"""
|
|
191
|
-
self.set("App Settings",
|
|
202
|
+
self.set("App Settings", option, str(enabled))
|
|
192
203
|
self.write_to_file()
|
|
193
204
|
|
|
194
|
-
console.
|
|
195
|
-
f"[bold green]{'[+] Enabled' if enabled else '[-] Disabled'} proxy usage for requests."
|
|
196
|
-
)
|
|
205
|
+
console.info(f"{'Enabled' if enabled else 'Disabled'} option: {option}.")
|
|
197
206
|
|
|
198
|
-
def
|
|
207
|
+
def set_app_option(self, option, value):
|
|
199
208
|
"""
|
|
200
|
-
|
|
209
|
+
Set an option in the App Settings to a specific value.
|
|
201
210
|
|
|
202
|
-
:param
|
|
203
|
-
|
|
211
|
+
:param option: The option to set.
|
|
212
|
+
:param value: The value to set the option to.
|
|
204
213
|
"""
|
|
205
|
-
self.set("App Settings",
|
|
214
|
+
self.set("App Settings", option, str(value))
|
|
206
215
|
self.write_to_file()
|
|
207
216
|
|
|
208
|
-
console.
|
|
209
|
-
|
|
210
|
-
|
|
217
|
+
console.info(f"Set {option} to {value}.")
|
|
218
|
+
|
|
219
|
+
def option_exists(self, option, exclude_sections=()):
|
|
220
|
+
"""
|
|
221
|
+
Check if an option exists in any section of the configuration.
|
|
222
|
+
|
|
223
|
+
:param option: The option to check.
|
|
224
|
+
:param exclude_sections: Sections to exclude from the check.
|
|
225
|
+
:return: True if the option exists, False otherwise.
|
|
226
|
+
"""
|
|
227
|
+
for section in [section for section in self.sections() if section not in exclude_sections]:
|
|
228
|
+
if option in self.options(section):
|
|
229
|
+
return True
|
|
230
|
+
return False
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def use_proxy(self):
|
|
234
|
+
"""Check if the application should use proxies for requests."""
|
|
235
|
+
return self.getboolean("App Settings", "use_proxy", fallback=False)
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def discord_notifications(self):
|
|
239
|
+
"""Check if the application should send Discord notifications."""
|
|
240
|
+
return self.getboolean("App Settings", "discord_notifications", fallback=False)
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def conversion_currency(self):
|
|
244
|
+
"""Get the conversion currency for price calculations."""
|
|
245
|
+
return self.get("App Settings", "conversion_currency", fallback="EUR")
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def proxy_api_key(self):
|
|
249
|
+
"""Get the API key for the proxy service."""
|
|
250
|
+
return self.get("User Settings", "proxy_api_key", fallback="")
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def discord_webhook_url(self):
|
|
254
|
+
"""Get the Discord webhook URL for notifications."""
|
|
255
|
+
return self.get("User Settings", "discord_webhook_url", fallback="")
|
|
211
256
|
|
|
212
257
|
|
|
213
258
|
config = ValidatedConfig()
|
cs2tracker/data/config.ini
CHANGED
|
@@ -1,56 +1,59 @@
|
|
|
1
1
|
[App Settings]
|
|
2
2
|
use_proxy ~ False
|
|
3
3
|
discord_notifications ~ False
|
|
4
|
+
conversion_currency ~ EUR
|
|
4
5
|
|
|
5
6
|
[User Settings]
|
|
6
7
|
proxy_api_key ~
|
|
7
8
|
discord_webhook_url ~
|
|
8
9
|
|
|
9
|
-
[
|
|
10
|
+
[Skins]
|
|
11
|
+
|
|
12
|
+
[Stickers]
|
|
10
13
|
|
|
11
14
|
[Cases]
|
|
12
|
-
https://steamcommunity.com/market/listings/730/Revolution%20Case ~ 0
|
|
13
|
-
https://steamcommunity.com/market/listings/730/Recoil%20Case ~ 0
|
|
14
|
-
https://steamcommunity.com/market/listings/730/Dreams%20%26%20Nightmares%20Case ~ 0
|
|
15
|
-
https://steamcommunity.com/market/listings/730/Operation%20Riptide%20Case ~ 0
|
|
16
|
-
https://steamcommunity.com/market/listings/730/Snakebite%20Case ~ 0
|
|
17
|
-
https://steamcommunity.com/market/listings/730/Operation%20Broken%20Fang%20Case ~ 0
|
|
18
|
-
https://steamcommunity.com/market/listings/730/Fracture%20Case ~ 0
|
|
19
|
-
https://steamcommunity.com/market/listings/730/Chroma%20Case ~ 0
|
|
20
|
-
https://steamcommunity.com/market/listings/730/Chroma%202%20Case ~ 0
|
|
21
|
-
https://steamcommunity.com/market/listings/730/Chroma%203%20Case ~ 0
|
|
22
|
-
https://steamcommunity.com/market/listings/730/Clutch%20Case ~ 0
|
|
23
15
|
https://steamcommunity.com/market/listings/730/CS%3AGO%20Weapon%20Case ~ 0
|
|
24
16
|
https://steamcommunity.com/market/listings/730/CS%3AGO%20Weapon%20Case%202 ~ 0
|
|
25
17
|
https://steamcommunity.com/market/listings/730/CS%3AGO%20Weapon%20Case%203 ~ 0
|
|
26
18
|
https://steamcommunity.com/market/listings/730/CS20%20Case ~ 0
|
|
19
|
+
https://steamcommunity.com/market/listings/730/Chroma%202%20Case ~ 0
|
|
20
|
+
https://steamcommunity.com/market/listings/730/Chroma%203%20Case ~ 0
|
|
21
|
+
https://steamcommunity.com/market/listings/730/Chroma%20Case ~ 0
|
|
22
|
+
https://steamcommunity.com/market/listings/730/Clutch%20Case ~ 0
|
|
27
23
|
https://steamcommunity.com/market/listings/730/Danger%20Zone%20Case ~ 0
|
|
28
|
-
https://steamcommunity.com/market/listings/730/
|
|
29
|
-
https://steamcommunity.com/market/listings/730/eSports%202013%20Winter%20Case ~ 0
|
|
30
|
-
https://steamcommunity.com/market/listings/730/eSports%202014%20Summer%20Case ~ 0
|
|
24
|
+
https://steamcommunity.com/market/listings/730/Dreams%20%26%20Nightmares%20Case ~ 0
|
|
31
25
|
https://steamcommunity.com/market/listings/730/Falchion%20Case ~ 0
|
|
32
|
-
https://steamcommunity.com/market/listings/730/
|
|
26
|
+
https://steamcommunity.com/market/listings/730/Fever%20Case ~ 0
|
|
27
|
+
https://steamcommunity.com/market/listings/730/Fracture%20Case ~ 0
|
|
28
|
+
https://steamcommunity.com/market/listings/730/Gallery%20Case ~ 0
|
|
33
29
|
https://steamcommunity.com/market/listings/730/Gamma%202%20Case ~ 0
|
|
30
|
+
https://steamcommunity.com/market/listings/730/Gamma%20Case ~ 0
|
|
34
31
|
https://steamcommunity.com/market/listings/730/Glove%20Case ~ 0
|
|
35
32
|
https://steamcommunity.com/market/listings/730/Horizon%20Case ~ 0
|
|
36
33
|
https://steamcommunity.com/market/listings/730/Huntsman%20Weapon%20Case ~ 0
|
|
34
|
+
https://steamcommunity.com/market/listings/730/Kilowatt%20Case ~ 0
|
|
37
35
|
https://steamcommunity.com/market/listings/730/Operation%20Bravo%20Case ~ 0
|
|
38
36
|
https://steamcommunity.com/market/listings/730/Operation%20Breakout%20Weapon%20Case ~ 0
|
|
37
|
+
https://steamcommunity.com/market/listings/730/Operation%20Broken%20Fang%20Case ~ 0
|
|
39
38
|
https://steamcommunity.com/market/listings/730/Operation%20Hydra%20Case ~ 0
|
|
40
39
|
https://steamcommunity.com/market/listings/730/Operation%20Phoenix%20Weapon%20Case ~ 0
|
|
40
|
+
https://steamcommunity.com/market/listings/730/Operation%20Riptide%20Case ~ 0
|
|
41
41
|
https://steamcommunity.com/market/listings/730/Operation%20Vanguard%20Weapon%20Case ~ 0
|
|
42
42
|
https://steamcommunity.com/market/listings/730/Operation%20Wildfire%20Case ~ 0
|
|
43
|
-
https://steamcommunity.com/market/listings/730/Prisma%20Case ~ 0
|
|
44
43
|
https://steamcommunity.com/market/listings/730/Prisma%202%20Case ~ 0
|
|
44
|
+
https://steamcommunity.com/market/listings/730/Prisma%20Case ~ 0
|
|
45
|
+
https://steamcommunity.com/market/listings/730/Recoil%20Case ~ 0
|
|
46
|
+
https://steamcommunity.com/market/listings/730/Revolution%20Case ~ 0
|
|
45
47
|
https://steamcommunity.com/market/listings/730/Revolver%20Case ~ 0
|
|
46
48
|
https://steamcommunity.com/market/listings/730/Shadow%20Case ~ 0
|
|
47
49
|
https://steamcommunity.com/market/listings/730/Shattered%20Web%20Case ~ 0
|
|
48
|
-
https://steamcommunity.com/market/listings/730/
|
|
50
|
+
https://steamcommunity.com/market/listings/730/Snakebite%20Case ~ 0
|
|
49
51
|
https://steamcommunity.com/market/listings/730/Spectrum%202%20Case ~ 0
|
|
52
|
+
https://steamcommunity.com/market/listings/730/Spectrum%20Case ~ 0
|
|
50
53
|
https://steamcommunity.com/market/listings/730/Winter%20Offensive%20Weapon%20Case ~ 0
|
|
51
|
-
https://steamcommunity.com/market/listings/730/
|
|
52
|
-
https://steamcommunity.com/market/listings/730/
|
|
53
|
-
https://steamcommunity.com/market/listings/730/
|
|
54
|
+
https://steamcommunity.com/market/listings/730/eSports%202013%20Case ~ 0
|
|
55
|
+
https://steamcommunity.com/market/listings/730/eSports%202013%20Winter%20Case ~ 0
|
|
56
|
+
https://steamcommunity.com/market/listings/730/eSports%202014%20Summer%20Case ~ 0
|
|
54
57
|
|
|
55
58
|
[Katowice 2014 Sticker Capsule]
|
|
56
59
|
https://steamcommunity.com/market/listings/730/EMS%20Katowice%202014%20Legends ~ 0
|
|
@@ -18,6 +18,7 @@ class ItemNameConverter {
|
|
|
18
18
|
this.prefabs = {};
|
|
19
19
|
this.paintKits = {};
|
|
20
20
|
this.stickerKits = {};
|
|
21
|
+
this.musicKits = {};
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
async initialize() {
|
|
@@ -58,6 +59,7 @@ class ItemNameConverter {
|
|
|
58
59
|
this.prefabs = require(itemsCacheFile).prefabs;
|
|
59
60
|
this.paintKits = require(itemsCacheFile).paint_kits;
|
|
60
61
|
this.stickerKits = require(itemsCacheFile).sticker_kits;
|
|
62
|
+
this.musicKits = require(itemsCacheFile).music_definitions;
|
|
61
63
|
return;
|
|
62
64
|
}
|
|
63
65
|
|
|
@@ -67,6 +69,7 @@ class ItemNameConverter {
|
|
|
67
69
|
this.prefabs = parsed.prefabs;
|
|
68
70
|
this.paintKits = parsed.paint_kits;
|
|
69
71
|
this.stickerKits = parsed.sticker_kits;
|
|
72
|
+
this.musicKits = parsed.music_definitions;
|
|
70
73
|
|
|
71
74
|
fs.writeFileSync(itemsCacheFile, JSON.stringify(parsed, null, 2));
|
|
72
75
|
}
|
|
@@ -78,24 +81,34 @@ class ItemNameConverter {
|
|
|
78
81
|
|
|
79
82
|
getItemName(item) {
|
|
80
83
|
const def = this.items[item.def_index];
|
|
81
|
-
if (
|
|
84
|
+
if (def === undefined) return "";
|
|
82
85
|
|
|
83
86
|
let baseName = "";
|
|
84
|
-
if (def.item_name) {
|
|
87
|
+
if (def.item_name !== undefined) {
|
|
85
88
|
baseName = this.translate(def.item_name);
|
|
86
|
-
} else if (
|
|
89
|
+
} else if (
|
|
90
|
+
def.prefab !== undefined &&
|
|
91
|
+
this.prefabs[def.prefab] !== undefined
|
|
92
|
+
) {
|
|
87
93
|
baseName = this.translate(this.prefabs[def.prefab].item_name);
|
|
88
94
|
}
|
|
89
95
|
|
|
90
96
|
let stickerName = "";
|
|
91
|
-
if (
|
|
97
|
+
if (
|
|
98
|
+
baseName === "Sticker" &&
|
|
99
|
+
item.stickers !== undefined &&
|
|
100
|
+
item.stickers.length === 1
|
|
101
|
+
) {
|
|
92
102
|
stickerName = this.translate(
|
|
93
|
-
this.stickerKits[
|
|
103
|
+
this.stickerKits[item.stickers[0].sticker_id].item_name,
|
|
94
104
|
);
|
|
95
105
|
}
|
|
96
106
|
|
|
97
107
|
let skinName = "";
|
|
98
|
-
if (
|
|
108
|
+
if (
|
|
109
|
+
item.paint_index !== undefined &&
|
|
110
|
+
this.paintKits[item.paint_index] !== undefined
|
|
111
|
+
) {
|
|
99
112
|
skinName = this.translate(
|
|
100
113
|
this.paintKits[item.paint_index].description_tag,
|
|
101
114
|
);
|
|
@@ -106,13 +119,8 @@ class ItemNameConverter {
|
|
|
106
119
|
wear = this.getWearName(item.paint_wear);
|
|
107
120
|
}
|
|
108
121
|
|
|
109
|
-
//
|
|
110
|
-
if (item.
|
|
111
|
-
baseName = "★ " + baseName;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// It is a stattrak or souvenir item
|
|
115
|
-
if (item.attribute && item.attribute.length > 0) {
|
|
122
|
+
// Item is stattrak/souvenir/music kit
|
|
123
|
+
if (item.attribute !== undefined && item.attribute.length > 0) {
|
|
116
124
|
for (let [_attributeName, attributeValue] of Object.entries(
|
|
117
125
|
item.attribute,
|
|
118
126
|
)) {
|
|
@@ -127,14 +135,27 @@ class ItemNameConverter {
|
|
|
127
135
|
? baseName
|
|
128
136
|
: "Souvenir " + baseName;
|
|
129
137
|
break;
|
|
138
|
+
case 166:
|
|
139
|
+
if (baseName === "Music Kit") {
|
|
140
|
+
let music_index = attributeValue.value_bytes.readUInt32LE(0);
|
|
141
|
+
let musicKitName = this.translate(
|
|
142
|
+
this.musicKits[music_index].loc_name,
|
|
143
|
+
);
|
|
144
|
+
baseName = baseName + ` | ${musicKitName}`;
|
|
145
|
+
}
|
|
130
146
|
}
|
|
131
147
|
}
|
|
132
148
|
}
|
|
133
149
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
150
|
+
// Item is a knife / glove
|
|
151
|
+
if (item.quality === 3) {
|
|
152
|
+
baseName = "★ " + baseName;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (baseName && stickerName) {
|
|
137
156
|
return `${baseName} | ${stickerName}`;
|
|
157
|
+
} else if (baseName && skinName) {
|
|
158
|
+
return `${baseName} | ${skinName}${wear ? ` (${wear})` : ""}`;
|
|
138
159
|
}
|
|
139
160
|
|
|
140
161
|
return baseName;
|
|
@@ -153,21 +174,21 @@ class ItemNameConverter {
|
|
|
153
174
|
|
|
154
175
|
getItemType(item) {
|
|
155
176
|
const def = this.items[item.def_index];
|
|
156
|
-
if (
|
|
177
|
+
if (def === undefined) return "unknown";
|
|
157
178
|
|
|
158
|
-
if (def.item_name) {
|
|
179
|
+
if (def.item_name !== undefined) {
|
|
159
180
|
let translatedName =
|
|
160
181
|
def.item_name.replace("#", "").toLowerCase() || def.item_name;
|
|
161
182
|
if (
|
|
162
|
-
translatedName.
|
|
163
|
-
translatedName.
|
|
183
|
+
translatedName.startsWith("csgo_crate_sticker_pack") ||
|
|
184
|
+
translatedName.startsWith("csgo_crate_signature_pack")
|
|
164
185
|
) {
|
|
165
186
|
return "sticker capsule";
|
|
166
|
-
} else if (translatedName.
|
|
187
|
+
} else if (translatedName.startsWith("csgo_crate_community")) {
|
|
167
188
|
return "case";
|
|
168
|
-
} else if (translatedName.
|
|
189
|
+
} else if (translatedName.startsWith("csgo_tool_spray")) {
|
|
169
190
|
return "graffiti kit";
|
|
170
|
-
} else if (translatedName.
|
|
191
|
+
} else if (translatedName.startsWith("csgo_tool_sticker")) {
|
|
171
192
|
return "sticker";
|
|
172
193
|
}
|
|
173
194
|
}
|
|
@@ -175,12 +196,71 @@ class ItemNameConverter {
|
|
|
175
196
|
return "other";
|
|
176
197
|
}
|
|
177
198
|
|
|
199
|
+
getItemTradable(item) {
|
|
200
|
+
const def = this.items[item.def_index];
|
|
201
|
+
if (def === undefined) return false;
|
|
202
|
+
|
|
203
|
+
if (def.item_name !== undefined) {
|
|
204
|
+
let translatedName =
|
|
205
|
+
def.item_name.replace("#", "").toLowerCase() || def.item_name;
|
|
206
|
+
if (
|
|
207
|
+
translatedName.startsWith("csgo_collectible") ||
|
|
208
|
+
translatedName.startsWith("csgo_tournamentpass") ||
|
|
209
|
+
translatedName.startsWith("csgo_tournamentjournal") ||
|
|
210
|
+
translatedName.startsWith("csgo_ticket") ||
|
|
211
|
+
translatedName.startsWith("csgo_tool_casket_tag")
|
|
212
|
+
) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (
|
|
218
|
+
def.prefab !== undefined &&
|
|
219
|
+
def.prefab.includes("collectible_untradable")
|
|
220
|
+
) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Base weapons with stickers/name tags
|
|
225
|
+
if (
|
|
226
|
+
def.image_inventory !== undefined &&
|
|
227
|
+
def.image_inventory.startsWith("econ/weapons/base_weapons")
|
|
228
|
+
) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Base weapons with stickers/name tags
|
|
233
|
+
if (
|
|
234
|
+
item.paint_index === undefined &&
|
|
235
|
+
def.image_inventory === undefined &&
|
|
236
|
+
def.prefab !== undefined
|
|
237
|
+
) {
|
|
238
|
+
let prefab = this.prefabs[def.prefab];
|
|
239
|
+
if (
|
|
240
|
+
prefab !== undefined &&
|
|
241
|
+
prefab.image_inventory !== undefined &&
|
|
242
|
+
prefab.image_inventory.startsWith("econ/weapons/base_weapons")
|
|
243
|
+
) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
|
|
178
251
|
convertInventory(inventoryList) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
252
|
+
// Some untradable items were too difficult to filter out via their properties,
|
|
253
|
+
// so we filter them out by their item name here.
|
|
254
|
+
let excludeItems = ["P250 | X-Ray", "Music Kit | Valve, CS:GO"];
|
|
255
|
+
|
|
256
|
+
return inventoryList
|
|
257
|
+
.map((item) => ({
|
|
258
|
+
...item,
|
|
259
|
+
item_name: this.getItemName(item),
|
|
260
|
+
item_type: this.getItemType(item),
|
|
261
|
+
item_tradable: this.getItemTradable(item),
|
|
262
|
+
}))
|
|
263
|
+
.filter((item) => !excludeItems.includes(item.item_name));
|
|
184
264
|
}
|
|
185
265
|
}
|
|
186
266
|
|
cs2tracker/data/get_inventory.js
CHANGED
|
@@ -22,15 +22,24 @@ const password = args[8];
|
|
|
22
22
|
const twoFactorCode = args[9];
|
|
23
23
|
|
|
24
24
|
const paddedLog = (...args) => {
|
|
25
|
-
console.log(" [
|
|
25
|
+
console.log(" [INFO] ", ...args);
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
const originalConsoleError = console.error;
|
|
29
29
|
console.error = (...args) => {
|
|
30
|
-
originalConsoleError("
|
|
30
|
+
originalConsoleError(" [ERROR] " + args.join(" "));
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
(async () => {
|
|
34
|
+
const closeWithError = (message) => {
|
|
35
|
+
console.error(message);
|
|
36
|
+
console.error("This window will automatically close in 10 seconds.");
|
|
37
|
+
setTimeout(() => {
|
|
38
|
+
user.logOff();
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}, 10000);
|
|
41
|
+
};
|
|
42
|
+
|
|
34
43
|
let user = new SteamUser();
|
|
35
44
|
|
|
36
45
|
paddedLog("Logging into Steam...");
|
|
@@ -41,25 +50,36 @@ console.error = (...args) => {
|
|
|
41
50
|
twoFactorCode: twoFactorCode,
|
|
42
51
|
});
|
|
43
52
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
const LOGIN_TIMEOUT_MS = 15000;
|
|
54
|
+
let loginTimeout = setTimeout(() => {
|
|
55
|
+
closeWithError(
|
|
56
|
+
"Login timed out. Please check your credentials and try again.",
|
|
57
|
+
);
|
|
58
|
+
}, LOGIN_TIMEOUT_MS);
|
|
59
|
+
|
|
60
|
+
user.on("steamGuard", (_domain, _callback, lastCodeWrong) => {
|
|
61
|
+
if (lastCodeWrong) {
|
|
62
|
+
closeWithError(
|
|
63
|
+
"The Steam Guard code you entered was incorrect. Please try again.",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
48
66
|
});
|
|
49
67
|
|
|
50
68
|
user.on("loggedOn", (_details, _parental) => {
|
|
69
|
+
clearTimeout(loginTimeout);
|
|
51
70
|
paddedLog("Logged into Steam.");
|
|
52
71
|
user.gamesPlayed([730]);
|
|
72
|
+
paddedLog("Connecting to CS2 Game Coordinator...");
|
|
53
73
|
});
|
|
54
74
|
|
|
55
|
-
|
|
75
|
+
user.on("error", (err) => {
|
|
76
|
+
closeWithError(`Steam Error: ${err.message}`);
|
|
77
|
+
});
|
|
56
78
|
|
|
57
|
-
|
|
79
|
+
let cs2 = new CS2(user);
|
|
58
80
|
|
|
59
81
|
cs2.on("error", (err) => {
|
|
60
|
-
|
|
61
|
-
user.logOff();
|
|
62
|
-
process.exit(1);
|
|
82
|
+
closeWithError(`CS2 Error: ${err.message}`);
|
|
63
83
|
});
|
|
64
84
|
|
|
65
85
|
let nameConverter = new ItemNameConverter();
|
|
@@ -96,8 +116,6 @@ console.error = (...args) => {
|
|
|
96
116
|
process.exit(0);
|
|
97
117
|
});
|
|
98
118
|
|
|
99
|
-
// TODO: The inventory may contain items that are not marketable or tradable,
|
|
100
|
-
// so we have to make sure to process these items correctly in the main app.
|
|
101
119
|
async function processInventory() {
|
|
102
120
|
try {
|
|
103
121
|
// filter out items that have the casket_id property set from the inventory
|
|
@@ -165,6 +183,9 @@ console.error = (...args) => {
|
|
|
165
183
|
function filterItems(items) {
|
|
166
184
|
let filteredItems = [];
|
|
167
185
|
items.forEach((item) => {
|
|
186
|
+
if (!item.item_tradable) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
168
189
|
if (
|
|
169
190
|
(item.item_type === "case" && importCases) ||
|
|
170
191
|
(item.item_type === "sticker capsule" && importStickerCapsules) ||
|