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