cs2tracker 2.1.14__py3-none-any.whl → 2.1.16__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.

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