cs2tracker 2.1.16__tar.gz → 2.1.18__tar.gz

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.

Files changed (45) hide show
  1. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/.gitignore +3 -1
  2. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/PKG-INFO +28 -7
  3. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/README.md +27 -6
  4. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker/_version.py +2 -2
  5. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker/app/app.py +48 -15
  6. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker/app/editor_frame.py +99 -45
  7. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker/config.py +61 -28
  8. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker/constants.py +86 -25
  9. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker/data/config.ini +14 -0
  10. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker/data/convert_inventory.js +50 -19
  11. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker/data/get_inventory.js +53 -24
  12. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker.egg-info/PKG-INFO +28 -7
  13. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/.flake8 +0 -0
  14. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/.isort.cfg +0 -0
  15. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/.pre-commit-config.yaml +0 -0
  16. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/.pylintrc +0 -0
  17. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/LICENSE +0 -0
  18. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/MANIFEST.in +0 -0
  19. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/assets/demo.gif +0 -0
  20. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/assets/icon.png +0 -0
  21. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker/__init__.py +0 -0
  22. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker/__main__.py +0 -0
  23. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker/app/__init__.py +0 -0
  24. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker/app/history_frame.py +0 -0
  25. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker/app/scraper_frame.py +0 -0
  26. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker/data/output.csv +0 -0
  27. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker/logs.py +0 -0
  28. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker/main.py +0 -0
  29. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker/scraper/__init__.py +0 -0
  30. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker/scraper/background_task.py +0 -0
  31. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker/scraper/discord_notifier.py +0 -0
  32. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker/scraper/parser.py +0 -0
  33. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker/scraper/scraper.py +0 -0
  34. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker/util/__init__.py +0 -0
  35. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker/util/currency_conversion.py +0 -0
  36. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker/util/padded_console.py +0 -0
  37. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker/util/tkinter.py +0 -0
  38. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker.egg-info/SOURCES.txt +0 -0
  39. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker.egg-info/dependency_links.txt +0 -0
  40. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker.egg-info/entry_points.txt +0 -0
  41. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker.egg-info/requires.txt +0 -0
  42. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/cs2tracker.egg-info/top_level.txt +0 -0
  43. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/pyproject.toml +0 -0
  44. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/requirements.txt +0 -0
  45. {cs2tracker-2.1.16 → cs2tracker-2.1.18}/setup.cfg +0 -0
@@ -14,8 +14,10 @@ cs2tracker.egg-info
14
14
  # Auto generated version file on build
15
15
  _version.py
16
16
 
17
- # PyInstaller output
17
+ # PyInstaller files
18
18
  output
19
+ cs2tracker.spec
20
+ build.ps1
19
21
 
20
22
  # Files generated by the program
21
23
  cs2tracker_scraper.bat
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cs2tracker
3
- Version: 2.1.16
3
+ Version: 2.1.18
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
@@ -54,6 +54,7 @@ Dynamic: license-file
54
54
  - [Usage](#usage)
55
55
  - [Configuration](#configuration)
56
56
  - [Advanced Features](#advanced-features)
57
+ - [FAQ](#faq)
57
58
  - [Contributing](#contributing)
58
59
  - [License](#license)
59
60
 
@@ -74,8 +75,9 @@ Dynamic: license-file
74
75
  #### Method 1: Executable
75
76
 
76
77
  Simply download the program and run it:
77
- - [Windows](https://github.com/ashiven/cs2tracker/releases/latest/download/cs2tracker-windows.zip)
78
- - [Linux](https://github.com/ashiven/cs2tracker/releases/latest/download/cs2tracker-linux.zip)
78
+
79
+ - [Windows](https://github.com/ashiven/cs2tracker/releases/latest/download/cs2tracker-windows.zip)
80
+ - [Linux](https://github.com/ashiven/cs2tracker/releases/latest/download/cs2tracker-linux.zip)
79
81
 
80
82
  #### Method 2: Install via Pip
81
83
 
@@ -90,6 +92,7 @@ Simply download the program and run it:
90
92
  ```bash
91
93
  cs2tracker
92
94
  ```
95
+
93
96
  ### Additional Setup
94
97
 
95
98
  - Register for the [Crawlbase Smart Proxy API](https://crawlbase.com/) and retrieve your API key. (Optional)
@@ -117,18 +120,36 @@ This will open the config editor where you can change any setting by double clic
117
120
 
118
121
  - Enable **Daily Background Calculations** to automatically run a daily calculation of your investment in the background.
119
122
  - 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`.
123
+ - 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
124
  - Enable **Proxy Requests** to prevent your requests from being rate limited by the steamcommunity server.
122
125
  - 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
126
 
127
+ ## FAQ
128
+
129
+ - **Q: Is it safe to login with my Steam account?**
130
+ - **A:** Yes, the program uses the [SteamUser](https://github.com/DoctorMcKay/node-steam-user?tab=readme-ov-file#methods-) and [Globaloffensive](https://github.com/DoctorMcKay/node-globaloffensive) libraries to sign in and import your Storage Units (the same method is used by [casemove](https://github.com/nombersDev/casemove)) and all of the login-related code is transparently available in [this file](cs2tracker/data/get_inventory.js).
131
+
132
+
133
+ - **Q: Do I have to login with my Steam account?**
134
+ - **A:** No, you can also manually specify the number of items you own in the config editor.
135
+
136
+
137
+ - **Q: Can I get VAC-banned for using this program?**
138
+ - **A:** No, this program does not interact with the game in any way and only reads your Storage Units.
139
+
140
+
141
+ - **Q: Why does Windows Defender flag this program as potentially harmful?**
142
+ - **A:** This is because the program is not signed with a [Code Signing Certificate](https://www.globalsign.com/en/code-signing-certificate/what-is-code-signing-certificate), which Windows uses to verify the identity of publishers. These certificates are very expensive and not something I am willing to invest in for a free and open source project like this.
143
+
124
144
  ## Contributing
125
145
 
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.
146
+ Please feel free to submit a [pull request](https://github.com/ashiven/cs2tracker/pulls) or open an [issue](https://github.com/ashiven/cs2tracker/issues).
127
147
 
128
148
  1. Fork the repository
129
- 2. Create a new branch
149
+ 2. Create a new branch: `git checkout -b feature-name`.
130
150
  3. Make your changes
131
- 4. Submit a PR
151
+ 4. Push your branch: `git push origin feature-name`.
152
+ 5. Submit a PR
132
153
 
133
154
  ## License
134
155
 
@@ -26,6 +26,7 @@
26
26
  - [Usage](#usage)
27
27
  - [Configuration](#configuration)
28
28
  - [Advanced Features](#advanced-features)
29
+ - [FAQ](#faq)
29
30
  - [Contributing](#contributing)
30
31
  - [License](#license)
31
32
 
@@ -46,8 +47,9 @@
46
47
  #### Method 1: Executable
47
48
 
48
49
  Simply download the program and run it:
49
- - [Windows](https://github.com/ashiven/cs2tracker/releases/latest/download/cs2tracker-windows.zip)
50
- - [Linux](https://github.com/ashiven/cs2tracker/releases/latest/download/cs2tracker-linux.zip)
50
+
51
+ - [Windows](https://github.com/ashiven/cs2tracker/releases/latest/download/cs2tracker-windows.zip)
52
+ - [Linux](https://github.com/ashiven/cs2tracker/releases/latest/download/cs2tracker-linux.zip)
51
53
 
52
54
  #### Method 2: Install via Pip
53
55
 
@@ -62,6 +64,7 @@ Simply download the program and run it:
62
64
  ```bash
63
65
  cs2tracker
64
66
  ```
67
+
65
68
  ### Additional Setup
66
69
 
67
70
  - Register for the [Crawlbase Smart Proxy API](https://crawlbase.com/) and retrieve your API key. (Optional)
@@ -89,18 +92,36 @@ This will open the config editor where you can change any setting by double clic
89
92
 
90
93
  - Enable **Daily Background Calculations** to automatically run a daily calculation of your investment in the background.
91
94
  - Use **Receive Discord Notifications** to receive a notification on your Discord server whenever the program has finished calculating your investment.
92
- - 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`.
95
+ - 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`.
93
96
  - Enable **Proxy Requests** to prevent your requests from being rate limited by the steamcommunity server.
94
97
  - 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`.
95
98
 
99
+ ## FAQ
100
+
101
+ - **Q: Is it safe to login with my Steam account?**
102
+ - **A:** Yes, the program uses the [SteamUser](https://github.com/DoctorMcKay/node-steam-user?tab=readme-ov-file#methods-) and [Globaloffensive](https://github.com/DoctorMcKay/node-globaloffensive) libraries to sign in and import your Storage Units (the same method is used by [casemove](https://github.com/nombersDev/casemove)) and all of the login-related code is transparently available in [this file](cs2tracker/data/get_inventory.js).
103
+
104
+
105
+ - **Q: Do I have to login with my Steam account?**
106
+ - **A:** No, you can also manually specify the number of items you own in the config editor.
107
+
108
+
109
+ - **Q: Can I get VAC-banned for using this program?**
110
+ - **A:** No, this program does not interact with the game in any way and only reads your Storage Units.
111
+
112
+
113
+ - **Q: Why does Windows Defender flag this program as potentially harmful?**
114
+ - **A:** This is because the program is not signed with a [Code Signing Certificate](https://www.globalsign.com/en/code-signing-certificate/what-is-code-signing-certificate), which Windows uses to verify the identity of publishers. These certificates are very expensive and not something I am willing to invest in for a free and open source project like this.
115
+
96
116
  ## Contributing
97
117
 
98
- 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.
118
+ Please feel free to submit a [pull request](https://github.com/ashiven/cs2tracker/pulls) or open an [issue](https://github.com/ashiven/cs2tracker/issues).
99
119
 
100
120
  1. Fork the repository
101
- 2. Create a new branch
121
+ 2. Create a new branch: `git checkout -b feature-name`.
102
122
  3. Make your changes
103
- 4. Submit a PR
123
+ 4. Push your branch: `git push origin feature-name`.
124
+ 5. Submit a PR
104
125
 
105
126
  ## License
106
127
 
@@ -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.16'
21
- __version_tuple__ = version_tuple = (2, 1, 16)
20
+ __version__ = version = '2.1.18'
21
+ __version_tuple__ = version_tuple = (2, 1, 18)
@@ -15,6 +15,7 @@ from cs2tracker.logs import PriceLogs
15
15
  from cs2tracker.scraper.background_task import BackgroundTask
16
16
  from cs2tracker.scraper.scraper import Scraper
17
17
  from cs2tracker.util.currency_conversion import CURRENCY_SYMBOLS
18
+ from cs2tracker.util.padded_console import get_console
18
19
  from cs2tracker.util.tkinter import centered, fix_sv_ttk, size_info
19
20
 
20
21
  APPLICATION_NAME = "CS2Tracker"
@@ -31,6 +32,7 @@ PRICE_HISTORY_TITLE = "Price History"
31
32
  PRICE_HISTORY_SIZE = "900x700"
32
33
 
33
34
  config = get_config()
35
+ console = get_console()
34
36
 
35
37
 
36
38
  class Application:
@@ -48,6 +50,7 @@ class Application:
48
50
 
49
51
  fix_sv_ttk(ttk.Style())
50
52
 
53
+ window.focus_force()
51
54
  window.mainloop()
52
55
 
53
56
  def _configure_window(self):
@@ -76,6 +79,10 @@ class MainFrame(ttk.Frame):
76
79
  super().__init__(parent, padding=15)
77
80
  self.parent = parent
78
81
  self.scraper = scraper
82
+
83
+ self.scraper_window = None
84
+ self.config_editor_window = None
85
+ self.price_history_window = None
79
86
  self._add_widgets()
80
87
 
81
88
  def _add_widgets(self):
@@ -181,13 +188,21 @@ class MainFrame(ttk.Frame):
181
188
  """Scrape prices from the configured sources, print the total, and save the
182
189
  results to a file.
183
190
  """
184
- scraper_window = tk.Toplevel(self.parent)
185
- scraper_window.geometry(centered(scraper_window, SCRAPER_WINDOW_SIZE))
186
- scraper_window.minsize(*size_info(SCRAPER_WINDOW_SIZE))
187
- scraper_window.title(SCRAPER_WINDOW_TITLE)
191
+ if self.scraper_window is None or not self.scraper_window.winfo_exists():
192
+ self._open_scraper_window()
193
+ else:
194
+ self.scraper_window.lift()
195
+ self.scraper_window.focus_set()
196
+
197
+ def _open_scraper_window(self):
198
+ """Open a new window with the scraper GUI."""
199
+ self.scraper_window = tk.Toplevel(self.parent)
200
+ self.scraper_window.geometry(centered(self.scraper_window, SCRAPER_WINDOW_SIZE))
201
+ self.scraper_window.minsize(*size_info(SCRAPER_WINDOW_SIZE))
202
+ self.scraper_window.title(SCRAPER_WINDOW_TITLE)
188
203
 
189
204
  run_frame = ScraperFrame(
190
- scraper_window,
205
+ self.scraper_window,
191
206
  self.scraper,
192
207
  sheet_size=SCRAPER_WINDOW_SIZE,
193
208
  dark_theme=self.dark_theme_checkbox_value.get(),
@@ -196,26 +211,42 @@ class MainFrame(ttk.Frame):
196
211
  run_frame.start()
197
212
 
198
213
  def _edit_config(self):
214
+ """Open a new window with a config editor GUI or lift the existing one."""
215
+ if self.config_editor_window is None or not self.config_editor_window.winfo_exists():
216
+ self._open_config_editor()
217
+ else:
218
+ self.config_editor_window.lift()
219
+ self.config_editor_window.focus_set()
220
+
221
+ def _open_config_editor(self):
199
222
  """Open a new window with a config editor GUI."""
200
- config_editor_window = tk.Toplevel(self.parent)
201
- config_editor_window.geometry(centered(config_editor_window, CONFIG_EDITOR_SIZE))
202
- config_editor_window.minsize(*size_info(CONFIG_EDITOR_SIZE))
203
- config_editor_window.title(CONFIG_EDITOR_TITLE)
223
+ self.config_editor_window = tk.Toplevel(self.parent)
224
+ self.config_editor_window.geometry(centered(self.config_editor_window, CONFIG_EDITOR_SIZE))
225
+ self.config_editor_window.minsize(*size_info(CONFIG_EDITOR_SIZE))
226
+ self.config_editor_window.title(CONFIG_EDITOR_TITLE)
204
227
 
205
- editor_frame = ConfigEditorFrame(config_editor_window)
228
+ editor_frame = ConfigEditorFrame(self.config_editor_window)
206
229
  editor_frame.pack(expand=True, fill="both")
207
230
 
208
231
  def _show_history(self):
209
232
  """Show a chart consisting of past calculations."""
233
+ if self.price_history_window is None or not self.price_history_window.winfo_exists():
234
+ self._open_history_window()
235
+ else:
236
+ self.price_history_window.lift()
237
+ self.price_history_window.focus_set()
238
+
239
+ def _open_history_window(self):
240
+ """Open a new window with a price history GUI."""
210
241
  if PriceLogs.empty():
211
242
  return
212
243
 
213
- price_history_window = tk.Toplevel(self.parent)
214
- price_history_window.geometry(centered(price_history_window, PRICE_HISTORY_SIZE))
215
- price_history_window.minsize(*size_info(PRICE_HISTORY_SIZE))
216
- price_history_window.title(PRICE_HISTORY_TITLE)
244
+ self.price_history_window = tk.Toplevel(self.parent)
245
+ self.price_history_window.geometry(centered(self.price_history_window, PRICE_HISTORY_SIZE))
246
+ self.price_history_window.minsize(*size_info(PRICE_HISTORY_SIZE))
247
+ self.price_history_window.title(PRICE_HISTORY_TITLE)
217
248
 
218
- history_frame = PriceHistoryFrame(price_history_window)
249
+ history_frame = PriceHistoryFrame(self.price_history_window)
219
250
  history_frame.pack(expand=True, fill="both")
220
251
 
221
252
  def _export_log_file(self):
@@ -239,8 +270,10 @@ class MainFrame(ttk.Frame):
239
270
  filetypes=[("CSV files", "*.csv")],
240
271
  )
241
272
  if not PriceLogs.validate_file(import_path):
273
+ console.error("Invalid log file format.")
242
274
  return
243
275
  copy(import_path, OUTPUT_FILE)
276
+ console.info("Log file imported successfully.")
244
277
 
245
278
  def _toggle_background_task(self, enabled: bool):
246
279
  """Toggle whether a daily price calculation should run in the background."""
@@ -8,7 +8,7 @@ from tkinter import messagebox, ttk
8
8
  from nodejs import node
9
9
  from ttk_text import ThemedText
10
10
 
11
- from cs2tracker.config import get_config
11
+ from cs2tracker.config import CUSTOM_SECTIONS, get_config
12
12
  from cs2tracker.constants import (
13
13
  CONFIG_FILE,
14
14
  CONFIG_FILE_BACKUP,
@@ -131,7 +131,7 @@ class ConfigEditorFrame(ttk.Frame):
131
131
  if selected:
132
132
  item = selected[0]
133
133
  section_name = self.tree.parent(item)
134
- if section_name in ("Stickers", "Skins"):
134
+ if section_name in CUSTOM_SECTIONS:
135
135
  next_option = self.tree.next(item)
136
136
  self.tree.delete(item)
137
137
  self.save_config()
@@ -169,31 +169,19 @@ class ConfigEditorFrame(ttk.Frame):
169
169
  continue
170
170
 
171
171
  section_level = self.tree.insert("", "end", iid=section, text=section)
172
-
173
- # Items in the Stickers, Cases, and Skins sections should be displayed alphabetically sorted
174
- section_items = config.items(section)
175
- if section in ("Stickers", "Cases", "Skins"):
176
- section_items = sorted(section_items)
177
-
178
- for config_option, value in section_items:
172
+ sorted_section_items = sorted(config.items(section))
173
+ for config_option, value in sorted_section_items:
179
174
  if section not in ("User Settings", "App Settings"):
180
175
  option_name = config.option_to_name(config_option, href=True)
181
- self.tree.insert(
182
- section_level,
183
- "end",
184
- iid=f"{section}-{option_name}",
185
- text=option_name,
186
- values=[value],
187
- )
188
176
  else:
189
177
  option_name = config.option_to_name(config_option)
190
- self.tree.insert(
191
- section_level,
192
- "end",
193
- iid=f"{section}-{option_name}",
194
- text=option_name,
195
- values=[value],
196
- )
178
+ self.tree.insert(
179
+ section_level,
180
+ "end",
181
+ iid=f"{section}-{option_name}",
182
+ text=option_name,
183
+ values=[value],
184
+ )
197
185
 
198
186
  self.tree.focus("User Settings")
199
187
  self.tree.selection_set("User Settings")
@@ -253,6 +241,8 @@ class ConfigEditorButtonFrame(ttk.Frame):
253
241
  self.editor_frame = editor_frame
254
242
  self.custom_item_dialog = None
255
243
 
244
+ self.custom_item_window = None
245
+ self.steam_inventory_window = None
256
246
  self._add_widgets()
257
247
 
258
248
  def _add_widgets(self):
@@ -265,6 +255,25 @@ class ConfigEditorButtonFrame(ttk.Frame):
265
255
  custom_item_button = ttk.Button(self, text="Add Item", command=self._add_custom_item)
266
256
  custom_item_button.pack(side="left", expand=True, padx=5)
267
257
 
258
+ def open_custom_enter(_):
259
+ selected = self.editor_frame.tree.selection()
260
+ if selected and not self.editor_frame.tree.parent(selected[0]):
261
+ selected_section = self.editor_frame.tree.item(selected[0], "text")
262
+ if selected_section in CUSTOM_SECTIONS and not self.editor_frame.tree.get_children(
263
+ selected_section
264
+ ):
265
+ custom_item_button.invoke()
266
+
267
+ def open_custom_click(event):
268
+ selected_section = self.editor_frame.tree.identify_row(event.y)
269
+ if selected_section in CUSTOM_SECTIONS and not self.editor_frame.tree.get_children(
270
+ selected_section
271
+ ):
272
+ custom_item_button.invoke()
273
+
274
+ self.editor_frame.tree.bind("<Return>", open_custom_enter, add="+")
275
+ self.editor_frame.tree.bind("<Double-1>", open_custom_click, add="+")
276
+
268
277
  import_inventory_button = ttk.Button(
269
278
  self, text="Import Steam Inventory", command=self._import_steam_inventory
270
279
  )
@@ -285,37 +294,59 @@ class ConfigEditorButtonFrame(ttk.Frame):
285
294
  self.editor_frame.tree.focus_set()
286
295
 
287
296
  def _add_custom_item(self):
297
+ """Open a window to add a new custom item or lift the existing one if it is
298
+ already open.
299
+ """
300
+ if self.custom_item_window is None or not self.custom_item_window.winfo_exists():
301
+ self._open_custom_item_window()
302
+ else:
303
+ self.custom_item_window.lift()
304
+ self.custom_item_window.focus_set()
305
+
306
+ def _open_custom_item_window(self):
288
307
  """Open a window to add a new custom item."""
289
- custom_item_window = tk.Toplevel(self.editor_frame)
290
- custom_item_window.title(ADD_CUSTOM_ITEM_TITLE)
291
- custom_item_window.geometry(centered(custom_item_window, ADD_CUSTOM_ITEM_SIZE))
292
- custom_item_window.minsize(*size_info(ADD_CUSTOM_ITEM_SIZE))
293
- custom_item_window.focus_set()
308
+ self.custom_item_window = tk.Toplevel(self.editor_frame)
309
+ self.custom_item_window.title(ADD_CUSTOM_ITEM_TITLE)
310
+ self.custom_item_window.geometry(centered(self.custom_item_window, ADD_CUSTOM_ITEM_SIZE))
311
+ self.custom_item_window.minsize(*size_info(ADD_CUSTOM_ITEM_SIZE))
312
+ self.custom_item_window.focus_set()
294
313
 
295
314
  def on_close():
296
- custom_item_window.destroy()
315
+ self.custom_item_window.destroy() # type: ignore
297
316
  self.editor_frame.tree.focus_set()
298
317
 
299
- custom_item_window.protocol("WM_DELETE_WINDOW", on_close)
318
+ self.custom_item_window.protocol("WM_DELETE_WINDOW", on_close)
300
319
 
301
- custom_item_frame = CustomItemFrame(custom_item_window, self.editor_frame)
320
+ custom_item_frame = CustomItemFrame(self.custom_item_window, self.editor_frame)
302
321
  custom_item_frame.pack(expand=True, fill="both", padx=15, pady=15)
303
322
 
304
323
  def _import_steam_inventory(self):
324
+ """Open a window to import the user's Steam inventory or lift the existing one
325
+ if it is already open.
326
+ """
327
+ if self.steam_inventory_window is None or not self.steam_inventory_window.winfo_exists():
328
+ self._open_steam_inventory_window()
329
+ else:
330
+ self.steam_inventory_window.lift()
331
+ self.steam_inventory_window.focus_set()
332
+
333
+ def _open_steam_inventory_window(self):
305
334
  """Open a window to import the user's Steam inventory."""
306
- steam_inventory_window = tk.Toplevel(self.editor_frame)
307
- steam_inventory_window.title(IMPORT_INVENTORY_TITLE)
308
- steam_inventory_window.geometry(centered(steam_inventory_window, IMPORT_INVENTORY_SIZE))
309
- steam_inventory_window.minsize(*size_info(IMPORT_INVENTORY_SIZE))
310
- steam_inventory_window.focus_set()
335
+ self.steam_inventory_window = tk.Toplevel(self.editor_frame)
336
+ self.steam_inventory_window.title(IMPORT_INVENTORY_TITLE)
337
+ self.steam_inventory_window.geometry(
338
+ centered(self.steam_inventory_window, IMPORT_INVENTORY_SIZE)
339
+ )
340
+ self.steam_inventory_window.minsize(*size_info(IMPORT_INVENTORY_SIZE))
341
+ self.steam_inventory_window.focus_set()
311
342
 
312
343
  def on_close():
313
- steam_inventory_window.destroy()
344
+ self.steam_inventory_window.destroy() # type: ignore
314
345
  self.editor_frame.tree.focus_set()
315
346
 
316
- steam_inventory_window.protocol("WM_DELETE_WINDOW", on_close)
347
+ self.steam_inventory_window.protocol("WM_DELETE_WINDOW", on_close)
317
348
 
318
- steam_inventory_frame = InventoryImportFrame(steam_inventory_window, self.editor_frame)
349
+ steam_inventory_frame = InventoryImportFrame(self.steam_inventory_window, self.editor_frame)
319
350
  steam_inventory_frame.pack(expand=True, fill="both", padx=15, pady=15)
320
351
 
321
352
 
@@ -373,6 +404,30 @@ class CustomItemFrame(ttk.Frame):
373
404
  break
374
405
  return insert_index
375
406
 
407
+ def _identify_custom_section(self, item_name):
408
+ # pylint: disable=too-many-return-statements
409
+ """Given an item name, identify the custom section it belongs to."""
410
+ if "Patch Pack" in item_name or "Patch Collection" in item_name:
411
+ return "Patch Packs"
412
+ elif "Patch |" in item_name:
413
+ return "Patches"
414
+ elif "Sticker |" in item_name:
415
+ return "Stickers"
416
+ elif "Charm |" in item_name:
417
+ return "Charms"
418
+ elif "Souvenir" in item_name and "|" not in item_name:
419
+ return "Souvenirs"
420
+ elif "★ " in item_name:
421
+ return "Special Items"
422
+ elif " | " in item_name and "(" in item_name and ")" in item_name:
423
+ return "Skins"
424
+ elif "Music Kit |" in item_name:
425
+ return "Others"
426
+ elif " | " in item_name:
427
+ return "Agents"
428
+ else:
429
+ return "Others"
430
+
376
431
  def _add_custom_item(self, item_href, item_owned):
377
432
  """Add a custom item to the configuration."""
378
433
  if not item_href or not item_owned:
@@ -380,7 +435,7 @@ class CustomItemFrame(ttk.Frame):
380
435
  "Input Error", "All fields must be filled out.", parent=self.window
381
436
  )
382
437
  return
383
- if config.option_exists(item_href, exclude_sections=("Stickers", "Skins")):
438
+ if config.option_exists(item_href, exclude_sections=CUSTOM_SECTIONS):
384
439
  messagebox.showerror(
385
440
  "Item Exists", "This item already exists in another section.", parent=self.window
386
441
  )
@@ -392,12 +447,11 @@ class CustomItemFrame(ttk.Frame):
392
447
  messagebox.showerror("Invalid URL", str(error), parent=self.window)
393
448
  return
394
449
 
395
- if self._update_existing("Stickers", item_name, item_owned):
396
- return
397
- if self._update_existing("Skins", item_name, item_owned):
398
- return
450
+ for section in CUSTOM_SECTIONS:
451
+ if self._update_existing(section, item_name, item_owned):
452
+ return
399
453
 
400
- section = "Stickers" if item_name.startswith("Sticker") else "Skins"
454
+ section = self._identify_custom_section(item_name)
401
455
  insert_index = self._get_insert_index(item_name, section)
402
456
  self.editor_frame.tree.insert(
403
457
  section,
@@ -3,12 +3,53 @@ import re
3
3
  from configparser import ConfigParser, ParsingError
4
4
  from urllib.parse import quote, unquote
5
5
 
6
- from cs2tracker.constants import CAPSULE_PAGES, CONFIG_FILE, INVENTORY_IMPORT_FILE
6
+ from cs2tracker.constants import (
7
+ CONFIG_FILE,
8
+ INVENTORY_IMPORT_FILE,
9
+ )
7
10
  from cs2tracker.util.padded_console import get_console
8
11
 
9
12
  STEAM_MARKET_LISTING_BASEURL_CS2 = "https://steamcommunity.com/market/listings/730/"
10
13
  STEAM_MARKET_LISTING_REGEX = r"^https://steamcommunity.com/market/listings/\d+/.+$"
11
14
 
15
+ CUSTOM_SECTIONS = [
16
+ "Skins",
17
+ "Special Items",
18
+ "Agents",
19
+ "Charms",
20
+ "Patches",
21
+ "Patch Packs",
22
+ "Stickers",
23
+ "Souvenirs",
24
+ "Others",
25
+ ]
26
+
27
+ PREEXISTING_SECTIONS = [
28
+ "Cases",
29
+ "Katowice 2014 Sticker Capsule",
30
+ "Cologne 2014 Sticker Capsule",
31
+ "DreamHack 2014 Sticker Capsule",
32
+ "Katowice 2015 Sticker Capsule",
33
+ "Cologne 2015 Sticker Capsule",
34
+ "Cluj-Napoca 2015 Sticker Capsule",
35
+ "Columbus 2016 Sticker Capsule",
36
+ "Cologne 2016 Sticker Capsule",
37
+ "Atlanta 2017 Sticker Capsule",
38
+ "Krakow 2017 Sticker Capsule",
39
+ "Boston 2018 Sticker Capsule",
40
+ "London 2018 Sticker Capsule",
41
+ "Katowice 2019 Sticker Capsule",
42
+ "Berlin 2019 Sticker Capsule",
43
+ "2020 RMR Sticker Capsule",
44
+ "Stockholm 2021 Sticker Capsule",
45
+ "Antwerp 2022 Sticker Capsule",
46
+ "Rio 2022 Sticker Capsule",
47
+ "Paris 2023 Sticker Capsule",
48
+ "Copenhagen 2024 Sticker Capsule",
49
+ "Shanghai 2024 Sticker Capsule",
50
+ "Austin 2025 Sticker Capsule",
51
+ ]
52
+
12
53
  console = get_console()
13
54
 
14
55
 
@@ -46,19 +87,12 @@ class ValidatedConfig(ConfigParser):
46
87
 
47
88
  def _validate_config_sections(self):
48
89
  """Validate that the configuration file has all required sections."""
49
- if not self.has_section("User Settings"):
50
- raise ValueError("Missing 'User Settings' section in the configuration file.")
51
- if not self.has_section("App Settings"):
52
- raise ValueError("Missing 'App Settings' section in the configuration file.")
53
- if not self.has_section("Stickers"):
54
- raise ValueError("Missing 'Stickers' section in the configuration file.")
55
- if not self.has_section("Cases"):
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.")
59
- for capsule_section in CAPSULE_PAGES:
60
- if not self.has_section(capsule_section):
61
- raise ValueError(f"Missing '{capsule_section}' section in the configuration file.")
90
+ for section in CUSTOM_SECTIONS:
91
+ if not self.has_section(section):
92
+ raise ValueError(f"Missing '{section}' section in the configuration file.")
93
+ for section in PREEXISTING_SECTIONS:
94
+ if not self.has_section(section):
95
+ raise ValueError(f"Missing '{section}' section in the configuration file.")
62
96
 
63
97
  def _validate_config_values(self):
64
98
  # pylint: disable=too-many-branches
@@ -136,23 +170,22 @@ class ValidatedConfig(ConfigParser):
136
170
  try:
137
171
  with open(INVENTORY_IMPORT_FILE, "r", encoding="utf-8") as inventory_file:
138
172
  inventory_data = json.load(inventory_file)
139
- sorted_inventory_data = dict(sorted(inventory_data.items()))
140
173
 
141
174
  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)
144
- for section in self.sections():
145
- if option in self.options(section):
146
- self.set(section, option, str(item_owned))
147
- added_to_config.add(item_name)
148
-
149
- for item_name, item_owned in sorted_inventory_data.items():
150
- if item_name not in added_to_config:
175
+ for _, item_infos in inventory_data.items():
176
+ for item_name, item_owned in item_infos.items():
151
177
  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))
178
+ for section in self.sections():
179
+ if option in self.options(section):
180
+ self.set(section, option, str(item_owned))
181
+ added_to_config.add(item_name)
182
+
183
+ for section, item_infos in inventory_data.items():
184
+ sorted_item_infos = dict(sorted(item_infos.items()))
185
+ for item_name, item_owned in sorted_item_infos.items():
186
+ if item_name not in added_to_config:
187
+ option = self.name_to_option(item_name, href=True)
188
+ self.set(section, option, str(item_owned))
156
189
 
157
190
  self.write_to_file()
158
191
  except (FileNotFoundError, json.JSONDecodeError) as error:
@@ -1,12 +1,18 @@
1
+ import ctypes
1
2
  import enum
2
3
  import os
3
4
  import sys
5
+ import tkinter as tk
4
6
  from datetime import datetime
5
- from shutil import copy
7
+ from shutil import copy, copytree
6
8
  from subprocess import DEVNULL
9
+ from tkinter import ttk
7
10
 
11
+ import sv_ttk
8
12
  from nodejs import npm
9
13
 
14
+ from cs2tracker.util.tkinter import centered
15
+
10
16
  try:
11
17
  from cs2tracker._version import version # pylint: disable=E0611
12
18
 
@@ -48,33 +54,86 @@ if RUNNING_IN_EXE:
48
54
  else:
49
55
  raise NotImplementedError(f"Unsupported OS: {OS}")
50
56
 
51
- DATA_DIR = os.path.join(APP_DATA_DIR, "cs2tracker", "data")
52
- os.makedirs(DATA_DIR, exist_ok=True)
53
-
54
57
  CONFIG_FILE_SOURCE = os.path.join(MODULE_DIR, "data", "config.ini")
55
58
  OUTPUT_FILE_SOURCE = os.path.join(MODULE_DIR, "data", "output.csv")
56
- IVENTORY_CONVERT_SCRIPT_SOURCE = os.path.join(MODULE_DIR, "data", "convert_inventory.js")
59
+ INVENTORY_CONVERT_SCRIPT_SOURCE = os.path.join(MODULE_DIR, "data", "convert_inventory.js")
57
60
  INVENTORY_IMPORT_SCRIPT_SOURCE = os.path.join(MODULE_DIR, "data", "get_inventory.js")
61
+ NODE_MODULES_SOURCE = os.path.join(MODULE_DIR, "data", "node_modules")
58
62
 
63
+ DATA_DIR = os.path.join(APP_DATA_DIR, "cs2tracker", "data")
59
64
  CONFIG_FILE = os.path.join(DATA_DIR, "config.ini")
60
65
  CONFIG_FILE_BACKUP = os.path.join(DATA_DIR, "config.ini.bak")
61
66
  OUTPUT_FILE = os.path.join(DATA_DIR, "output.csv")
62
- IVENTORY_CONVERT_SCRIPT = os.path.join(DATA_DIR, "convert_inventory.js")
67
+ INVENTORY_CONVERT_SCRIPT = os.path.join(DATA_DIR, "convert_inventory.js")
63
68
  INVENTORY_IMPORT_SCRIPT = os.path.join(DATA_DIR, "get_inventory.js")
69
+ NODE_MODULES = os.path.join(DATA_DIR, "node_modules")
70
+
71
+ ICON_FILE = os.path.join(PROJECT_DIR, "assets", "icon.png")
72
+ BATCH_FILE = os.path.join(DATA_DIR, "cs2tracker_scraper.bat")
73
+ INVENTORY_IMPORT_FILE = os.path.join(DATA_DIR, "inventory.json")
74
+ INVENTORY_IMPORT_SCRIPT_DEPENDENCIES = [
75
+ "steam-user",
76
+ "globaloffensive",
77
+ "@node-steam/vdf",
78
+ "axios",
79
+ ]
80
+
81
+ def show_temp_popup():
82
+ """Show a temporary popup window while copying initial files."""
83
+ popup = tk.Tk()
84
+ popup.title("Please wait")
85
+ popup.geometry(centered(popup, "300x80"))
86
+ popup.resizable(False, False)
87
+ if OS == OSType.WINDOWS:
88
+ app_id = "cs2tracker.unique.id"
89
+ ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id)
90
+ icon = tk.PhotoImage(file=ICON_FILE)
91
+ popup.wm_iconphoto(True, icon)
92
+
93
+ label = ttk.Label(popup, text="Setting up the application. Please wait...")
94
+ label.pack(pady=20)
95
+
96
+ sv_ttk.use_dark_theme()
97
+
98
+ popup.update()
99
+ return popup
100
+
101
+ def copy_initial_files_with_popup():
102
+ """Copy initial files to the user data directory with a temporary popup."""
103
+ popup = show_temp_popup()
104
+
105
+ try:
106
+ if not os.path.exists(DATA_DIR):
107
+ os.makedirs(DATA_DIR)
108
+ if not os.path.exists(OUTPUT_FILE):
109
+ copy(OUTPUT_FILE_SOURCE, OUTPUT_FILE)
110
+ if not os.path.exists(CONFIG_FILE):
111
+ copy(CONFIG_FILE_SOURCE, CONFIG_FILE)
112
+ if not os.path.exists(INVENTORY_CONVERT_SCRIPT):
113
+ copy(INVENTORY_CONVERT_SCRIPT_SOURCE, INVENTORY_CONVERT_SCRIPT)
114
+ if not os.path.exists(INVENTORY_IMPORT_SCRIPT):
115
+ copy(INVENTORY_IMPORT_SCRIPT_SOURCE, INVENTORY_IMPORT_SCRIPT)
116
+ if not os.path.exists(NODE_MODULES):
117
+ copytree(NODE_MODULES_SOURCE, NODE_MODULES)
118
+ finally:
119
+ popup.destroy()
120
+
121
+ # pylint: disable=too-many-boolean-expressions
122
+ if (
123
+ not os.path.exists(DATA_DIR)
124
+ or not os.path.exists(OUTPUT_FILE)
125
+ or not os.path.exists(CONFIG_FILE)
126
+ or not os.path.exists(INVENTORY_CONVERT_SCRIPT)
127
+ or not os.path.exists(INVENTORY_IMPORT_SCRIPT)
128
+ or not os.path.exists(NODE_MODULES)
129
+ ):
130
+ copy_initial_files_with_popup()
64
131
 
65
132
  # Always copy the source config into the user data directory as a backup
66
133
  # and overwrite the existing backup if it exists
67
134
  # (This is to ensure that no outdated config backup remains in the user data directory)
68
135
  copy(CONFIG_FILE_SOURCE, CONFIG_FILE_BACKUP)
69
136
 
70
- if not os.path.exists(OUTPUT_FILE):
71
- copy(OUTPUT_FILE_SOURCE, OUTPUT_FILE)
72
- if not os.path.exists(CONFIG_FILE):
73
- copy(CONFIG_FILE_SOURCE, CONFIG_FILE)
74
- if not os.path.exists(IVENTORY_CONVERT_SCRIPT):
75
- copy(IVENTORY_CONVERT_SCRIPT_SOURCE, IVENTORY_CONVERT_SCRIPT)
76
- if not os.path.exists(INVENTORY_IMPORT_SCRIPT):
77
- copy(INVENTORY_IMPORT_SCRIPT_SOURCE, INVENTORY_IMPORT_SCRIPT)
78
137
 
79
138
  else:
80
139
  MODULE_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -86,24 +145,25 @@ else:
86
145
  OUTPUT_FILE = os.path.join(DATA_DIR, "output.csv")
87
146
  INVENTORY_CONVERT_SCRIPT = os.path.join(DATA_DIR, "convert_inventory.js")
88
147
  INVENTORY_IMPORT_SCRIPT = os.path.join(DATA_DIR, "get_inventory.js")
148
+ NODE_MODULES = os.path.join(DATA_DIR, "node_modules")
149
+
150
+ ICON_FILE = os.path.join(PROJECT_DIR, "assets", "icon.png")
151
+ BATCH_FILE = os.path.join(DATA_DIR, "cs2tracker_scraper.bat")
152
+ INVENTORY_IMPORT_FILE = os.path.join(DATA_DIR, "inventory.json")
153
+ INVENTORY_IMPORT_SCRIPT_DEPENDENCIES = [
154
+ "steam-user",
155
+ "globaloffensive",
156
+ "@node-steam/vdf",
157
+ "axios",
158
+ ]
89
159
 
90
160
  if not os.path.exists(CONFIG_FILE_BACKUP):
91
161
  copy(CONFIG_FILE, CONFIG_FILE_BACKUP)
92
162
 
93
163
 
94
- ICON_FILE = os.path.join(PROJECT_DIR, "assets", "icon.png")
95
- BATCH_FILE = os.path.join(DATA_DIR, "cs2tracker_scraper.bat")
96
- INVENTORY_IMPORT_FILE = os.path.join(DATA_DIR, "inventory.json")
97
- INVENTORY_IMPORT_SCRIPT_DEPENDENCIES = [
98
- "steam-user",
99
- "globaloffensive",
100
- "@node-steam/vdf",
101
- "axios",
102
- ]
103
-
104
164
  # Ensures that the necessary node modules are installed if a user wants
105
165
  # to import their steam inventory via the cs2tracker/data/get_inventory.js Node.js script.
106
- if not os.path.exists(os.path.join(DATA_DIR, "node_modules")):
166
+ if not os.path.exists(NODE_MODULES):
107
167
  npm.Popen(
108
168
  ["install", "-g", "--prefix", DATA_DIR] + INVENTORY_IMPORT_SCRIPT_DEPENDENCIES,
109
169
  stdout=DEVNULL,
@@ -127,6 +187,7 @@ AUTHOR_STRING = (
127
187
  f"Version: {VERSION} - {datetime.today().strftime('%Y/%m/%d')} - Jannik Novak @ashiven\n"
128
188
  )
129
189
 
190
+
130
191
  CAPSULE_PAGES = {
131
192
  "Katowice 2014 Sticker Capsule": "https://steamcommunity.com/market/search?q=katowice+2014+legends+challengers",
132
193
  "Cologne 2014 Sticker Capsule": "https://steamcommunity.com/market/search?q=cologne+2014+legends+challengers",
@@ -9,8 +9,22 @@ discord_webhook_url ~
9
9
 
10
10
  [Skins]
11
11
 
12
+ [Special Items]
13
+
14
+ [Agents]
15
+
16
+ [Charms]
17
+
18
+ [Patches]
19
+
20
+ [Patch Packs]
21
+
12
22
  [Stickers]
13
23
 
24
+ [Souvenirs]
25
+
26
+ [Others]
27
+
14
28
  [Cases]
15
29
  https://steamcommunity.com/market/listings/730/CS%3AGO%20Weapon%20Case ~ 0
16
30
  https://steamcommunity.com/market/listings/730/CS%3AGO%20Weapon%20Case%202 ~ 0
@@ -11,6 +11,9 @@ const itemsGameLink =
11
11
  const translationsCacheFile = path.join(__dirname, "translations.json");
12
12
  const itemsCacheFile = path.join(__dirname, "items.json");
13
13
 
14
+ const translationRegex = /"(.+?)"\s+"(.+?)"/;
15
+ const souvenirRegex = /^csgo_crate_[a-z0-9]+_promo.*/;
16
+
14
17
  class ItemNameConverter {
15
18
  constructor() {
16
19
  this.translations = {};
@@ -38,7 +41,7 @@ class ItemNameConverter {
38
41
  const res = await axios.get(translationsLink);
39
42
  const lines = res.data.split(/\n/);
40
43
  for (const line of lines) {
41
- const match = line.match(/"(.+?)"\s+"(.+?)"/);
44
+ const match = line.match(translationRegex);
42
45
  if (match) {
43
46
  this.translations[match[1].toLowerCase()] = match[2];
44
47
  }
@@ -174,7 +177,7 @@ class ItemNameConverter {
174
177
 
175
178
  getItemType(item) {
176
179
  const def = this.items[item.def_index];
177
- if (def === undefined) return "unknown";
180
+ if (def === undefined) return "Unknown";
178
181
 
179
182
  if (def.item_name !== undefined) {
180
183
  let translatedName =
@@ -183,17 +186,48 @@ class ItemNameConverter {
183
186
  translatedName.startsWith("csgo_crate_sticker_pack") ||
184
187
  translatedName.startsWith("csgo_crate_signature_pack")
185
188
  ) {
186
- return "sticker capsule";
187
- } else if (translatedName.startsWith("csgo_crate_community")) {
188
- return "case";
189
+ return "Sticker Capsules";
190
+ } else if (translatedName.startsWith("csgo_crate_patch_pack")) {
191
+ return "Patch Packs";
192
+ } else if (translatedName.match(souvenirRegex)) {
193
+ return "Souvenirs";
194
+ } else if (
195
+ translatedName.startsWith("csgo_crate_community") ||
196
+ translatedName.startsWith("csgo_crate_gamma") ||
197
+ translatedName.startsWith("csgo_crate_valve") ||
198
+ translatedName.startsWith("csgo_crate_esports") ||
199
+ translatedName.startsWith("csgo_crate_operation")
200
+ ) {
201
+ return "Cases";
189
202
  } else if (translatedName.startsWith("csgo_tool_spray")) {
190
- return "graffiti kit";
203
+ return "Graffitis";
191
204
  } else if (translatedName.startsWith("csgo_tool_sticker")) {
192
- return "sticker";
205
+ return "Stickers";
206
+ } else if (translatedName.startsWith("csgo_tool_patch")) {
207
+ return "Patches";
208
+ } else if (translatedName.startsWith("csgo_tool_keychain")) {
209
+ return "Charms";
210
+ } else if (translatedName.startsWith("csgo_customplayer")) {
211
+ return "Agents";
193
212
  }
194
213
  }
195
214
 
196
- return "other";
215
+ if (item.quality === 3) {
216
+ return "Special Items";
217
+ }
218
+
219
+ if (def.prefab !== undefined) {
220
+ let prefab = this.prefabs[def.prefab];
221
+ if (
222
+ prefab !== undefined &&
223
+ prefab.image_inventory !== undefined &&
224
+ prefab.image_inventory.startsWith("econ/weapons/base_weapons")
225
+ ) {
226
+ return "Skins";
227
+ }
228
+ }
229
+
230
+ return "Others";
197
231
  }
198
232
 
199
233
  getItemTradable(item) {
@@ -204,11 +238,13 @@ class ItemNameConverter {
204
238
  let translatedName =
205
239
  def.item_name.replace("#", "").toLowerCase() || def.item_name;
206
240
  if (
207
- translatedName.startsWith("csgo_collectible") ||
241
+ (translatedName.startsWith("csgo_collectible") &&
242
+ !translatedName.startsWith("csgo_collectible_pin")) ||
208
243
  translatedName.startsWith("csgo_tournamentpass") ||
209
244
  translatedName.startsWith("csgo_tournamentjournal") ||
210
245
  translatedName.startsWith("csgo_ticket") ||
211
- translatedName.startsWith("csgo_tool_casket_tag")
246
+ translatedName.startsWith("csgo_tool_casket_tag") ||
247
+ translatedName.startsWith("sfui_wpnhud_c4")
212
248
  ) {
213
249
  return false;
214
250
  }
@@ -221,14 +257,6 @@ class ItemNameConverter {
221
257
  return false;
222
258
  }
223
259
 
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
260
  // Base weapons with stickers/name tags
233
261
  if (
234
262
  item.paint_index === undefined &&
@@ -239,7 +267,10 @@ class ItemNameConverter {
239
267
  if (
240
268
  prefab !== undefined &&
241
269
  prefab.image_inventory !== undefined &&
242
- prefab.image_inventory.startsWith("econ/weapons/base_weapons")
270
+ prefab.image_inventory.startsWith("econ/weapons/base_weapons") &&
271
+ !prefab.image_inventory.startsWith(
272
+ "econ/weapons/base_weapons/weapon_knife",
273
+ )
243
274
  ) {
244
275
  return false;
245
276
  }
@@ -91,16 +91,12 @@ console.error = (...args) => {
91
91
 
92
92
  if (importInventory) {
93
93
  const inventoryItemCounts = await processInventory();
94
- for (const [itemName, count] of Object.entries(inventoryItemCounts)) {
95
- finalItemCounts[itemName] = (finalItemCounts[itemName] || 0) + count;
96
- }
94
+ mergeItemCounts(finalItemCounts, inventoryItemCounts);
97
95
  }
98
96
 
99
97
  if (importStorageUnits) {
100
98
  const storageUnitItemCounts = await processStorageUnits();
101
- for (const [itemName, count] of Object.entries(storageUnitItemCounts)) {
102
- finalItemCounts[itemName] = (finalItemCounts[itemName] || 0) + count;
103
- }
99
+ mergeItemCounts(finalItemCounts, storageUnitItemCounts);
104
100
  }
105
101
 
106
102
  paddedLog("Saving config...");
@@ -127,7 +123,7 @@ console.error = (...args) => {
127
123
  const convertedItems =
128
124
  nameConverter.convertInventory(prefilteredInventory);
129
125
  const filteredItems = filterItems(convertedItems);
130
- const itemCounts = countItems(filteredItems);
126
+ const itemCounts = groupAndCountItems(filteredItems);
131
127
  paddedLog(`${filteredItems.length} items found in inventory`);
132
128
  console.log(itemCounts);
133
129
  return itemCounts;
@@ -145,10 +141,8 @@ console.error = (...args) => {
145
141
  const items = await getCasketContentsAsync(cs2, unitId);
146
142
  const convertedItems = nameConverter.convertInventory(items);
147
143
  const filteredItems = filterItems(convertedItems);
148
- const itemCounts = countItems(filteredItems);
149
- for (const [itemName, count] of Object.entries(itemCounts)) {
150
- finalItemCounts[itemName] = (finalItemCounts[itemName] || 0) + count;
151
- }
144
+ const itemCounts = groupAndCountItems(filteredItems);
145
+ mergeItemCounts(finalItemCounts, itemCounts);
152
146
  paddedLog(
153
147
  `${filteredItems.length} items found in storage unit: ${unitIndex + 1}/${storageUnitIds.length}`,
154
148
  );
@@ -181,16 +175,27 @@ console.error = (...args) => {
181
175
  }
182
176
 
183
177
  function filterItems(items) {
178
+ const otherItemTypes = [
179
+ "Skins",
180
+ "Special Items",
181
+ "Agents",
182
+ "Charms",
183
+ "Patches",
184
+ "Patch Packs",
185
+ "Souvenirs",
186
+ "Others",
187
+ ];
184
188
  let filteredItems = [];
189
+
185
190
  items.forEach((item) => {
186
191
  if (!item.item_tradable) {
187
192
  return;
188
193
  }
189
194
  if (
190
- (item.item_type === "case" && importCases) ||
191
- (item.item_type === "sticker capsule" && importStickerCapsules) ||
192
- (item.item_type === "sticker" && importStickers) ||
193
- (item.item_type === "other" && importOthers)
195
+ (item.item_type === "Cases" && importCases) ||
196
+ (item.item_type === "Sticker Capsules" && importStickerCapsules) ||
197
+ (item.item_type === "Stickers" && importStickers) ||
198
+ (otherItemTypes.includes(item.item_type) && importOthers)
194
199
  ) {
195
200
  filteredItems.push(item);
196
201
  }
@@ -198,15 +203,39 @@ console.error = (...args) => {
198
203
  return filteredItems;
199
204
  }
200
205
 
201
- function countItems(items) {
202
- let itemCounts = {};
203
- items.forEach((item) => {
204
- if (itemCounts[item.item_name]) {
205
- itemCounts[item.item_name]++;
206
- } else {
207
- itemCounts[item.item_name] = 1;
206
+ function groupAndCountItems(items) {
207
+ let groupedItems = items.reduce((acc, item) => {
208
+ const { item_name, item_type } = item;
209
+
210
+ if (!acc[item_type]) {
211
+ acc[item_type] = {};
208
212
  }
209
- });
210
- return itemCounts;
213
+
214
+ if (!acc[item_type][item_name]) {
215
+ acc[item_type][item_name] = 0;
216
+ }
217
+
218
+ acc[item_type][item_name]++;
219
+ return acc;
220
+ }, {});
221
+
222
+ return groupedItems;
223
+ }
224
+
225
+ function mergeItemCounts(finalItemCounts, currentItemCounts) {
226
+ for (const item_type in currentItemCounts) {
227
+ if (!finalItemCounts[item_type]) {
228
+ finalItemCounts[item_type] = {};
229
+ }
230
+
231
+ for (const item_name in currentItemCounts[item_type]) {
232
+ if (!finalItemCounts[item_type][item_name]) {
233
+ finalItemCounts[item_type][item_name] = 0;
234
+ }
235
+
236
+ finalItemCounts[item_type][item_name] +=
237
+ currentItemCounts[item_type][item_name];
238
+ }
239
+ }
211
240
  }
212
241
  })();
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cs2tracker
3
- Version: 2.1.16
3
+ Version: 2.1.18
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
@@ -54,6 +54,7 @@ Dynamic: license-file
54
54
  - [Usage](#usage)
55
55
  - [Configuration](#configuration)
56
56
  - [Advanced Features](#advanced-features)
57
+ - [FAQ](#faq)
57
58
  - [Contributing](#contributing)
58
59
  - [License](#license)
59
60
 
@@ -74,8 +75,9 @@ Dynamic: license-file
74
75
  #### Method 1: Executable
75
76
 
76
77
  Simply download the program and run it:
77
- - [Windows](https://github.com/ashiven/cs2tracker/releases/latest/download/cs2tracker-windows.zip)
78
- - [Linux](https://github.com/ashiven/cs2tracker/releases/latest/download/cs2tracker-linux.zip)
78
+
79
+ - [Windows](https://github.com/ashiven/cs2tracker/releases/latest/download/cs2tracker-windows.zip)
80
+ - [Linux](https://github.com/ashiven/cs2tracker/releases/latest/download/cs2tracker-linux.zip)
79
81
 
80
82
  #### Method 2: Install via Pip
81
83
 
@@ -90,6 +92,7 @@ Simply download the program and run it:
90
92
  ```bash
91
93
  cs2tracker
92
94
  ```
95
+
93
96
  ### Additional Setup
94
97
 
95
98
  - Register for the [Crawlbase Smart Proxy API](https://crawlbase.com/) and retrieve your API key. (Optional)
@@ -117,18 +120,36 @@ This will open the config editor where you can change any setting by double clic
117
120
 
118
121
  - Enable **Daily Background Calculations** to automatically run a daily calculation of your investment in the background.
119
122
  - 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`.
123
+ - 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
124
  - Enable **Proxy Requests** to prevent your requests from being rate limited by the steamcommunity server.
122
125
  - 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
126
 
127
+ ## FAQ
128
+
129
+ - **Q: Is it safe to login with my Steam account?**
130
+ - **A:** Yes, the program uses the [SteamUser](https://github.com/DoctorMcKay/node-steam-user?tab=readme-ov-file#methods-) and [Globaloffensive](https://github.com/DoctorMcKay/node-globaloffensive) libraries to sign in and import your Storage Units (the same method is used by [casemove](https://github.com/nombersDev/casemove)) and all of the login-related code is transparently available in [this file](cs2tracker/data/get_inventory.js).
131
+
132
+
133
+ - **Q: Do I have to login with my Steam account?**
134
+ - **A:** No, you can also manually specify the number of items you own in the config editor.
135
+
136
+
137
+ - **Q: Can I get VAC-banned for using this program?**
138
+ - **A:** No, this program does not interact with the game in any way and only reads your Storage Units.
139
+
140
+
141
+ - **Q: Why does Windows Defender flag this program as potentially harmful?**
142
+ - **A:** This is because the program is not signed with a [Code Signing Certificate](https://www.globalsign.com/en/code-signing-certificate/what-is-code-signing-certificate), which Windows uses to verify the identity of publishers. These certificates are very expensive and not something I am willing to invest in for a free and open source project like this.
143
+
124
144
  ## Contributing
125
145
 
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.
146
+ Please feel free to submit a [pull request](https://github.com/ashiven/cs2tracker/pulls) or open an [issue](https://github.com/ashiven/cs2tracker/issues).
127
147
 
128
148
  1. Fork the repository
129
- 2. Create a new branch
149
+ 2. Create a new branch: `git checkout -b feature-name`.
130
150
  3. Make your changes
131
- 4. Submit a PR
151
+ 4. Push your branch: `git push origin feature-name`.
152
+ 5. Submit a PR
132
153
 
133
154
  ## License
134
155
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes