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.
- cs2tracker/_version.py +2 -2
- cs2tracker/app/application.py +84 -88
- cs2tracker/app/editor_frame.py +451 -143
- cs2tracker/app/scraper_frame.py +43 -9
- cs2tracker/constants.py +66 -451
- cs2tracker/data/config.ini +153 -153
- cs2tracker/data/convert_inventory.js +187 -0
- cs2tracker/data/get_inventory.js +191 -0
- cs2tracker/main.py +2 -2
- cs2tracker/scraper/background_task.py +4 -4
- cs2tracker/scraper/discord_notifier.py +4 -4
- cs2tracker/scraper/parsers.py +192 -0
- cs2tracker/scraper/scraper.py +146 -224
- cs2tracker/util/__init__.py +2 -2
- cs2tracker/util/padded_console.py +32 -0
- cs2tracker/util/validated_config.py +117 -16
- cs2tracker-2.1.14.dist-info/METADATA +148 -0
- cs2tracker-2.1.14.dist-info/RECORD +28 -0
- cs2tracker-2.1.14.dist-info/licenses/LICENSE +402 -0
- cs2tracker-2.1.12.dist-info/METADATA +0 -82
- cs2tracker-2.1.12.dist-info/RECORD +0 -25
- cs2tracker-2.1.12.dist-info/licenses/LICENSE.md +0 -21
- {cs2tracker-2.1.12.dist-info → cs2tracker-2.1.14.dist-info}/WHEEL +0 -0
- {cs2tracker-2.1.12.dist-info → cs2tracker-2.1.14.dist-info}/entry_points.txt +0 -0
- {cs2tracker-2.1.12.dist-info → cs2tracker-2.1.14.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
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 =
|
|
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
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|
|
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 =
|
|
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.
|
|
84
|
+
console.error(f"Failed to send Discord notification: {error}\n")
|
|
85
85
|
except Exception as error:
|
|
86
|
-
console.
|
|
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
|