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

@@ -0,0 +1,191 @@
1
+ const SteamUser = require("steam-user");
2
+ const CS2 = require("globaloffensive");
3
+ const { argv } = require("process");
4
+ const fs = require("fs");
5
+
6
+ const ItemNameConverter = require("./convert_inventory.js");
7
+
8
+ process.stdin.setEncoding("utf-8");
9
+ process.stdout.setEncoding("utf-8");
10
+ process.stderr.setEncoding("utf-8");
11
+
12
+ const args = argv.slice(2);
13
+ const processedInventoryPath = args[0];
14
+ const importInventory = args[1] === "True" ? true : false;
15
+ const importStorageUnits = args[2] === "True" ? true : false;
16
+ const importCases = args[3] === "True" ? true : false;
17
+ const importStickerCapsules = args[4] === "True" ? true : false;
18
+ const importStickers = args[5] === "True" ? true : false;
19
+ const importOthers = args[6] === "True" ? true : false;
20
+ const userName = args[7];
21
+ const password = args[8];
22
+ const twoFactorCode = args[9];
23
+
24
+ const paddedLog = (...args) => {
25
+ console.log(" [+] ", ...args);
26
+ };
27
+
28
+ const originalConsoleError = console.error;
29
+ console.error = (...args) => {
30
+ originalConsoleError(" [!] " + args.join(" "));
31
+ };
32
+
33
+ (async () => {
34
+ let user = new SteamUser();
35
+
36
+ paddedLog("Logging into Steam...");
37
+
38
+ user.logOn({
39
+ accountName: userName,
40
+ password: password,
41
+ twoFactorCode: twoFactorCode,
42
+ });
43
+
44
+ user.on("error", (err) => {
45
+ console.error("Steam Error: " + err);
46
+ user.logOff();
47
+ process.exit(1);
48
+ });
49
+
50
+ user.on("loggedOn", (_details, _parental) => {
51
+ paddedLog("Logged into Steam.");
52
+ user.gamesPlayed([730]);
53
+ });
54
+
55
+ let cs2 = new CS2(user);
56
+
57
+ paddedLog("Connecting to CS2 Game Coordinator...");
58
+
59
+ cs2.on("error", (err) => {
60
+ console.error("CS2 Error: " + err);
61
+ user.logOff();
62
+ process.exit(1);
63
+ });
64
+
65
+ let nameConverter = new ItemNameConverter();
66
+ await nameConverter.initialize();
67
+
68
+ cs2.on("connectedToGC", async () => {
69
+ paddedLog("Connected to CS2 Game Coordinator.");
70
+ let finalItemCounts = {};
71
+
72
+ if (importInventory) {
73
+ const inventoryItemCounts = await processInventory();
74
+ for (const [itemName, count] of Object.entries(inventoryItemCounts)) {
75
+ finalItemCounts[itemName] = (finalItemCounts[itemName] || 0) + count;
76
+ }
77
+ }
78
+
79
+ if (importStorageUnits) {
80
+ const storageUnitItemCounts = await processStorageUnits();
81
+ for (const [itemName, count] of Object.entries(storageUnitItemCounts)) {
82
+ finalItemCounts[itemName] = (finalItemCounts[itemName] || 0) + count;
83
+ }
84
+ }
85
+
86
+ paddedLog("Saving config...");
87
+ fs.writeFileSync(
88
+ processedInventoryPath,
89
+ JSON.stringify(finalItemCounts, null, 2),
90
+ );
91
+
92
+ paddedLog("Processing complete.");
93
+ paddedLog("This window will automatically close in 10 seconds.");
94
+ await new Promise((resolve) => setTimeout(resolve, 10000));
95
+ user.logOff();
96
+ process.exit(0);
97
+ });
98
+
99
+ // TODO: The inventory may contain items that are not marketable or tradable,
100
+ // so we have to make sure to process these items correctly in the main app.
101
+ async function processInventory() {
102
+ try {
103
+ // filter out items that have the casket_id property set from the inventory
104
+ // because these are items that should be contained in storage units
105
+ const prefilteredInventory = cs2.inventory.filter((item) => {
106
+ return !item.casket_id;
107
+ });
108
+
109
+ const convertedItems =
110
+ nameConverter.convertInventory(prefilteredInventory);
111
+ const filteredItems = filterItems(convertedItems);
112
+ const itemCounts = countItems(filteredItems);
113
+ paddedLog(`${filteredItems.length} items found in inventory`);
114
+ console.log(itemCounts);
115
+ return itemCounts;
116
+ } catch (err) {
117
+ console.error("An error occurred while processing the inventory:", err);
118
+ return {};
119
+ }
120
+ }
121
+
122
+ async function processStorageUnits() {
123
+ let finalItemCounts = {};
124
+ try {
125
+ const storageUnitIds = getStorageUnitIds();
126
+ for (const [unitIndex, unitId] of storageUnitIds.entries()) {
127
+ const items = await getCasketContentsAsync(cs2, unitId);
128
+ const convertedItems = nameConverter.convertInventory(items);
129
+ const filteredItems = filterItems(convertedItems);
130
+ const itemCounts = countItems(filteredItems);
131
+ for (const [itemName, count] of Object.entries(itemCounts)) {
132
+ finalItemCounts[itemName] = (finalItemCounts[itemName] || 0) + count;
133
+ }
134
+ paddedLog(
135
+ `${filteredItems.length} items found in storage unit: ${unitIndex + 1}/${storageUnitIds.length}`,
136
+ );
137
+ console.log(itemCounts);
138
+ }
139
+ return finalItemCounts;
140
+ } catch (err) {
141
+ console.error("An error occurred while processing storage units:", err);
142
+ return {};
143
+ }
144
+ }
145
+
146
+ function getStorageUnitIds() {
147
+ let storageUnitIds = [];
148
+ for (let item of cs2.inventory) {
149
+ if (item.casket_contained_item_count > 0) {
150
+ storageUnitIds.push(item.id);
151
+ }
152
+ }
153
+ return storageUnitIds;
154
+ }
155
+
156
+ function getCasketContentsAsync(cs2, unitId) {
157
+ return new Promise((resolve, reject) => {
158
+ cs2.getCasketContents(unitId, (err, items) => {
159
+ if (err) return reject(err);
160
+ resolve(items);
161
+ });
162
+ });
163
+ }
164
+
165
+ function filterItems(items) {
166
+ let filteredItems = [];
167
+ items.forEach((item) => {
168
+ if (
169
+ (item.item_type === "case" && importCases) ||
170
+ (item.item_type === "sticker capsule" && importStickerCapsules) ||
171
+ (item.item_type === "sticker" && importStickers) ||
172
+ (item.item_type === "other" && importOthers)
173
+ ) {
174
+ filteredItems.push(item);
175
+ }
176
+ });
177
+ return filteredItems;
178
+ }
179
+
180
+ function countItems(items) {
181
+ let itemCounts = {};
182
+ items.forEach((item) => {
183
+ if (itemCounts[item.item_name]) {
184
+ itemCounts[item.item_name]++;
185
+ } else {
186
+ itemCounts[item.item_name] = 1;
187
+ }
188
+ });
189
+ return itemCounts;
190
+ }
191
+ })();
cs2tracker/main.py CHANGED
@@ -5,7 +5,7 @@ import urllib3
5
5
  from cs2tracker.app import Application
6
6
  from cs2tracker.constants import AUTHOR_STRING, BANNER, OS, OSType
7
7
  from cs2tracker.scraper import Scraper
8
- from cs2tracker.util import PaddedConsole
8
+ from cs2tracker.util import get_console
9
9
 
10
10
 
11
11
  def main():
@@ -23,7 +23,7 @@ def main():
23
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
- console = PaddedConsole()
26
+ console = get_console()
27
27
  console.print(f"[bold yellow]{BANNER}\n{AUTHOR_STRING}\n")
28
28
 
29
29
  if "--only-scrape" in sys.argv:
@@ -9,7 +9,7 @@ from cs2tracker.constants import (
9
9
  RUNNING_IN_EXE,
10
10
  OSType,
11
11
  )
12
- from cs2tracker.util import PaddedConsole
12
+ from cs2tracker.util import get_console
13
13
 
14
14
  WIN_BACKGROUND_TASK_NAME = "CS2Tracker Daily Calculation"
15
15
  WIN_BACKGROUND_TASK_SCHEDULE = "DAILY"
@@ -18,7 +18,7 @@ WIN_BACKGROUND_TASK_CMD = (
18
18
  f"powershell -WindowStyle Hidden -Command \"Start-Process '{BATCH_FILE}' -WindowStyle Hidden\""
19
19
  )
20
20
 
21
- console = PaddedConsole()
21
+ console = get_console()
22
22
 
23
23
 
24
24
  class BackgroundTask:
@@ -85,14 +85,14 @@ class BackgroundTask:
85
85
  if return_code == 0:
86
86
  console.print("[bold green][+] Background task enabled.")
87
87
  else:
88
- console.print("[bold red][!] Failed to enable background task.")
88
+ console.error("Failed to enable background task.")
89
89
  else:
90
90
  cmd = ["schtasks", "/delete", "/tn", WIN_BACKGROUND_TASK_NAME, "/f"]
91
91
  return_code = call(cmd, stdout=DEVNULL, stderr=DEVNULL)
92
92
  if return_code == 0:
93
93
  console.print("[bold green][-] Background task disabled.")
94
94
  else:
95
- console.print("[bold red][!] Failed to disable background task.")
95
+ console.error("Failed to disable background task.")
96
96
 
97
97
  @classmethod
98
98
  def toggle(cls, enabled: bool):
@@ -1,13 +1,13 @@
1
1
  import requests
2
2
  from requests.exceptions import RequestException
3
3
 
4
- from cs2tracker.util import PaddedConsole, PriceLogs
4
+ from cs2tracker.util import PriceLogs, get_console
5
5
 
6
6
  DC_WEBHOOK_USERNAME = "CS2Tracker"
7
7
  DC_WEBHOOK_AVATAR_URL = "https://img.icons8.com/?size=100&id=uWQJp2tLXUH6&format=png&color=000000"
8
8
  DC_RECENT_HISTORY_LIMIT = 5
9
9
 
10
- console = PaddedConsole()
10
+ console = get_console()
11
11
 
12
12
 
13
13
  class DiscordNotifier:
@@ -81,6 +81,6 @@ class DiscordNotifier:
81
81
  response.raise_for_status()
82
82
  console.print("[bold steel_blue3][+] Discord notification sent.\n")
83
83
  except RequestException as error:
84
- console.print(f"[bold red][!] Failed to send Discord notification: {error}\n")
84
+ console.error(f"Failed to send Discord notification: {error}\n")
85
85
  except Exception as error:
86
- console.print(f"[bold red][!] An unexpected error occurred: {error}\n")
86
+ console.error(f"An unexpected error occurred: {error}\n")
@@ -0,0 +1,192 @@
1
+ from abc import ABC, abstractmethod
2
+ from enum import Enum
3
+ from urllib.parse import unquote
4
+
5
+ from bs4 import BeautifulSoup
6
+ from bs4.element import Tag
7
+
8
+ from cs2tracker.constants import CAPSULE_PAGES
9
+ from cs2tracker.util import get_console
10
+ from cs2tracker.util.validated_config import get_config
11
+
12
+ config = get_config()
13
+ console = get_console()
14
+
15
+
16
+ class PriceSource(Enum):
17
+ STEAM = "steam"
18
+ BUFF163 = "buff163"
19
+ SKINPORT = "skinport"
20
+
21
+
22
+ class Parser(ABC):
23
+ @classmethod
24
+ @abstractmethod
25
+ def get_item_page_url(cls, item_href, source=PriceSource.STEAM) -> str:
26
+ """
27
+ Convert an href of a Steam Community Market item to a Parser-specific market
28
+ page URL.
29
+
30
+ :param item_href: The href of the item listing, typically ending with the item's
31
+ name.
32
+ :return: A URL string for the Parser market page of the item.
33
+ """
34
+
35
+ @classmethod
36
+ @abstractmethod
37
+ def parse_item_price(cls, item_page, item_href, source=PriceSource.STEAM) -> float:
38
+ """
39
+ Parse the price of an item from the given Parser market page and steamcommunity
40
+ item href.
41
+
42
+ :param item_page: The HTTP response object containing the item page content.
43
+ :param item_href: The href of the item listing to find the price for.
44
+ :return: The price of the item as a float.
45
+ :raises ValueError: If the item listing or price span cannot be found.
46
+ """
47
+
48
+
49
+ class SteamParser(Parser):
50
+ STEAM_MARKET_SEARCH_PAGE_BASE_URL = "https://steamcommunity.com/market/search?q={}"
51
+ PRICE_INFO = "Owned: {:<10} {} price: ${:<10} Total: ${:<10}"
52
+ NEEDS_TIMEOUT = True
53
+ SOURCES = [PriceSource.STEAM]
54
+
55
+ @classmethod
56
+ def get_item_page_url(cls, item_href, source=PriceSource.STEAM):
57
+ _ = source
58
+
59
+ # For higher efficiency we want to reuse the same page for sticker capsules (scraper uses caching)
60
+ # Therefore, if the provided item is a sticker capsule we return a search page defined in CAPSULE_PAGES
61
+ # where all of the sticker capsules of one section are listed
62
+ for section in config.sections():
63
+ if section in ("Custom Items", "Cases", "User Settings", "App Settings"):
64
+ continue
65
+ if any(item_href == option for option in config.options(section)):
66
+ return CAPSULE_PAGES[section]
67
+
68
+ url_encoded_name = item_href.split("/")[-1]
69
+ page_url = cls.STEAM_MARKET_SEARCH_PAGE_BASE_URL.format(url_encoded_name)
70
+
71
+ return page_url
72
+
73
+ @classmethod
74
+ def parse_item_price(cls, item_page, item_href, source=PriceSource.STEAM):
75
+ _ = source
76
+
77
+ item_soup = BeautifulSoup(item_page.content, "html.parser")
78
+ item_listing = item_soup.find("a", attrs={"href": f"{item_href}"})
79
+ if not isinstance(item_listing, Tag):
80
+ raise ValueError(f"Steam: Failed to find item listing for: {item_href}")
81
+
82
+ item_price_span = item_listing.find("span", attrs={"class": "normal_price"})
83
+ if not isinstance(item_price_span, Tag):
84
+ raise ValueError(f"Steam: Failed to find price span in item listing for: {item_href}")
85
+
86
+ price_str = item_price_span.text.split()[2]
87
+ price = float(price_str.replace("$", ""))
88
+
89
+ return price
90
+
91
+
92
+ class SkinLedgerParser(Parser):
93
+ SKINLEDGER_PRICE_LIST = ""
94
+ PRICE_INFO = "Owned: {:<10} {} price: ${:<10} Total: ${:<10}"
95
+ NEEDS_TIMEOUT = False
96
+ SOURCES = [PriceSource.STEAM, PriceSource.BUFF163, PriceSource.SKINPORT]
97
+
98
+ @classmethod
99
+ def get_item_page_url(cls, item_href, source=PriceSource.STEAM) -> str:
100
+ _ = source
101
+ return super().get_item_page_url(item_href)
102
+
103
+ @classmethod
104
+ def parse_item_price(cls, item_page, item_href, source=PriceSource.STEAM) -> float:
105
+ _, _ = item_href, source
106
+ return super().parse_item_price(item_page, item_href)
107
+
108
+
109
+ class ClashParser(Parser):
110
+ CLASH_ITEM_API_BASE_URL = "https://inventory.clash.gg/api/GetItemPrice?id={}"
111
+ PRICE_INFO = "Owned: {:<10} {} price: ${:<10} Total: ${:<10}"
112
+ NEEDS_TIMEOUT = True
113
+ SOURCES = [PriceSource.STEAM]
114
+
115
+ @classmethod
116
+ def get_item_page_url(cls, item_href, source=PriceSource.STEAM):
117
+ _ = source
118
+
119
+ url_encoded_name = item_href.split("/")[-1]
120
+ page_url = cls.CLASH_ITEM_API_BASE_URL.format(url_encoded_name)
121
+
122
+ return page_url
123
+
124
+ @classmethod
125
+ def parse_item_price(cls, item_page, item_href, source=PriceSource.STEAM):
126
+ _, _ = item_href, source
127
+
128
+ data = item_page.json()
129
+ if data.get("success", "false") == "false":
130
+ raise ValueError(f"Clash: Response failed for: {item_href}")
131
+
132
+ price = data.get("average_price", None)
133
+ if not price:
134
+ raise ValueError(f"Clash: Failed to find item price for: {item_href}")
135
+
136
+ price = float(price)
137
+
138
+ return price
139
+
140
+
141
+ class CSGOTrader(Parser):
142
+ CSGOTRADER_PRICE_LIST = "https://prices.csgotrader.app/latest/{}.json"
143
+ PRICE_INFO = "Owned: {:<10} {:<10}: ${:<10} Total: ${:<10}"
144
+ NEEDS_TIMEOUT = False
145
+ SOURCES = [PriceSource.STEAM, PriceSource.BUFF163, PriceSource.SKINPORT]
146
+
147
+ @classmethod
148
+ def get_item_page_url(cls, item_href, source=PriceSource.STEAM):
149
+ _ = item_href
150
+
151
+ page_url = cls.CSGOTRADER_PRICE_LIST.format(source.value)
152
+
153
+ return page_url
154
+
155
+ @classmethod
156
+ def parse_item_price(cls, item_page, item_href, source=PriceSource.STEAM):
157
+ _ = source
158
+
159
+ price_list = item_page.json()
160
+
161
+ url_decoded_name = unquote(item_href.split("/")[-1])
162
+ if source in (PriceSource.BUFF163, PriceSource.SKINPORT):
163
+ url_decoded_name = url_decoded_name.replace("Holo-Foil", "Holo/Foil")
164
+
165
+ price_info = price_list.get(url_decoded_name, None)
166
+ if not price_info:
167
+ raise ValueError(f"CSGOTrader: Could not find item price info: {url_decoded_name}")
168
+
169
+ if source == PriceSource.STEAM:
170
+ price = price_info.get("last_24h")
171
+ if not price:
172
+ price = price_info.get("last_7d")
173
+ if not price:
174
+ raise ValueError(
175
+ f"CSGOTrader: Could not find steam price of the past 7 days: {url_decoded_name}"
176
+ )
177
+ elif source == PriceSource.BUFF163:
178
+ price = price_info.get("starting_at")
179
+ if not price:
180
+ raise ValueError(f"CSGOTrader: Could not find buff163 listing: {url_decoded_name}")
181
+ price = price.get("price")
182
+ if not price:
183
+ raise ValueError(
184
+ f"CSGOTrader: Could not find recent buff163 price: {url_decoded_name}"
185
+ )
186
+ else:
187
+ price = price_info.get("starting_at")
188
+ if not price:
189
+ raise ValueError(f"CSGOTrader: Could not find skinport listing: {url_decoded_name}")
190
+
191
+ price = float(price)
192
+ return price