cs2tracker 2.1.7__py3-none-any.whl → 2.1.9__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 CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '2.1.7'
21
- __version_tuple__ = version_tuple = (2, 1, 7)
20
+ __version__ = version = '2.1.9'
21
+ __version_tuple__ = version_tuple = (2, 1, 9)
cs2tracker/application.py CHANGED
@@ -1,7 +1,9 @@
1
1
  import ctypes
2
2
  import tkinter as tk
3
+ from shutil import copy
3
4
  from subprocess import Popen
4
5
  from threading import Thread
6
+ from tkinter import messagebox
5
7
  from typing import cast
6
8
 
7
9
  import matplotlib.pyplot as plt
@@ -10,9 +12,11 @@ from matplotlib.dates import DateFormatter
10
12
 
11
13
  from cs2tracker.constants import (
12
14
  CONFIG_FILE,
15
+ CONFIG_FILE_BACKUP,
13
16
  ICON_FILE,
14
17
  OS,
15
18
  OUTPUT_FILE,
19
+ POWERSHELL_COLORIZE_OUTPUT,
16
20
  PYTHON_EXECUTABLE,
17
21
  RUNNING_IN_EXE,
18
22
  TEXT_EDITOR,
@@ -22,7 +26,7 @@ from cs2tracker.scraper import Scraper
22
26
 
23
27
  APPLICATION_NAME = "CS2Tracker"
24
28
 
25
- WINDOW_SIZE = "500x450"
29
+ WINDOW_SIZE = "550x500"
26
30
  BACKGROUND_COLOR = "#1e1e1e"
27
31
  BUTTON_COLOR = "#3c3f41"
28
32
  BUTTON_HOVER_COLOR = "#505354"
@@ -104,6 +108,7 @@ class Application:
104
108
 
105
109
  self._add_button(frame, "Run!", self.scrape_prices)
106
110
  self._add_button(frame, "Edit Config", self._edit_config)
111
+ self._add_button(frame, "Reset Config", self._confirm_reset_config)
107
112
  self._add_button(frame, "Show History (Chart)", self._draw_plot)
108
113
  self._add_button(frame, "Show History (File)", self._edit_log_file)
109
114
 
@@ -118,7 +123,7 @@ class Application:
118
123
  lambda: self._toggle_background_task(background_checkbox_value.get()),
119
124
  )
120
125
 
121
- discord_webhook_value = tk.BooleanVar(
126
+ discord_webhook_checkbox_value = tk.BooleanVar(
122
127
  value=self.scraper.config.getboolean(
123
128
  "App Settings", "discord_notifications", fallback=False
124
129
  )
@@ -126,8 +131,8 @@ class Application:
126
131
  self._add_checkbox(
127
132
  checkbox_frame,
128
133
  "Receive Discord Notifications",
129
- discord_webhook_value,
130
- lambda: self._toggle_discord_webhook(discord_webhook_value.get()),
134
+ discord_webhook_checkbox_value,
135
+ lambda: self._toggle_discord_webhook(discord_webhook_checkbox_value.get()),
131
136
  )
132
137
 
133
138
  use_proxy_checkbox_value = tk.BooleanVar(
@@ -158,7 +163,7 @@ class Application:
158
163
 
159
164
  if RUNNING_IN_EXE:
160
165
  # The python executable is set as the executable itself in PyInstaller
161
- scraper_cmd = f"{PYTHON_EXECUTABLE} --only-scrape | Out-Host -Paging"
166
+ scraper_cmd = f"{PYTHON_EXECUTABLE} --only-scrape | {POWERSHELL_COLORIZE_OUTPUT}"
162
167
  else:
163
168
  scraper_cmd = f"{PYTHON_EXECUTABLE} -m cs2tracker --only-scrape"
164
169
 
@@ -202,6 +207,18 @@ class Application:
202
207
  callback=self.scraper.parse_config,
203
208
  )
204
209
 
210
+ def _confirm_reset_config(self):
211
+ confirm = messagebox.askokcancel(
212
+ "Reset Config", "Are you sure you want to reset the config file?"
213
+ )
214
+ if confirm:
215
+ self._reset_config()
216
+
217
+ def _reset_config(self):
218
+ """Reset the configuration file to its default state."""
219
+ copy(CONFIG_FILE_BACKUP, CONFIG_FILE)
220
+ self.scraper.parse_config()
221
+
205
222
  def _draw_plot(self):
206
223
  """Draw a plot of the scraped prices over time."""
207
224
  dates, dollars, euros = self.scraper.read_price_log()
cs2tracker/constants.py CHANGED
@@ -22,14 +22,6 @@ TEXT_EDITOR = "notepad" if OS == OSType.WINDOWS else "nano"
22
22
  PYTHON_EXECUTABLE = sys.executable
23
23
 
24
24
 
25
- MODULE_DIR = os.path.dirname(os.path.abspath(__file__))
26
- PROJECT_DIR = os.path.dirname(MODULE_DIR)
27
- ICON_FILE = os.path.join(PROJECT_DIR, "assets", "icon.png")
28
- OUTPUT_FILE = os.path.join(MODULE_DIR, "data", "output.csv")
29
- CONFIG_FILE = os.path.join(MODULE_DIR, "data", "config.ini")
30
- BATCH_FILE = os.path.join(MODULE_DIR, "data", "cs2tracker_scraper.bat")
31
-
32
-
33
25
  RUNNING_IN_EXE = getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS")
34
26
 
35
27
  if RUNNING_IN_EXE:
@@ -45,12 +37,31 @@ if RUNNING_IN_EXE:
45
37
  os.makedirs(DATA_DIR, exist_ok=True)
46
38
 
47
39
  CONFIG_FILE = os.path.join(DATA_DIR, "config.ini")
40
+ CONFIG_FILE_BACKUP = os.path.join(DATA_DIR, "config.ini.bak")
48
41
  OUTPUT_FILE = os.path.join(DATA_DIR, "output.csv")
49
42
  BATCH_FILE = os.path.join(DATA_DIR, "cs2tracker_scraper.bat")
50
- if not os.path.exists(CONFIG_FILE):
51
- copy(CONFIG_FILE_SOURCE, CONFIG_FILE)
43
+
44
+ # Always copy the source config into the user data directory as a backup
45
+ # and overwrite the existing backup if it exists
46
+ # (This is to ensure that no outdated config backup remains in the user data directory)
47
+ copy(CONFIG_FILE_SOURCE, CONFIG_FILE_BACKUP)
48
+
52
49
  if not os.path.exists(OUTPUT_FILE):
53
50
  copy(OUTPUT_FILE_SOURCE, OUTPUT_FILE)
51
+ if not os.path.exists(CONFIG_FILE):
52
+ copy(CONFIG_FILE_SOURCE, CONFIG_FILE)
53
+
54
+ else:
55
+ MODULE_DIR = os.path.dirname(os.path.abspath(__file__))
56
+ PROJECT_DIR = os.path.dirname(MODULE_DIR)
57
+ ICON_FILE = os.path.join(PROJECT_DIR, "assets", "icon.png")
58
+ CONFIG_FILE = os.path.join(MODULE_DIR, "data", "config.ini")
59
+ CONFIG_FILE_BACKUP = os.path.join(MODULE_DIR, "data", "config.ini.bak")
60
+ OUTPUT_FILE = os.path.join(MODULE_DIR, "data", "output.csv")
61
+ BATCH_FILE = os.path.join(MODULE_DIR, "data", "cs2tracker_scraper.bat")
62
+
63
+ if not os.path.exists(CONFIG_FILE_BACKUP):
64
+ copy(CONFIG_FILE, CONFIG_FILE_BACKUP)
54
65
 
55
66
 
56
67
  BANNER = """
@@ -68,50 +79,17 @@ AUTHOR_STRING = (
68
79
  )
69
80
 
70
81
 
71
- CASE_PAGES = [
72
- "https://steamcommunity.com/market/search?q=revolution+case",
73
- "https://steamcommunity.com/market/search?q=recoil+case",
74
- "https://steamcommunity.com/market/search?q=dreams+and+nightmares+case",
75
- "https://steamcommunity.com/market/search?q=operation+riptide+case",
76
- "https://steamcommunity.com/market/search?q=snakebite+case",
77
- "https://steamcommunity.com/market/search?q=broken+fang+case",
78
- "https://steamcommunity.com/market/search?q=fracture+case",
79
- "https://steamcommunity.com/market/search?q=chroma+case",
80
- "https://steamcommunity.com/market/search?q=chroma+case",
81
- "https://steamcommunity.com/market/search?q=chroma+case",
82
- "https://steamcommunity.com/market/search?q=clutch+case",
83
- "https://steamcommunity.com/market/search?q=csgo+weapon+case",
84
- "https://steamcommunity.com/market/search?q=csgo+weapon+case",
85
- "https://steamcommunity.com/market/search?q=csgo+weapon+case",
86
- "https://steamcommunity.com/market/search?q=cs20+case",
87
- "https://steamcommunity.com/market/search?q=danger+zone+case",
88
- "https://steamcommunity.com/market/search?q=esports+case",
89
- "https://steamcommunity.com/market/search?q=esports+case",
90
- "https://steamcommunity.com/market/search?q=esports+case",
91
- "https://steamcommunity.com/market/search?q=falchion+case",
92
- "https://steamcommunity.com/market/search?q=gamma+case",
93
- "https://steamcommunity.com/market/search?q=gamma+case",
94
- "https://steamcommunity.com/market/search?q=glove+case",
95
- "https://steamcommunity.com/market/search?q=horizon+case",
96
- "https://steamcommunity.com/market/search?q=huntsman+weapon+case",
97
- "https://steamcommunity.com/market/search?q=operation+bravo+case",
98
- "https://steamcommunity.com/market/search?q=operation+breakout+case",
99
- "https://steamcommunity.com/market/search?q=operation+hydra+case",
100
- "https://steamcommunity.com/market/search?q=operation+phoenix+case",
101
- "https://steamcommunity.com/market/search?q=operation+vanguard+case",
102
- "https://steamcommunity.com/market/search?q=operation+wildfire+case",
103
- "https://steamcommunity.com/market/search?q=prisma+case",
104
- "https://steamcommunity.com/market/search?q=prisma+case",
105
- "https://steamcommunity.com/market/search?q=revolver+case",
106
- "https://steamcommunity.com/market/search?q=shadow+case",
107
- "https://steamcommunity.com/market/search?q=shattered+web+case",
108
- "https://steamcommunity.com/market/search?q=spectrum+case",
109
- "https://steamcommunity.com/market/search?q=spectrum+case",
110
- "https://steamcommunity.com/market/search?q=winter+offensive+case",
111
- "https://steamcommunity.com/market/search?q=kilowatt+case",
112
- "https://steamcommunity.com/market/search?q=gallery+case",
113
- "https://steamcommunity.com/market/search?q=fever+case",
114
- ]
82
+ POWERSHELL_COLORIZE_OUTPUT = (
83
+ "%{ "
84
+ "if($_ -match 'Version|\\\\|_') { Write-Host $_ -ForegroundColor yellow } "
85
+ "elseif($_ -match 'USD|EUR|^[-|\\s]+$') { Write-Host $_ -ForegroundColor green } "
86
+ "elseif($_ -match 'Case|Capsule|-[A-Za-z0-9]') { Write-Host $_ -ForegroundColor magenta } "
87
+ "elseif($_ -match '\\[!\\]') { Write-Host $_ -ForegroundColor red } "
88
+ "elseif($_ -match 'Legends|Challengers|Contenders|Champions|Finalists') { Write-Host $_ -ForegroundColor blue } "
89
+ "else { Write-Host $_ } "
90
+ "}"
91
+ )
92
+
115
93
 
116
94
  CASE_HREFS = [
117
95
  "https://steamcommunity.com/market/listings/730/Revolution%20Case",
@@ -6,6 +6,9 @@ discord_webhook_url = None
6
6
  use_proxy = False
7
7
  discord_notifications = False
8
8
 
9
+ [Custom Items]
10
+ copenhagen_flames_gold_2022 = 0 https://steamcommunity.com/market/listings/730/Sticker%20%7C%20Copenhagen%20Flames%20%28Gold%29%20%7C%20Antwerp%202022
11
+
9
12
  [Cases]
10
13
  revolution_case = 0
11
14
  recoil_case = 0
cs2tracker/main.py CHANGED
@@ -20,7 +20,7 @@ def main():
20
20
  urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
21
21
 
22
22
  # Set output encoding to UTF-8 with BOM for Windows compatibility
23
- if OS == OSType.WINDOWS:
23
+ if OS == OSType.WINDOWS and sys.stdout is not None:
24
24
  sys.stdout.reconfigure(encoding="utf-8-sig") # type: ignore
25
25
 
26
26
  console = PaddedConsole()
cs2tracker/scraper.py CHANGED
@@ -18,7 +18,6 @@ from cs2tracker.constants import (
18
18
  BATCH_FILE,
19
19
  CAPSULE_INFO,
20
20
  CASE_HREFS,
21
- CASE_PAGES,
22
21
  CONFIG_FILE,
23
22
  OS,
24
23
  OUTPUT_FILE,
@@ -58,10 +57,75 @@ class Scraper:
58
57
  self.usd_total = 0
59
58
  self.eur_total = 0
60
59
 
60
+ def _validate_config_sections(self):
61
+ """Validate that the configuration file has all required sections."""
62
+ if not self.config.has_section("User Settings"):
63
+ raise ValueError("Missing 'User Settings' section in the configuration file.")
64
+ if not self.config.has_section("App Settings"):
65
+ raise ValueError("Missing 'App Settings' section in the configuration file.")
66
+ if not self.config.has_section("Custom Items"):
67
+ raise ValueError("Missing 'Custom Items' section in the configuration file.")
68
+ if not self.config.has_section("Cases"):
69
+ raise ValueError("Missing 'Cases' section in the configuration file.")
70
+ for capsule_section in CAPSULE_INFO:
71
+ if not self.config.has_section(capsule_section):
72
+ raise ValueError(f"Missing '{capsule_section}' section in the configuration file.")
73
+
74
+ def _validate_config_values(self):
75
+ """Validate that the configuration file has valid values for all sections."""
76
+ try:
77
+ for custom_item_name, custom_item_owned in self.config.items("Custom Items"):
78
+ if " " not in custom_item_owned:
79
+ raise ValueError(
80
+ f"Invalid custom item format (<item_name> = <owned_count> <item_url>): {custom_item_name} = {custom_item_owned}"
81
+ )
82
+ owned, _ = custom_item_owned.split(" ", 1)
83
+ if int(owned) < 0:
84
+ raise ValueError(
85
+ f"Invalid value in 'Custom Items' section: {custom_item_name} = {custom_item_owned}"
86
+ )
87
+ for case_name, case_owned in self.config.items("Cases"):
88
+ if int(case_owned) < 0:
89
+ raise ValueError(
90
+ f"Invalid value in 'Cases' section: {case_name} = {case_owned}"
91
+ )
92
+ for capsule_section in CAPSULE_INFO:
93
+ for capsule_name, capsule_owned in self.config.items(capsule_section):
94
+ if int(capsule_owned) < 0:
95
+ raise ValueError(
96
+ f"Invalid value in '{capsule_section}' section: {capsule_name} = {capsule_owned}"
97
+ )
98
+ except ValueError as error:
99
+ if "Invalid " in str(error):
100
+ raise
101
+ raise ValueError("Invalid value type. All values must be integers.") from error
102
+
103
+ def _validate_config(self):
104
+ """
105
+ Validate the configuration file to ensure all required sections exist with the
106
+ right values.
107
+
108
+ :raises ValueError: If any required section is missing or if any value is
109
+ invalid.
110
+ """
111
+ self._validate_config_sections()
112
+ self._validate_config_values()
113
+
61
114
  def parse_config(self):
62
- """Parse the configuration file to read settings and user-owned items."""
63
- self.config = ConfigParser()
115
+ """
116
+ Parse the configuration file to read settings and user-owned items.
117
+
118
+ Sets self.valid_config to True if the configuration is valid, and False if it is
119
+ not.
120
+ """
121
+ self.config = ConfigParser(interpolation=None)
64
122
  self.config.read(CONFIG_FILE)
123
+ try:
124
+ self._validate_config()
125
+ self.valid_config = True
126
+ except ValueError as error:
127
+ self.console.print(f"[bold red][!] Configuration error: {error}")
128
+ self.valid_config = False
65
129
 
66
130
  def _start_session(self):
67
131
  """Start a requests session with custom headers and retry logic."""
@@ -79,24 +143,19 @@ class Scraper:
79
143
  """Scrape prices for capsules and cases, calculate totals in USD and EUR, and
80
144
  print/save the results.
81
145
  """
82
- capsule_usd_total = 0
83
- try:
84
- capsule_usd_total = self.scrape_capsule_section_prices()
85
- except (RequestException, AttributeError, RetryError, ValueError):
146
+ if not self.valid_config:
86
147
  self.console.print(
87
- "[bold red][!] Failed to scrape capsule prices. (Consider using proxies to prevent rate limiting)\n"
148
+ "[bold red][!] Invalid configuration. Please fix the config file before running."
88
149
  )
150
+ return
89
151
 
90
- case_usd_total = 0
91
- try:
92
- case_usd_total = self._scrape_case_prices()
93
- except (RequestException, AttributeError, RetryError, ValueError):
94
- self.console.print(
95
- "[bold red][!] Failed to scrape case prices. (Consider using proxies to prevent rate limiting)\n"
96
- )
152
+ capsule_usd_total = self._scrape_capsule_section_prices()
153
+ case_usd_total = self._scrape_case_prices()
154
+ custom_item_usd_total = self._scrape_custom_item_prices()
97
155
 
98
156
  self.usd_total += capsule_usd_total
99
157
  self.usd_total += case_usd_total
158
+ self.usd_total += custom_item_usd_total
100
159
  self.eur_total = CurrencyConverter().convert(self.usd_total, "USD", "EUR")
101
160
 
102
161
  self._print_total()
@@ -273,6 +332,7 @@ class Scraper:
273
332
  use_proxy = self.config.getboolean("App Settings", "use_proxy", fallback=False)
274
333
  api_key = self.config.get("User Settings", "api_key", fallback=None)
275
334
  api_key = None if api_key in ("None", "") else api_key
335
+
276
336
  if use_proxy and api_key:
277
337
  page = self.session.get(
278
338
  url=url,
@@ -286,32 +346,33 @@ class Scraper:
286
346
  page = self.session.get(url)
287
347
 
288
348
  if not page.ok or not page.content:
289
- status = page.status_code
290
- self.console.print(f"[bold red][!] Failed to load page ({status}). Retrying...\n")
349
+ self.console.print(
350
+ f"[bold red][!] Failed to load page ({page.status_code}). Retrying...\n"
351
+ )
291
352
  raise RequestException(f"Failed to load page: {url}")
292
353
 
293
354
  return page
294
355
 
295
- def _parse_capsule_price(self, capsule_page, capsule_href):
356
+ def _parse_item_price(self, item_page, item_href):
296
357
  """
297
- Parse the price of a capsule from the given page and href.
358
+ Parse the price of an item from the given steamcommunity market page and item
359
+ href.
298
360
 
299
- :param capsule_page: The HTTP response object containing the capsule page
300
- content.
301
- :param capsule_href: The href of the capsule listing to find the price for.
302
- :return: The price of the capsule as a float.
303
- :raises ValueError: If the capsule listing or price span cannot be found.
361
+ :param item_page: The HTTP response object containing the item page content.
362
+ :param item_href: The href of the item listing to find the price for.
363
+ :return: The price of the item as a float.
364
+ :raises ValueError: If the item listing or price span cannot be found.
304
365
  """
305
- capsule_soup = BeautifulSoup(capsule_page.content, "html.parser")
306
- capsule_listing = capsule_soup.find("a", attrs={"href": f"{capsule_href}"})
307
- if not isinstance(capsule_listing, Tag):
308
- raise ValueError(f"Failed to find capsule listing: {capsule_href}")
366
+ item_soup = BeautifulSoup(item_page.content, "html.parser")
367
+ item_listing = item_soup.find("a", attrs={"href": f"{item_href}"})
368
+ if not isinstance(item_listing, Tag):
369
+ raise ValueError(f"Failed to find item listing: {item_href}")
309
370
 
310
- price_span = capsule_listing.find("span", attrs={"class": "normal_price"})
311
- if not isinstance(price_span, Tag):
312
- raise ValueError(f"Failed to find price span in capsule listing: {capsule_href}")
371
+ item_price_span = item_listing.find("span", attrs={"class": "normal_price"})
372
+ if not isinstance(item_price_span, Tag):
373
+ raise ValueError(f"Failed to find price span in item listing: {item_href}")
313
374
 
314
- price_str = price_span.text.split()[2]
375
+ price_str = item_price_span.text.split()[2]
315
376
  price = float(price_str.replace("$", ""))
316
377
 
317
378
  return price
@@ -330,26 +391,35 @@ class Scraper:
330
391
  hrefs, and names.
331
392
  """
332
393
  capsule_title = capsule_section.center(MAX_LINE_LEN, SEPARATOR)
333
- self.console.print(f"[bold magenta]{capsule_title}")
394
+ self.console.print(f"[bold magenta]{capsule_title}\n")
334
395
 
335
396
  capsule_usd_total = 0
336
- capsule_page = self._get_page(capsule_info["page"])
337
- for capsule_name, capsule_href in zip(capsule_info["names"], capsule_info["items"]):
338
- config_capsule_name = capsule_name.replace(" ", "_")
339
- owned = self.config.getint(capsule_section, config_capsule_name, fallback=0)
340
- if owned == 0:
341
- continue
342
-
343
- price_usd = self._parse_capsule_price(capsule_page, capsule_href)
344
- price_usd_owned = round(float(owned * price_usd), 2)
345
-
346
- self.console.print(f"[bold deep_sky_blue4]{capsule_name}")
347
- self.console.print(PRICE_INFO.format(owned, price_usd, price_usd_owned))
348
- capsule_usd_total += price_usd_owned
397
+ try:
398
+ capsule_page = self._get_page(capsule_info["page"])
399
+ for capsule_name, capsule_href in zip(capsule_info["names"], capsule_info["items"]):
400
+ config_capsule_name = capsule_name.replace(" ", "_")
401
+ owned = self.config.getint(capsule_section, config_capsule_name, fallback=0)
402
+ if owned == 0:
403
+ continue
404
+
405
+ price_usd = self._parse_item_price(capsule_page, capsule_href)
406
+ price_usd_owned = round(float(owned * price_usd), 2)
407
+
408
+ self.console.print(f"[bold deep_sky_blue4]{capsule_name}")
409
+ self.console.print(PRICE_INFO.format(owned, price_usd, price_usd_owned))
410
+ capsule_usd_total += price_usd_owned
411
+ except (RetryError, ValueError):
412
+ self.console.print(
413
+ "[bold red][!] Failed to scrape capsule prices. (Consider using proxies to prevent rate limiting)\n"
414
+ )
415
+ except Exception as error:
416
+ self.console.print(
417
+ f"[bold red][!] An unexpected error occurred while scraping capsule prices: {error}\n"
418
+ )
349
419
 
350
420
  return capsule_usd_total
351
421
 
352
- def scrape_capsule_section_prices(self):
422
+ def _scrape_capsule_section_prices(self):
353
423
  """Scrape prices for all capsule sections defined in the configuration."""
354
424
  capsule_usd_total = 0
355
425
  for capsule_section, capsule_info in CAPSULE_INFO.items():
@@ -359,28 +429,18 @@ class Scraper:
359
429
 
360
430
  return capsule_usd_total
361
431
 
362
- def _parse_case_price(self, case_page, case_href):
432
+ def _market_page_from_href(self, item_href):
363
433
  """
364
- Parse the price of a case from the given page and href.
434
+ Convert an href of a Steam Community Market item to a market page URL.
365
435
 
366
- :param case_page: The HTTP response object containing the case page content.
367
- :param case_href: The href of the case listing to find the price for.
368
- :return: The price of the case as a float.
369
- :raises ValueError: If the case listing or price span cannot be found.
436
+ :param item_href: The href of the item listing, typically ending with the item's
437
+ name.
438
+ :return: A URL string for the Steam Community Market page of the item.
370
439
  """
371
- case_soup = BeautifulSoup(case_page.content, "html.parser")
372
- case_listing = case_soup.find("a", attrs={"href": case_href})
373
- if not isinstance(case_listing, Tag):
374
- raise ValueError(f"Failed to find case listing: {case_href}")
375
-
376
- price_class = case_listing.find("span", attrs={"class": "normal_price"})
377
- if not isinstance(price_class, Tag):
378
- raise ValueError(f"Failed to find price class in case listing: {case_href}")
379
-
380
- price_str = price_class.text.split()[2]
381
- price = float(price_str.replace("$", ""))
440
+ url_encoded_name = item_href.split("/")[-1]
441
+ page_url = f"https://steamcommunity.com/market/search?q={url_encoded_name}"
382
442
 
383
- return price
443
+ return page_url
384
444
 
385
445
  def _scrape_case_prices(self):
386
446
  """
@@ -396,19 +456,68 @@ class Scraper:
396
456
 
397
457
  case_name = config_case_name.replace("_", " ").title()
398
458
  case_title = case_name.center(MAX_LINE_LEN, SEPARATOR)
399
- self.console.print(f"[bold magenta]{case_title}")
459
+ self.console.print(f"[bold magenta]{case_title}\n")
460
+
461
+ try:
462
+ case_page_url = self._market_page_from_href(CASE_HREFS[case_index])
463
+ case_page = self._get_page(case_page_url)
464
+ price_usd = self._parse_item_price(case_page, CASE_HREFS[case_index])
465
+ price_usd_owned = round(float(int(owned) * price_usd), 2)
466
+
467
+ self.console.print(PRICE_INFO.format(owned, price_usd, price_usd_owned))
468
+ case_usd_total += price_usd_owned
469
+
470
+ if not self.config.getboolean("App Settings", "use_proxy", fallback=False):
471
+ time.sleep(1)
472
+ except (RetryError, ValueError):
473
+ self.console.print(
474
+ "[bold red][!] Failed to scrape case prices. (Consider using proxies to prevent rate limiting)\n"
475
+ )
476
+ except Exception as error:
477
+ self.console.print(
478
+ f"[bold red][!] An unexpected error occurred while scraping case prices: {error}\n"
479
+ )
400
480
 
401
- case_page = self._get_page(CASE_PAGES[case_index])
402
- price_usd = self._parse_case_price(case_page, CASE_HREFS[case_index])
403
- price_usd_owned = round(float(int(owned) * price_usd), 2)
481
+ return case_usd_total
404
482
 
405
- self.console.print(PRICE_INFO.format(owned, price_usd, price_usd_owned))
406
- case_usd_total += price_usd_owned
483
+ def _scrape_custom_item_prices(self):
484
+ """
485
+ Scrape prices for custom items defined in the configuration.
407
486
 
408
- if not self.config.getboolean("App Settings", "use_proxy", fallback=False):
409
- time.sleep(1)
487
+ For each custom item, it prints the item name, owned count, price per item, and
488
+ total price for owned items.
489
+ """
490
+ custom_item_usd_total = 0
491
+ for config_custom_item_name, owned_and_href in self.config.items("Custom Items"):
492
+ owned, custom_item_href = owned_and_href.split(" ", 1)
493
+ if int(owned) == 0:
494
+ continue
410
495
 
411
- return case_usd_total
496
+ custom_item_name = config_custom_item_name.replace("_", " ").title()
497
+ custom_item_title = custom_item_name.center(MAX_LINE_LEN, SEPARATOR)
498
+ self.console.print(f"[bold magenta]{custom_item_title}\n")
499
+
500
+ try:
501
+ custom_item_page_url = self._market_page_from_href(custom_item_href)
502
+ custom_item_page = self._get_page(custom_item_page_url)
503
+ price_usd = self._parse_item_price(custom_item_page, custom_item_href)
504
+ price_usd_owned = round(float(int(owned) * price_usd), 2)
505
+
506
+ self.console.print(PRICE_INFO.format(owned, price_usd, price_usd_owned))
507
+ custom_item_usd_total += price_usd_owned
508
+
509
+ if not self.config.getboolean("App Settings", "use_proxy", fallback=False):
510
+ time.sleep(1)
511
+ except (RetryError, ValueError):
512
+ self.console.print(
513
+ "[bold red][!] Failed to scrape custom item prices. (Consider using proxies to prevent rate limiting)\n"
514
+ )
515
+ except Exception as error:
516
+ self.console.print(
517
+ f"[bold red][!] An unexpected error occurred while scraping custom item prices: {error}\n"
518
+ )
519
+
520
+ return custom_item_usd_total
412
521
 
413
522
  def identify_background_task(self):
414
523
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cs2tracker
3
- Version: 2.1.7
3
+ Version: 2.1.9
4
4
  Summary: Tracking the steam market prices of CS2 items
5
5
  Home-page: https://github.com/ashiven/cs2tracker
6
6
  Author: Jannik Novak
@@ -42,6 +42,7 @@ Dynamic: license-file
42
42
 
43
43
  - Download and install the latest versions of [Python](https://www.python.org/downloads/) and [Pip](https://pypi.org/project/pip/). (Required on Linux)
44
44
  - Register for the [Crawlbase Smart Proxy API](https://crawlbase.com/) and retrieve your API key. (Optional)
45
+ - Create a [Discord Webhook](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) to be notified about recent price updates. (Optional)
45
46
 
46
47
  ### Setup
47
48
 
@@ -58,6 +59,7 @@ Dynamic: license-file
58
59
  ```
59
60
 
60
61
  2. Run it:
62
+
61
63
  ```bash
62
64
  cs2tracker
63
65
  ```
@@ -65,7 +67,7 @@ Dynamic: license-file
65
67
  ### Options
66
68
 
67
69
  - `Run!` to gather the current market prices of your items and calculate the total amount in USD and EUR.
68
- - `Edit Config` to change the specific numbers of each item you own and then save the config file.
70
+ - `Edit Config` to specify the numbers of items owned in the config file. You can also add items other than cases and sticker capsules following the format in the `Custom Items` section. (item_name = item_owned item_page)
69
71
  - `Show History` to see a price chart consisting of past calculations. A new data point is generated once a day upon running the program.
70
72
  - `Daily Background Calculations` to automatically run a daily calculation of your investment in the background and save the results such that they can later be viewed via `Show History`.
71
73
  - `Receive Discord Notifications` to receive a notification on your Discord server when the program has finished calculating your investment. 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 file.
@@ -0,0 +1,16 @@
1
+ cs2tracker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ cs2tracker/__main__.py,sha256=Ub--oSMv48YzfWF1CZqYlkn1-HvZ7Bhxoc7urn1oY6o,249
3
+ cs2tracker/_version.py,sha256=RhQLK1m9uLWnJF6WDfppvypZmuwbm4NwHzCGbpSNQe8,511
4
+ cs2tracker/application.py,sha256=R_a8n0AgKLCHEG5O8K8_Q246Q_sopzRHvOQi4HtA0sE,9632
5
+ cs2tracker/constants.py,sha256=2jbg5tkRe3dFb_h47NG_CWNr11to3d3ojrr2s3YjyMY,27471
6
+ cs2tracker/main.py,sha256=jXEgZIpM_cDENXOaXCVTg2n50Xso7btI5FImg9BBeXQ,1041
7
+ cs2tracker/padded_console.py,sha256=lPEa34p-8LTmTbpf-2S5uYPaA2UmsIOPq2_UoVhMRgU,674
8
+ cs2tracker/scraper.py,sha256=G-7e2zwCoFt6ZCdo-PwLUtD7ZYd4Wdk2r0jgDoctN6A,26039
9
+ cs2tracker/data/config.ini,sha256=960jvrTt6ZOwCrHTVC5Q4Uw9lVGNnVRY7-kG6-k_Mig,5197
10
+ cs2tracker/data/output.csv,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ cs2tracker-2.1.9.dist-info/licenses/LICENSE.md,sha256=G5wqQ_8KGA808kVuF-Fpu_Yhteg8K_5ux9n2v8eQK7s,1069
12
+ cs2tracker-2.1.9.dist-info/METADATA,sha256=TyaeU7b7hlEw32XtOQgp9U8xOf0bisfTYspqoGZkA7Y,3734
13
+ cs2tracker-2.1.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ cs2tracker-2.1.9.dist-info/entry_points.txt,sha256=K8IwDIkg8QztSB9g9c89B9jR_2pG4QyJGrNs4z5RcZw,63
15
+ cs2tracker-2.1.9.dist-info/top_level.txt,sha256=2HB4xDDOxaU5BDc_yvdi9UlYLgL768n8aR-hRhFM6VQ,11
16
+ cs2tracker-2.1.9.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- cs2tracker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- cs2tracker/__main__.py,sha256=Ub--oSMv48YzfWF1CZqYlkn1-HvZ7Bhxoc7urn1oY6o,249
3
- cs2tracker/_version.py,sha256=THS-9QFidqyyR34eNGqu3fdLul8R-2jelw5ShM5U3hQ,511
4
- cs2tracker/application.py,sha256=aIWNLi0ufq6R7Hi86mPWCqS_7Prr7tIzvvXcnx_6Sbs,8991
5
- cs2tracker/constants.py,sha256=swkxVJQSwvavTorL3t_pBOHLtcgP3SaU9SU98XXuj48,29199
6
- cs2tracker/main.py,sha256=6mlmpOhI7gCithWCLz-EVD1wabxRd-DWvnNku3s5DNs,1014
7
- cs2tracker/padded_console.py,sha256=lPEa34p-8LTmTbpf-2S5uYPaA2UmsIOPq2_UoVhMRgU,674
8
- cs2tracker/scraper.py,sha256=vARZSpYPKflPS7VBexFbtql3_b13QDwHxrzn8fUzu34,20887
9
- cs2tracker/data/config.ini,sha256=ogy_g_UV4EUmwaMAoHTYO5NhkBkzU4dQo7um-d4kd_k,5027
10
- cs2tracker/data/output.csv,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- cs2tracker-2.1.7.dist-info/licenses/LICENSE.md,sha256=G5wqQ_8KGA808kVuF-Fpu_Yhteg8K_5ux9n2v8eQK7s,1069
12
- cs2tracker-2.1.7.dist-info/METADATA,sha256=21Ug5DWN90oXfZoCGXI3dmlu5wc2SoHJUHV8vTvIAaw,3452
13
- cs2tracker-2.1.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- cs2tracker-2.1.7.dist-info/entry_points.txt,sha256=K8IwDIkg8QztSB9g9c89B9jR_2pG4QyJGrNs4z5RcZw,63
15
- cs2tracker-2.1.7.dist-info/top_level.txt,sha256=2HB4xDDOxaU5BDc_yvdi9UlYLgL768n8aR-hRhFM6VQ,11
16
- cs2tracker-2.1.7.dist-info/RECORD,,