cs2tracker 2.1.8__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.8 → cs2tracker-2.1.9}/.gitignore +1 -0
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/PKG-INFO +1 -1
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/cs2tracker/_version.py +2 -2
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/cs2tracker/application.py +19 -2
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/cs2tracker/constants.py +32 -54
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/cs2tracker/scraper.py +146 -76
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/cs2tracker.egg-info/PKG-INFO +1 -1
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/.flake8 +0 -0
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/.isort.cfg +0 -0
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/.pre-commit-config.yaml +0 -0
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/.pylintrc +0 -0
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/LICENSE.md +0 -0
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/MANIFEST.in +0 -0
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/README.md +0 -0
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/assets/icon.png +0 -0
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/cs2tracker/__init__.py +0 -0
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/cs2tracker/__main__.py +0 -0
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/cs2tracker/data/config.ini +0 -0
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/cs2tracker/data/output.csv +0 -0
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/cs2tracker/main.py +0 -0
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/cs2tracker/padded_console.py +0 -0
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/cs2tracker.egg-info/SOURCES.txt +0 -0
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/cs2tracker.egg-info/dependency_links.txt +0 -0
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/cs2tracker.egg-info/entry_points.txt +0 -0
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/cs2tracker.egg-info/requires.txt +0 -0
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/cs2tracker.egg-info/top_level.txt +0 -0
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/pyproject.toml +0 -0
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/requirements.txt +0 -0
- {cs2tracker-2.1.8 → cs2tracker-2.1.9}/setup.cfg +0 -0
|
@@ -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
|
|
|
@@ -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",
|
|
@@ -4,7 +4,6 @@ import time
|
|
|
4
4
|
from configparser import ConfigParser
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
from subprocess import DEVNULL, call
|
|
7
|
-
from urllib.parse import unquote
|
|
8
7
|
|
|
9
8
|
from bs4 import BeautifulSoup
|
|
10
9
|
from bs4.element import Tag
|
|
@@ -19,7 +18,6 @@ from cs2tracker.constants import (
|
|
|
19
18
|
BATCH_FILE,
|
|
20
19
|
CAPSULE_INFO,
|
|
21
20
|
CASE_HREFS,
|
|
22
|
-
CASE_PAGES,
|
|
23
21
|
CONFIG_FILE,
|
|
24
22
|
OS,
|
|
25
23
|
OUTPUT_FILE,
|
|
@@ -59,10 +57,75 @@ class Scraper:
|
|
|
59
57
|
self.usd_total = 0
|
|
60
58
|
self.eur_total = 0
|
|
61
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
|
+
|
|
62
114
|
def parse_config(self):
|
|
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
|
+
"""
|
|
64
121
|
self.config = ConfigParser(interpolation=None)
|
|
65
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
|
|
66
129
|
|
|
67
130
|
def _start_session(self):
|
|
68
131
|
"""Start a requests session with custom headers and retry logic."""
|
|
@@ -80,29 +143,15 @@ class Scraper:
|
|
|
80
143
|
"""Scrape prices for capsules and cases, calculate totals in USD and EUR, and
|
|
81
144
|
print/save the results.
|
|
82
145
|
"""
|
|
83
|
-
|
|
84
|
-
try:
|
|
85
|
-
capsule_usd_total = self.scrape_capsule_section_prices()
|
|
86
|
-
except (RequestException, AttributeError, RetryError, ValueError):
|
|
146
|
+
if not self.valid_config:
|
|
87
147
|
self.console.print(
|
|
88
|
-
"[bold red][!]
|
|
148
|
+
"[bold red][!] Invalid configuration. Please fix the config file before running."
|
|
89
149
|
)
|
|
150
|
+
return
|
|
90
151
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
except (RequestException, AttributeError, RetryError, ValueError):
|
|
95
|
-
self.console.print(
|
|
96
|
-
"[bold red][!] Failed to scrape case prices. (Consider using proxies to prevent rate limiting)\n"
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
custom_item_usd_total = 0
|
|
100
|
-
try:
|
|
101
|
-
custom_item_usd_total = self._scrape_custom_item_prices()
|
|
102
|
-
except (RequestException, AttributeError, RetryError, ValueError):
|
|
103
|
-
self.console.print(
|
|
104
|
-
"[bold red][!] Failed to scrape custom item prices. (Consider using proxies to prevent rate limiting)\n"
|
|
105
|
-
)
|
|
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()
|
|
106
155
|
|
|
107
156
|
self.usd_total += capsule_usd_total
|
|
108
157
|
self.usd_total += case_usd_total
|
|
@@ -283,6 +332,7 @@ class Scraper:
|
|
|
283
332
|
use_proxy = self.config.getboolean("App Settings", "use_proxy", fallback=False)
|
|
284
333
|
api_key = self.config.get("User Settings", "api_key", fallback=None)
|
|
285
334
|
api_key = None if api_key in ("None", "") else api_key
|
|
335
|
+
|
|
286
336
|
if use_proxy and api_key:
|
|
287
337
|
page = self.session.get(
|
|
288
338
|
url=url,
|
|
@@ -296,8 +346,9 @@ class Scraper:
|
|
|
296
346
|
page = self.session.get(url)
|
|
297
347
|
|
|
298
348
|
if not page.ok or not page.content:
|
|
299
|
-
|
|
300
|
-
|
|
349
|
+
self.console.print(
|
|
350
|
+
f"[bold red][!] Failed to load page ({page.status_code}). Retrying...\n"
|
|
351
|
+
)
|
|
301
352
|
raise RequestException(f"Failed to load page: {url}")
|
|
302
353
|
|
|
303
354
|
return page
|
|
@@ -343,23 +394,32 @@ class Scraper:
|
|
|
343
394
|
self.console.print(f"[bold magenta]{capsule_title}\n")
|
|
344
395
|
|
|
345
396
|
capsule_usd_total = 0
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
+
)
|
|
359
419
|
|
|
360
420
|
return capsule_usd_total
|
|
361
421
|
|
|
362
|
-
def
|
|
422
|
+
def _scrape_capsule_section_prices(self):
|
|
363
423
|
"""Scrape prices for all capsule sections defined in the configuration."""
|
|
364
424
|
capsule_usd_total = 0
|
|
365
425
|
for capsule_section, capsule_info in CAPSULE_INFO.items():
|
|
@@ -369,6 +429,19 @@ class Scraper:
|
|
|
369
429
|
|
|
370
430
|
return capsule_usd_total
|
|
371
431
|
|
|
432
|
+
def _market_page_from_href(self, item_href):
|
|
433
|
+
"""
|
|
434
|
+
Convert an href of a Steam Community Market item to a market page URL.
|
|
435
|
+
|
|
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.
|
|
439
|
+
"""
|
|
440
|
+
url_encoded_name = item_href.split("/")[-1]
|
|
441
|
+
page_url = f"https://steamcommunity.com/market/search?q={url_encoded_name}"
|
|
442
|
+
|
|
443
|
+
return page_url
|
|
444
|
+
|
|
372
445
|
def _scrape_case_prices(self):
|
|
373
446
|
"""
|
|
374
447
|
Scrape prices for all cases defined in the configuration.
|
|
@@ -385,34 +458,28 @@ class Scraper:
|
|
|
385
458
|
case_title = case_name.center(MAX_LINE_LEN, SEPARATOR)
|
|
386
459
|
self.console.print(f"[bold magenta]{case_title}\n")
|
|
387
460
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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)
|
|
391
466
|
|
|
392
|
-
|
|
393
|
-
|
|
467
|
+
self.console.print(PRICE_INFO.format(owned, price_usd, price_usd_owned))
|
|
468
|
+
case_usd_total += price_usd_owned
|
|
394
469
|
|
|
395
|
-
|
|
396
|
-
|
|
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
|
+
)
|
|
397
480
|
|
|
398
481
|
return case_usd_total
|
|
399
482
|
|
|
400
|
-
def _market_page_from_href(self, item_href):
|
|
401
|
-
"""
|
|
402
|
-
Convert an href of a Steam Community Market item to a market page URL. This is
|
|
403
|
-
done by decoding the URL-encoded item name and formatting it into a search URL.
|
|
404
|
-
|
|
405
|
-
:param item_href: The href of the item listing, typically ending with the item's
|
|
406
|
-
name.
|
|
407
|
-
:return: A URL string for the Steam Community Market page of the item.
|
|
408
|
-
"""
|
|
409
|
-
url_encoded_name = item_href.split("/")[-1]
|
|
410
|
-
decoded_name = unquote(url_encoded_name)
|
|
411
|
-
decoded_name_query = decoded_name.lower().replace(" ", "+")
|
|
412
|
-
page_url = f"https://steamcommunity.com/market/search?q={decoded_name_query}"
|
|
413
|
-
|
|
414
|
-
return page_url
|
|
415
|
-
|
|
416
483
|
def _scrape_custom_item_prices(self):
|
|
417
484
|
"""
|
|
418
485
|
Scrape prices for custom items defined in the configuration.
|
|
@@ -422,12 +489,6 @@ class Scraper:
|
|
|
422
489
|
"""
|
|
423
490
|
custom_item_usd_total = 0
|
|
424
491
|
for config_custom_item_name, owned_and_href in self.config.items("Custom Items"):
|
|
425
|
-
if " " not in owned_and_href:
|
|
426
|
-
self.console.print(
|
|
427
|
-
"[bold red][!] Invalid custom item format (<item_name> = <owned_count> <item_url>)\n"
|
|
428
|
-
)
|
|
429
|
-
continue
|
|
430
|
-
|
|
431
492
|
owned, custom_item_href = owned_and_href.split(" ", 1)
|
|
432
493
|
if int(owned) == 0:
|
|
433
494
|
continue
|
|
@@ -436,16 +497,25 @@ class Scraper:
|
|
|
436
497
|
custom_item_title = custom_item_name.center(MAX_LINE_LEN, SEPARATOR)
|
|
437
498
|
self.console.print(f"[bold magenta]{custom_item_title}\n")
|
|
438
499
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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)
|
|
443
505
|
|
|
444
|
-
|
|
445
|
-
|
|
506
|
+
self.console.print(PRICE_INFO.format(owned, price_usd, price_usd_owned))
|
|
507
|
+
custom_item_usd_total += price_usd_owned
|
|
446
508
|
|
|
447
|
-
|
|
448
|
-
|
|
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
|
+
)
|
|
449
519
|
|
|
450
520
|
return custom_item_usd_total
|
|
451
521
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|