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/scraper/scraper.py
CHANGED
|
@@ -11,7 +11,7 @@ from tenacity import RetryError, retry, stop_after_attempt
|
|
|
11
11
|
|
|
12
12
|
from cs2tracker.constants import AUTHOR_STRING, BANNER, CAPSULE_INFO, CASE_HREFS
|
|
13
13
|
from cs2tracker.scraper.discord_notifier import DiscordNotifier
|
|
14
|
-
from cs2tracker.util import
|
|
14
|
+
from cs2tracker.util import PriceLogs, get_config, get_console
|
|
15
15
|
|
|
16
16
|
MAX_LINE_LEN = 72
|
|
17
17
|
SEPARATOR = "-"
|
|
@@ -20,22 +20,38 @@ PRICE_INFO = "Owned: {:<10} Steam market price: ${:<10} Total: ${:<10}\n"
|
|
|
20
20
|
HTTP_PROXY_URL = "http://{}:@smartproxy.crawlbase.com:8012"
|
|
21
21
|
HTTPS_PROXY_URL = "http://{}:@smartproxy.crawlbase.com:8012"
|
|
22
22
|
|
|
23
|
-
console =
|
|
23
|
+
console = get_console()
|
|
24
|
+
config = get_config()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ConfigError:
|
|
28
|
+
def __init__(self):
|
|
29
|
+
self.message = "Invalid configuration. Please fix the config file before running."
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RequestLimitExceededError:
|
|
33
|
+
def __init__(self):
|
|
34
|
+
self.message = "Too many requests. Consider using proxies to prevent rate limiting."
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PageLoadError:
|
|
38
|
+
def __init__(self, status_code):
|
|
39
|
+
self.message = f"Failed to load page: {status_code}. Retrying..."
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class UnexpectedError:
|
|
43
|
+
def __init__(self, error):
|
|
44
|
+
self.message = f"An unexpected error occurred: {error}"
|
|
24
45
|
|
|
25
46
|
|
|
26
47
|
class Scraper:
|
|
27
48
|
def __init__(self):
|
|
28
49
|
"""Initialize the Scraper class."""
|
|
29
|
-
self.load_config()
|
|
30
50
|
self._start_session()
|
|
31
|
-
|
|
51
|
+
self.error_stack = []
|
|
32
52
|
self.usd_total = 0
|
|
33
53
|
self.eur_total = 0
|
|
34
54
|
|
|
35
|
-
def load_config(self):
|
|
36
|
-
"""Load the configuration file and validate its contents."""
|
|
37
|
-
self.config = ValidatedConfig()
|
|
38
|
-
|
|
39
55
|
def _start_session(self):
|
|
40
56
|
"""Start a requests session with custom headers and retry logic."""
|
|
41
57
|
self.session = Session()
|
|
@@ -48,6 +64,12 @@ class Scraper:
|
|
|
48
64
|
self.session.mount("http://", HTTPAdapter(max_retries=retries))
|
|
49
65
|
self.session.mount("https://", HTTPAdapter(max_retries=retries))
|
|
50
66
|
|
|
67
|
+
def _print_error(self):
|
|
68
|
+
"""Print the last error message from the error stack, if any."""
|
|
69
|
+
last_error = self.error_stack[-1] if self.error_stack else None
|
|
70
|
+
if last_error:
|
|
71
|
+
console.error(f"{last_error.message}\n")
|
|
72
|
+
|
|
51
73
|
def scrape_prices(self, update_sheet_callback=None):
|
|
52
74
|
"""
|
|
53
75
|
Scrape prices for capsules and cases, calculate totals in USD and EUR, and
|
|
@@ -56,12 +78,15 @@ class Scraper:
|
|
|
56
78
|
:param update_sheet_callback: Optional callback function to update a tksheet
|
|
57
79
|
that is displayed in the GUI with the latest scraper price calculation.
|
|
58
80
|
"""
|
|
59
|
-
if not
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
)
|
|
81
|
+
if not config.valid:
|
|
82
|
+
self.error_stack.append(ConfigError())
|
|
83
|
+
self._print_error()
|
|
63
84
|
return
|
|
64
85
|
|
|
86
|
+
# Reset totals from the previous run and clear the error stack
|
|
87
|
+
self.usd_total, self.eur_total = 0, 0
|
|
88
|
+
self.error_stack.clear()
|
|
89
|
+
|
|
65
90
|
capsule_usd_total = self._scrape_capsule_section_prices(update_sheet_callback)
|
|
66
91
|
case_usd_total = self._scrape_case_prices(update_sheet_callback)
|
|
67
92
|
custom_item_usd_total = self._scrape_custom_item_prices(update_sheet_callback)
|
|
@@ -86,9 +111,6 @@ class Scraper:
|
|
|
86
111
|
PriceLogs.save(self.usd_total, self.eur_total)
|
|
87
112
|
self._send_discord_notification()
|
|
88
113
|
|
|
89
|
-
# Reset totals for next run
|
|
90
|
-
self.usd_total, self.eur_total = 0, 0
|
|
91
|
-
|
|
92
114
|
def _print_total(self):
|
|
93
115
|
"""Print the total prices in USD and EUR, formatted with titles and
|
|
94
116
|
separators.
|
|
@@ -108,10 +130,10 @@ class Scraper:
|
|
|
108
130
|
"""Send a message to a Discord webhook if notifications are enabled in the
|
|
109
131
|
config file and a webhook URL is provided.
|
|
110
132
|
"""
|
|
111
|
-
discord_notifications =
|
|
133
|
+
discord_notifications = config.getboolean(
|
|
112
134
|
"App Settings", "discord_notifications", fallback=False
|
|
113
135
|
)
|
|
114
|
-
webhook_url =
|
|
136
|
+
webhook_url = config.get("User Settings", "discord_webhook_url", fallback=None)
|
|
115
137
|
|
|
116
138
|
if discord_notifications and webhook_url:
|
|
117
139
|
DiscordNotifier.notify(webhook_url)
|
|
@@ -127,8 +149,8 @@ class Scraper:
|
|
|
127
149
|
:raises RequestException: If the request fails.
|
|
128
150
|
:raises RetryError: If the retry limit is reached.
|
|
129
151
|
"""
|
|
130
|
-
use_proxy =
|
|
131
|
-
proxy_api_key =
|
|
152
|
+
use_proxy = config.getboolean("App Settings", "use_proxy", fallback=False)
|
|
153
|
+
proxy_api_key = config.get("User Settings", "proxy_api_key", fallback=None)
|
|
132
154
|
|
|
133
155
|
if use_proxy and proxy_api_key:
|
|
134
156
|
page = self.session.get(
|
|
@@ -143,11 +165,32 @@ class Scraper:
|
|
|
143
165
|
page = self.session.get(url)
|
|
144
166
|
|
|
145
167
|
if not page.ok or not page.content:
|
|
146
|
-
|
|
168
|
+
self.error_stack.append(PageLoadError(page.status_code))
|
|
169
|
+
self._print_error()
|
|
147
170
|
raise RequestException(f"Failed to load page: {url}")
|
|
148
171
|
|
|
149
172
|
return page
|
|
150
173
|
|
|
174
|
+
def _print_item_title(self, raw_item_str, from_config=False, from_href=False):
|
|
175
|
+
"""
|
|
176
|
+
Print the title for a case, capsule, or custom item.
|
|
177
|
+
|
|
178
|
+
:param raw_item_str: The raw string to convert into an item name and title.
|
|
179
|
+
:param from_config: Whether the raw item string is from the config file.
|
|
180
|
+
:param from_href: Whether the raw item string is an href.
|
|
181
|
+
:return: The formatted item name.
|
|
182
|
+
"""
|
|
183
|
+
if from_config:
|
|
184
|
+
item_name = raw_item_str.replace("_", " ").title()
|
|
185
|
+
elif from_href:
|
|
186
|
+
item_name = unquote(raw_item_str.split("/")[-1])
|
|
187
|
+
else:
|
|
188
|
+
item_name = raw_item_str
|
|
189
|
+
|
|
190
|
+
item_title = item_name.center(MAX_LINE_LEN, SEPARATOR)
|
|
191
|
+
console.print(f"[bold magenta]{item_title}\n")
|
|
192
|
+
return item_name
|
|
193
|
+
|
|
151
194
|
def _parse_item_price(self, item_page, item_href):
|
|
152
195
|
"""
|
|
153
196
|
Parse the price of an item from the given steamcommunity market page and item
|
|
@@ -183,15 +226,14 @@ class Scraper:
|
|
|
183
226
|
:param update_sheet_callback: Optional callback function to update a tksheet
|
|
184
227
|
that is displayed in the GUI with the latest scraper price calculation.
|
|
185
228
|
"""
|
|
186
|
-
|
|
187
|
-
console.print(f"[bold magenta]{capsule_title}\n")
|
|
188
|
-
|
|
229
|
+
self._print_item_title(capsule_section)
|
|
189
230
|
capsule_usd_total = 0
|
|
190
231
|
try:
|
|
191
232
|
capsule_page = self._get_page(capsule_info["page"])
|
|
192
|
-
for
|
|
233
|
+
for capsule_href in capsule_info["items"]:
|
|
234
|
+
capsule_name = unquote(capsule_href.split("/")[-1])
|
|
193
235
|
config_capsule_name = capsule_name.replace(" ", "_").lower()
|
|
194
|
-
owned =
|
|
236
|
+
owned = config.getint(capsule_section, config_capsule_name, fallback=0)
|
|
195
237
|
if owned == 0:
|
|
196
238
|
continue
|
|
197
239
|
|
|
@@ -200,15 +242,16 @@ class Scraper:
|
|
|
200
242
|
|
|
201
243
|
console.print(f"[bold deep_sky_blue4]{capsule_name}")
|
|
202
244
|
console.print(PRICE_INFO.format(owned, price_usd, price_usd_owned))
|
|
245
|
+
capsule_usd_total += price_usd_owned
|
|
246
|
+
|
|
203
247
|
if update_sheet_callback:
|
|
204
248
|
update_sheet_callback([capsule_name, owned, price_usd, price_usd_owned])
|
|
205
|
-
capsule_usd_total += price_usd_owned
|
|
206
249
|
except (RetryError, ValueError):
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
)
|
|
250
|
+
self.error_stack.append(RequestLimitExceededError())
|
|
251
|
+
self._print_error()
|
|
210
252
|
except Exception as error:
|
|
211
|
-
|
|
253
|
+
self.error_stack.append(UnexpectedError(error))
|
|
254
|
+
self._print_error()
|
|
212
255
|
|
|
213
256
|
return capsule_usd_total
|
|
214
257
|
|
|
@@ -221,12 +264,18 @@ class Scraper:
|
|
|
221
264
|
"""
|
|
222
265
|
capsule_usd_total = 0
|
|
223
266
|
for capsule_section, capsule_info in CAPSULE_INFO.items():
|
|
267
|
+
if self.error_stack:
|
|
268
|
+
break
|
|
269
|
+
|
|
224
270
|
# Only scrape capsule sections where the user owns at least one item
|
|
225
|
-
if any(int(owned) > 0 for _, owned in
|
|
271
|
+
if any(int(owned) > 0 for _, owned in config.items(capsule_section)):
|
|
226
272
|
capsule_usd_total += self._scrape_capsule_prices(
|
|
227
273
|
capsule_section, capsule_info, update_sheet_callback
|
|
228
274
|
)
|
|
229
275
|
|
|
276
|
+
if not config.getboolean("App Settings", "use_proxy", fallback=False):
|
|
277
|
+
time.sleep(1)
|
|
278
|
+
|
|
230
279
|
return capsule_usd_total
|
|
231
280
|
|
|
232
281
|
def _market_page_from_href(self, item_href):
|
|
@@ -253,14 +302,13 @@ class Scraper:
|
|
|
253
302
|
that is displayed in the GUI with the latest scraper price calculation.
|
|
254
303
|
"""
|
|
255
304
|
case_usd_total = 0
|
|
256
|
-
for case_index, (config_case_name, owned) in enumerate(
|
|
305
|
+
for case_index, (config_case_name, owned) in enumerate(config.items("Cases")):
|
|
306
|
+
if self.error_stack:
|
|
307
|
+
break
|
|
257
308
|
if int(owned) == 0:
|
|
258
309
|
continue
|
|
259
310
|
|
|
260
|
-
case_name =
|
|
261
|
-
case_title = case_name.center(MAX_LINE_LEN, SEPARATOR)
|
|
262
|
-
console.print(f"[bold magenta]{case_title}\n")
|
|
263
|
-
|
|
311
|
+
case_name = self._print_item_title(config_case_name, from_config=True)
|
|
264
312
|
try:
|
|
265
313
|
case_page_url = self._market_page_from_href(CASE_HREFS[case_index])
|
|
266
314
|
case_page = self._get_page(case_page_url)
|
|
@@ -268,18 +316,19 @@ class Scraper:
|
|
|
268
316
|
price_usd_owned = round(float(int(owned) * price_usd), 2)
|
|
269
317
|
|
|
270
318
|
console.print(PRICE_INFO.format(owned, price_usd, price_usd_owned))
|
|
319
|
+
case_usd_total += price_usd_owned
|
|
320
|
+
|
|
271
321
|
if update_sheet_callback:
|
|
272
322
|
update_sheet_callback([case_name, owned, price_usd, price_usd_owned])
|
|
273
|
-
case_usd_total += price_usd_owned
|
|
274
323
|
|
|
275
|
-
if not
|
|
324
|
+
if not config.getboolean("App Settings", "use_proxy", fallback=False):
|
|
276
325
|
time.sleep(1)
|
|
277
326
|
except (RetryError, ValueError):
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
)
|
|
327
|
+
self.error_stack.append(RequestLimitExceededError())
|
|
328
|
+
self._print_error()
|
|
281
329
|
except Exception as error:
|
|
282
|
-
|
|
330
|
+
self.error_stack.append(UnexpectedError(error))
|
|
331
|
+
self._print_error()
|
|
283
332
|
|
|
284
333
|
return case_usd_total
|
|
285
334
|
|
|
@@ -294,14 +343,13 @@ class Scraper:
|
|
|
294
343
|
that is displayed in the GUI with the latest scraper price calculation.
|
|
295
344
|
"""
|
|
296
345
|
custom_item_usd_total = 0
|
|
297
|
-
for custom_item_href, owned in
|
|
346
|
+
for custom_item_href, owned in config.items("Custom Items"):
|
|
347
|
+
if self.error_stack:
|
|
348
|
+
break
|
|
298
349
|
if int(owned) == 0:
|
|
299
350
|
continue
|
|
300
351
|
|
|
301
|
-
custom_item_name =
|
|
302
|
-
custom_item_title = custom_item_name.center(MAX_LINE_LEN, SEPARATOR)
|
|
303
|
-
console.print(f"[bold magenta]{custom_item_title}\n")
|
|
304
|
-
|
|
352
|
+
custom_item_name = self._print_item_title(custom_item_href, from_href=True)
|
|
305
353
|
try:
|
|
306
354
|
custom_item_page_url = self._market_page_from_href(custom_item_href)
|
|
307
355
|
custom_item_page = self._get_page(custom_item_page_url)
|
|
@@ -309,38 +357,22 @@ class Scraper:
|
|
|
309
357
|
price_usd_owned = round(float(int(owned) * price_usd), 2)
|
|
310
358
|
|
|
311
359
|
console.print(PRICE_INFO.format(owned, price_usd, price_usd_owned))
|
|
360
|
+
custom_item_usd_total += price_usd_owned
|
|
361
|
+
|
|
312
362
|
if update_sheet_callback:
|
|
313
363
|
update_sheet_callback([custom_item_name, owned, price_usd, price_usd_owned])
|
|
314
|
-
custom_item_usd_total += price_usd_owned
|
|
315
364
|
|
|
316
|
-
if not
|
|
365
|
+
if not config.getboolean("App Settings", "use_proxy", fallback=False):
|
|
317
366
|
time.sleep(1)
|
|
318
367
|
except (RetryError, ValueError):
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
)
|
|
368
|
+
self.error_stack.append(RequestLimitExceededError())
|
|
369
|
+
self._print_error()
|
|
322
370
|
except Exception as error:
|
|
323
|
-
|
|
371
|
+
self.error_stack.append(UnexpectedError(error))
|
|
372
|
+
self._print_error()
|
|
324
373
|
|
|
325
374
|
return custom_item_usd_total
|
|
326
375
|
|
|
327
|
-
def toggle_use_proxy(self, enabled: bool):
|
|
328
|
-
"""
|
|
329
|
-
Toggle the use of proxies for requests. This will update the configuration file.
|
|
330
|
-
|
|
331
|
-
:param enabled: If True, proxies will be used; if False, they will not be used.
|
|
332
|
-
"""
|
|
333
|
-
self.config.toggle_use_proxy(enabled)
|
|
334
|
-
|
|
335
|
-
def toggle_discord_webhook(self, enabled: bool):
|
|
336
|
-
"""
|
|
337
|
-
Toggle the use of a Discord webhook to notify users of price calculations.
|
|
338
|
-
|
|
339
|
-
:param enabled: If True, the webhook will be used; if False, it will not be
|
|
340
|
-
used.
|
|
341
|
-
"""
|
|
342
|
-
self.config.toggle_discord_webhook(enabled)
|
|
343
|
-
|
|
344
376
|
|
|
345
377
|
if __name__ == "__main__":
|
|
346
378
|
scraper = Scraper()
|
cs2tracker/util/__init__.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from cs2tracker.util.padded_console import ( # noqa: F401 # pylint:disable=unused-import
|
|
2
|
-
|
|
2
|
+
get_console,
|
|
3
3
|
)
|
|
4
4
|
from cs2tracker.util.price_logs import ( # noqa: F401 # pylint:disable=unused-import
|
|
5
5
|
PriceLogs,
|
|
6
6
|
)
|
|
7
7
|
from cs2tracker.util.validated_config import ( # noqa: F401 # pylint:disable=unused-import
|
|
8
|
-
|
|
8
|
+
get_config,
|
|
9
9
|
)
|
|
@@ -17,6 +17,19 @@ class PaddedConsole:
|
|
|
17
17
|
"""Print text with padding to the console."""
|
|
18
18
|
self.console.print(Padding(text, self.padding))
|
|
19
19
|
|
|
20
|
+
def error(self, text):
|
|
21
|
+
"""Print error text with padding to the console."""
|
|
22
|
+
text = "[bold red][!] " + text
|
|
23
|
+
self.print(text)
|
|
24
|
+
|
|
20
25
|
def __getattr__(self, attr):
|
|
21
26
|
"""Ensure console methods can be called directly on PaddedConsole."""
|
|
22
27
|
return getattr(self.console, attr)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
console = PaddedConsole()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_console():
|
|
34
|
+
"""Get the PaddedConsole instance."""
|
|
35
|
+
return console
|
|
@@ -1,24 +1,31 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import re
|
|
2
3
|
from configparser import ConfigParser
|
|
4
|
+
from urllib.parse import quote
|
|
3
5
|
|
|
4
|
-
from cs2tracker.constants import CAPSULE_INFO, CONFIG_FILE
|
|
5
|
-
from cs2tracker.util.padded_console import
|
|
6
|
-
|
|
7
|
-
console = PaddedConsole()
|
|
8
|
-
|
|
6
|
+
from cs2tracker.constants import CAPSULE_INFO, CONFIG_FILE, INVENTORY_IMPORT_FILE
|
|
7
|
+
from cs2tracker.util.padded_console import get_console
|
|
9
8
|
|
|
9
|
+
STEAM_MARKET_LISTING_BASEURL_CS2 = "https://steamcommunity.com/market/listings/730/"
|
|
10
10
|
STEAM_MARKET_LISTING_REGEX = r"^https://steamcommunity.com/market/listings/\d+/.+$"
|
|
11
11
|
|
|
12
|
+
console = get_console()
|
|
13
|
+
|
|
12
14
|
|
|
13
15
|
class ValidatedConfig(ConfigParser):
|
|
14
16
|
def __init__(self):
|
|
15
17
|
"""Initialize the ValidatedConfig class."""
|
|
16
18
|
super().__init__(delimiters=("~"), interpolation=None)
|
|
17
19
|
self.optionxform = str # type: ignore
|
|
18
|
-
super().read(CONFIG_FILE)
|
|
19
20
|
|
|
20
21
|
self.valid = False
|
|
21
22
|
self.last_error = None
|
|
23
|
+
self.load()
|
|
24
|
+
|
|
25
|
+
def load(self):
|
|
26
|
+
"""Load the configuration file and validate it."""
|
|
27
|
+
self.clear()
|
|
28
|
+
self.read(CONFIG_FILE)
|
|
22
29
|
self._validate_config()
|
|
23
30
|
|
|
24
31
|
def _validate_config_sections(self):
|
|
@@ -77,18 +84,50 @@ class ValidatedConfig(ConfigParser):
|
|
|
77
84
|
self._validate_config_values()
|
|
78
85
|
self.valid = True
|
|
79
86
|
except ValueError as error:
|
|
80
|
-
console.
|
|
87
|
+
console.error(f"Config error: {error}")
|
|
81
88
|
self.valid = False
|
|
82
89
|
self.last_error = error
|
|
83
90
|
|
|
84
91
|
def write_to_file(self):
|
|
85
|
-
"""
|
|
92
|
+
"""Validate the current configuration and write it to the configuration file if
|
|
93
|
+
it is valid.
|
|
94
|
+
"""
|
|
86
95
|
self._validate_config()
|
|
87
96
|
|
|
88
97
|
if self.valid:
|
|
89
98
|
with open(CONFIG_FILE, "w", encoding="utf-8") as config_file:
|
|
90
99
|
self.write(config_file)
|
|
91
100
|
|
|
101
|
+
def read_from_inventory_file(self):
|
|
102
|
+
"""
|
|
103
|
+
Read an inventory file into the configuration.
|
|
104
|
+
|
|
105
|
+
This file is generated after a user automatically imports their inventory.
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
with open(INVENTORY_IMPORT_FILE, "r", encoding="utf-8") as inventory_file:
|
|
109
|
+
inventory_data = json.load(inventory_file)
|
|
110
|
+
|
|
111
|
+
added_to_config = set()
|
|
112
|
+
for item_name, item_owned in inventory_data.items():
|
|
113
|
+
config_item_name = item_name.replace(" ", "_").lower()
|
|
114
|
+
for section in self.sections():
|
|
115
|
+
if config_item_name in self.options(section):
|
|
116
|
+
self.set(section, config_item_name, str(item_owned))
|
|
117
|
+
added_to_config.add(item_name)
|
|
118
|
+
|
|
119
|
+
for item_name, item_owned in inventory_data.items():
|
|
120
|
+
if item_name not in added_to_config:
|
|
121
|
+
url_encoded_item_name = quote(item_name)
|
|
122
|
+
listing_url = f"{STEAM_MARKET_LISTING_BASEURL_CS2}{url_encoded_item_name}"
|
|
123
|
+
self.set("Custom Items", listing_url, str(item_owned))
|
|
124
|
+
|
|
125
|
+
self.write_to_file()
|
|
126
|
+
except (FileNotFoundError, json.JSONDecodeError) as error:
|
|
127
|
+
console.error(f"Error reading inventory file: {error}")
|
|
128
|
+
self.last_error = error
|
|
129
|
+
self.valid = False
|
|
130
|
+
|
|
92
131
|
def toggle_use_proxy(self, enabled: bool):
|
|
93
132
|
"""
|
|
94
133
|
Toggle the use of proxies for requests. This will update the configuration file.
|
|
@@ -115,3 +154,11 @@ class ValidatedConfig(ConfigParser):
|
|
|
115
154
|
console.print(
|
|
116
155
|
f"[bold green]{'[+] Enabled' if enabled else '[-] Disabled'} Discord webhook notifications."
|
|
117
156
|
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
config = ValidatedConfig()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def get_config():
|
|
163
|
+
"""Accessor function to retrieve the current configuration."""
|
|
164
|
+
return config
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cs2tracker
|
|
3
|
+
Version: 2.1.13
|
|
4
|
+
Summary: Tracking the steam market prices of CS2 items
|
|
5
|
+
Home-page: https://github.com/ashiven/cs2tracker
|
|
6
|
+
Author: Jannik Novak
|
|
7
|
+
Author-email: nevisha@pm.me
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: numpy==1.26.4
|
|
15
|
+
Requires-Dist: beautifulsoup4==4.11.1
|
|
16
|
+
Requires-Dist: CurrencyConverter==0.17.9
|
|
17
|
+
Requires-Dist: matplotlib==3.7.0
|
|
18
|
+
Requires-Dist: Requests==2.31.0
|
|
19
|
+
Requires-Dist: rich==13.6.0
|
|
20
|
+
Requires-Dist: tenacity==8.2.2
|
|
21
|
+
Requires-Dist: urllib3==2.1.0
|
|
22
|
+
Requires-Dist: sv_ttk==2.6.1
|
|
23
|
+
Requires-Dist: tksheet==7.5.12
|
|
24
|
+
Requires-Dist: nodejs-bin==18.4.0a4
|
|
25
|
+
Requires-Dist: ttk-text==0.2.0
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
<p align="center">
|
|
29
|
+
<h2 align="center">CS2Tracker</h2>
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
<p align="center">
|
|
33
|
+
A simple, elegant GUI tool to track CS2 item investments
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
<div align="center">
|
|
37
|
+
|
|
38
|
+
[![CC BY-NC-ND 4.0][cc-by-nc-nd-shield]][cc-by-nc-nd]
|
|
39
|
+
[](https://github.com/ashiven/cs2tracker/releases)
|
|
40
|
+
[](https://badge.fury.io/py/cs2tracker)
|
|
41
|
+
[](https://github.com/ashiven/cs2tracker/issues)
|
|
42
|
+
[](https://github.com/ashiven/cs2tracker/pulls)
|
|
43
|
+

|
|
44
|
+
|
|
45
|
+
<img src="https://github.com/user-attachments/assets/9585afb2-bf1a-473c-be5d-cccbb3349b9a"/>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
## Table of Contents
|
|
49
|
+
|
|
50
|
+
- [Features](#features)
|
|
51
|
+
- [Getting Started](#getting-started)
|
|
52
|
+
- [Prerequisites](#prerequisites)
|
|
53
|
+
- [Installation](#installation)
|
|
54
|
+
- [Usage](#usage)
|
|
55
|
+
- [Configuration](#configuration)
|
|
56
|
+
- [Advanced Features](#advanced-features)
|
|
57
|
+
- [Contributing](#contributing)
|
|
58
|
+
- [License](#license)
|
|
59
|
+
|
|
60
|
+
## Features
|
|
61
|
+
|
|
62
|
+
- ⚡ Rapidly import your Storage Units
|
|
63
|
+
- 🔍 Track CS2 Steam Market prices
|
|
64
|
+
- 📈 View investment price history
|
|
65
|
+
- 🧾 Export/Import price data
|
|
66
|
+
- 📤 Discord notifications on updates
|
|
67
|
+
- 📅 Daily background calculations
|
|
68
|
+
- 🛡️ Proxy support to avoid rate limits
|
|
69
|
+
|
|
70
|
+
## Getting Started
|
|
71
|
+
|
|
72
|
+
### Prerequisites
|
|
73
|
+
|
|
74
|
+
- Download and install the latest versions of [Python](https://www.python.org/downloads/) and [Pip](https://pypi.org/project/pip/). (Required on Linux)
|
|
75
|
+
- Register for the [Crawlbase Smart Proxy API](https://crawlbase.com/) and retrieve your API key. (Optional)
|
|
76
|
+
- Create a [Discord Webhook](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) to be notified about recent price updates. (Optional)
|
|
77
|
+
|
|
78
|
+
### Installation
|
|
79
|
+
|
|
80
|
+
#### Option 1: Windows Executable
|
|
81
|
+
|
|
82
|
+
- Simply [download the latest executable](https://github.com/ashiven/cs2tracker/releases/latest/download/cs2tracker-windows.zip) and run it.
|
|
83
|
+
|
|
84
|
+
#### Option 2: Install via Pip
|
|
85
|
+
|
|
86
|
+
1. Install the program:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install cs2tracker
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
2. Run it:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
cs2tracker
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Usage
|
|
99
|
+
|
|
100
|
+
- Click **Run!** to gather the current market prices of your items and calculate the total amount in USD and EUR.
|
|
101
|
+
- The generated Excel sheet can be saved by right-clicking and then selecting **Save Sheet**.
|
|
102
|
+
- Use **Edit Config** to specify the numbers of items owned in the configuration.
|
|
103
|
+
- Click **Show History** to see a price chart consisting of past calculations.
|
|
104
|
+
- Use **Export / Import History** to export or import the price history to or from a CSV file.
|
|
105
|
+
|
|
106
|
+
## Configuration
|
|
107
|
+
|
|
108
|
+
You can configure the app settings via the **Edit Config** option.
|
|
109
|
+
This will open the config editor where you can change any setting by simply double clicking on it. On top of that, the config editor allows you to:
|
|
110
|
+
|
|
111
|
+
- Automatically import items from your Storage Units
|
|
112
|
+
- Manually Specify the number of items you own
|
|
113
|
+
- Add custom items that are not listed in the config
|
|
114
|
+
- Enter Discord webhook and Crawlbase proxy API keys
|
|
115
|
+
|
|
116
|
+
## Advanced Features
|
|
117
|
+
|
|
118
|
+
- Enable **Daily Background Calculations** to automatically run a daily calculation of your investment in the background.
|
|
119
|
+
- Use **Receive Discord Notifications** to receive a notification on your Discord server whenever the program has finished calculating your investment.
|
|
120
|
+
- You need to set up a [webhook](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) in your Discord server and enter the webhook url into the `discord_webhook_url` field in the config `User Settings`.
|
|
121
|
+
- Enable **Proxy Requests** to prevent your requests from being rate limited by the steamcommunity server.
|
|
122
|
+
- You need to register for a free API key on [Crawlbase](crawlbase.com) and enter it into the `proxy_api_key` field in the config `User Settings`.
|
|
123
|
+
|
|
124
|
+
## Contributing
|
|
125
|
+
|
|
126
|
+
Please feel free to submit a pull request or open an issue. See [issues](https://github.com/ashiven/cs2tracker/issues) and [pull requests](https://github.com/ashiven/cs2tracker/pulls) for current work.
|
|
127
|
+
|
|
128
|
+
1. Fork the repository
|
|
129
|
+
2. Create a new branch
|
|
130
|
+
3. Make your changes
|
|
131
|
+
4. Submit a PR
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
This project is licensed under the
|
|
136
|
+
[Creative Commons Attribution-NonCommercial-NoDerivs 4.0 International License][cc-by-nc-nd].
|
|
137
|
+
|
|
138
|
+
[![CC BY-NC-ND 4.0][cc-by-nc-nd-image]][cc-by-nc-nd]
|
|
139
|
+
|
|
140
|
+
[cc-by-nc-nd]: http://creativecommons.org/licenses/by-nc-nd/4.0/
|
|
141
|
+
[cc-by-nc-nd-image]: https://licensebuttons.net/l/by-nc-nd/4.0/88x31.png
|
|
142
|
+
[cc-by-nc-nd-shield]: https://img.shields.io/badge/License-CC%20BY--NC--ND%204.0-lightgrey.svg
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
> GitHub [@ashiven](https://github.com/Ashiven) ·
|
|
147
|
+
> Twitter [ashiven\_](https://twitter.com/ashiven_)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
cs2tracker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
cs2tracker/__main__.py,sha256=Ub--oSMv48YzfWF1CZqYlkn1-HvZ7Bhxoc7urn1oY6o,249
|
|
3
|
+
cs2tracker/_version.py,sha256=MzsJ263HZYR6SmK-vVyZsM4dSlCzgRW6jJZBsjDPWV8,513
|
|
4
|
+
cs2tracker/constants.py,sha256=65tLl30rCbR78xoNJSV0ubWc0ZWbytztj0S_uwgNrvU,22879
|
|
5
|
+
cs2tracker/main.py,sha256=Ahsu1gk0RuMS9FdVsv7exnnWENd8-vj_dcCEWjZP1DM,1552
|
|
6
|
+
cs2tracker/app/__init__.py,sha256=uqAxdDzoR2-2IrDc1riIU3Pi9vLEDwr68eg93-0RFmM,105
|
|
7
|
+
cs2tracker/app/application.py,sha256=TEoGvqxyCTJKdfC1Su7bh9fzTr71jiVqlTC_K4Wdu2Q,9395
|
|
8
|
+
cs2tracker/app/editor_frame.py,sha256=V_8SRWgterkEYiADrZww9prpKfX-3ccjd2OWVrx7-2g,18074
|
|
9
|
+
cs2tracker/app/scraper_frame.py,sha256=yrtnWtaNQqd8Ho6hVUxLP3A5oCBmtvEHJa3p8STEJtA,3399
|
|
10
|
+
cs2tracker/data/config.ini,sha256=anKg7t3JB7kQcvcMKHls4oxTEGwWnGkM9obn-_l0gEE,6806
|
|
11
|
+
cs2tracker/data/convert_inventory.js,sha256=5bEDfe9Rf2wYC4dIlmvjhbslfnuKuBBNjuTk67jEV9Q,5370
|
|
12
|
+
cs2tracker/data/get_inventory.js,sha256=O9w7w-diI7tD5NMgGMDyvrNfrv5ObE46m5rMK8PlpBE,4108
|
|
13
|
+
cs2tracker/data/output.csv,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
cs2tracker/scraper/__init__.py,sha256=kfUB9yfshSche92ZvTqQQYBkVqujRez46djPoa9u0YU,335
|
|
15
|
+
cs2tracker/scraper/background_task.py,sha256=1_lKnFWn9wuABpru58JdQbfDHjL0Up-7h6Nlcc4TsZ4,3671
|
|
16
|
+
cs2tracker/scraper/discord_notifier.py,sha256=-PdTRAtzMHTjDH17zguI0LKB7INITEohx0da0fWRdKI,2962
|
|
17
|
+
cs2tracker/scraper/scraper.py,sha256=Zj-8EZZNAjifiOywfvJrOQ8KtJK-SeaZerwx8plvLI8,15358
|
|
18
|
+
cs2tracker/util/__init__.py,sha256=Pgltyrkotd5ijU-srmY5vaFP7gzz9itSAl2LfS-D7-E,322
|
|
19
|
+
cs2tracker/util/padded_console.py,sha256=LXgPDqccgrIowwjV6CLsxn5zxuoiPA-9pomURPSLllU,936
|
|
20
|
+
cs2tracker/util/price_logs.py,sha256=JuZ2ptDtxA-NzKjpG_4q0eeOr1IaUvVah-Qth6GrDDU,4165
|
|
21
|
+
cs2tracker/util/validated_config.py,sha256=bQDIuu42hK0ZbFY1fYGTeYTip7tUNmxJLou88F3r4q8,6638
|
|
22
|
+
cs2tracker-2.1.13.dist-info/licenses/LICENSE,sha256=doPNswWMPXbkhplb9cnZLwJoqqS72pJPhkSib8kIF08,19122
|
|
23
|
+
cs2tracker-2.1.13.dist-info/METADATA,sha256=Os14jGDiniACP9PCaaaGL9SK2otVtIBkYt3mD-MtSIk,5694
|
|
24
|
+
cs2tracker-2.1.13.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
25
|
+
cs2tracker-2.1.13.dist-info/entry_points.txt,sha256=K8IwDIkg8QztSB9g9c89B9jR_2pG4QyJGrNs4z5RcZw,63
|
|
26
|
+
cs2tracker-2.1.13.dist-info/top_level.txt,sha256=2HB4xDDOxaU5BDc_yvdi9UlYLgL768n8aR-hRhFM6VQ,11
|
|
27
|
+
cs2tracker-2.1.13.dist-info/RECORD,,
|