cs2tracker 2.1.1__py3-none-any.whl → 2.1.2__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.
- cs2tracker/__main__.py +6 -1
- cs2tracker/_version.py +2 -2
- cs2tracker/application.py +31 -52
- cs2tracker/constants.py +167 -168
- cs2tracker/data/config.ini +9 -9
- cs2tracker/main.py +10 -3
- cs2tracker/scraper.py +245 -278
- {cs2tracker-2.1.1.dist-info → cs2tracker-2.1.2.dist-info}/METADATA +10 -4
- cs2tracker-2.1.2.dist-info/RECORD +14 -0
- cs2tracker-2.1.1.dist-info/RECORD +0 -14
- {cs2tracker-2.1.1.dist-info → cs2tracker-2.1.2.dist-info}/WHEEL +0 -0
- {cs2tracker-2.1.1.dist-info → cs2tracker-2.1.2.dist-info}/entry_points.txt +0 -0
- {cs2tracker-2.1.1.dist-info → cs2tracker-2.1.2.dist-info}/licenses/LICENSE.md +0 -0
- {cs2tracker-2.1.1.dist-info → cs2tracker-2.1.2.dist-info}/top_level.txt +0 -0
cs2tracker/__main__.py
CHANGED
cs2tracker/_version.py
CHANGED
cs2tracker/application.py
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import csv
|
|
2
|
-
import datetime
|
|
3
1
|
import os
|
|
4
2
|
import subprocess
|
|
5
3
|
import tkinter as tk
|
|
4
|
+
from typing import cast
|
|
6
5
|
|
|
7
6
|
import matplotlib.pyplot as plt
|
|
7
|
+
from matplotlib.axes import Axes
|
|
8
8
|
from matplotlib.dates import DateFormatter
|
|
9
9
|
|
|
10
|
-
from .constants import CONFIG_FILE, OUTPUT_FILE, TEXT_EDITOR
|
|
11
|
-
from .scraper import Scraper
|
|
10
|
+
from cs2tracker.constants import CONFIG_FILE, OUTPUT_FILE, TEXT_EDITOR
|
|
11
|
+
from cs2tracker.scraper import Scraper
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class Application:
|
|
@@ -16,6 +16,16 @@ class Application:
|
|
|
16
16
|
self.scraper = Scraper()
|
|
17
17
|
|
|
18
18
|
def run(self):
|
|
19
|
+
"""Run the main application window with buttons for scraping prices, editing the
|
|
20
|
+
configuration, showing history in a chart, and editing the log file.
|
|
21
|
+
"""
|
|
22
|
+
application_window = self._configure_window()
|
|
23
|
+
application_window.mainloop()
|
|
24
|
+
|
|
25
|
+
def _configure_window(self):
|
|
26
|
+
"""Configure the main application window layout with buttons for the various
|
|
27
|
+
actions.
|
|
28
|
+
"""
|
|
19
29
|
window = tk.Tk()
|
|
20
30
|
window.title("CS2Tracker")
|
|
21
31
|
window.geometry("400x400")
|
|
@@ -25,12 +35,8 @@ class Application:
|
|
|
25
35
|
|
|
26
36
|
run_button = tk.Button(window, text="Run!", command=self._scrape_prices)
|
|
27
37
|
edit_button = tk.Button(window, text="Edit Config", command=self._edit_config)
|
|
28
|
-
plot_button = tk.Button(
|
|
29
|
-
|
|
30
|
-
)
|
|
31
|
-
plotfile_button = tk.Button(
|
|
32
|
-
window, text="Show History (File)", command=self._plot_file
|
|
33
|
-
)
|
|
38
|
+
plot_button = tk.Button(window, text="Show History (Chart)", command=self._draw_plot)
|
|
39
|
+
plotfile_button = tk.Button(window, text="Show History (File)", command=self._edit_log_file)
|
|
34
40
|
|
|
35
41
|
run_button.grid(row=1, column=0, pady=10, sticky="NSEW")
|
|
36
42
|
edit_button.grid(row=2, column=0, pady=10, sticky="NSEW")
|
|
@@ -48,24 +54,28 @@ class Application:
|
|
|
48
54
|
plot_button.grid_configure(sticky="NSEW")
|
|
49
55
|
plotfile_button.grid_configure(sticky="NSEW")
|
|
50
56
|
|
|
51
|
-
window
|
|
57
|
+
return window
|
|
52
58
|
|
|
53
59
|
def _scrape_prices(self):
|
|
60
|
+
"""Scrape prices from the configured sources, print the total, and save the
|
|
61
|
+
results to a file.
|
|
62
|
+
"""
|
|
54
63
|
self.scraper.scrape_prices()
|
|
55
|
-
self.scraper.print_total()
|
|
56
|
-
self.scraper.save_to_file()
|
|
57
64
|
|
|
58
65
|
def _edit_config(self):
|
|
66
|
+
"""Edit the configuration file using the specified text editor."""
|
|
59
67
|
subprocess.call([TEXT_EDITOR, CONFIG_FILE])
|
|
60
|
-
|
|
61
|
-
self.scraper.set_config(config)
|
|
68
|
+
self.scraper.parse_config()
|
|
62
69
|
|
|
63
70
|
def _draw_plot(self):
|
|
64
|
-
|
|
71
|
+
"""Draw a plot of the scraped prices over time."""
|
|
72
|
+
dates, dollars, euros = self.scraper.read_price_log()
|
|
65
73
|
|
|
66
|
-
fig,
|
|
67
|
-
ax
|
|
68
|
-
|
|
74
|
+
fig, ax_raw = plt.subplots()
|
|
75
|
+
ax = cast(Axes, ax_raw)
|
|
76
|
+
|
|
77
|
+
ax.plot(dates, dollars, label="Dollars")
|
|
78
|
+
ax.plot(dates, euros, label="Euros")
|
|
69
79
|
ax.set_xlabel("Date")
|
|
70
80
|
ax.set_ylabel("Price")
|
|
71
81
|
ax.legend()
|
|
@@ -76,39 +86,8 @@ class Application:
|
|
|
76
86
|
|
|
77
87
|
plt.show()
|
|
78
88
|
|
|
79
|
-
def
|
|
80
|
-
|
|
81
|
-
date_str, price_str = row
|
|
82
|
-
price = float(price_str[:-1])
|
|
83
|
-
return date_str, price
|
|
84
|
-
|
|
85
|
-
dates = []
|
|
86
|
-
dollars = []
|
|
87
|
-
euros = []
|
|
88
|
-
row_num = 0
|
|
89
|
-
|
|
90
|
-
if not os.path.isfile(OUTPUT_FILE):
|
|
91
|
-
open(OUTPUT_FILE, "w", encoding="utf-8").close()
|
|
92
|
-
|
|
93
|
-
with open(OUTPUT_FILE, "r", newline="", encoding="utf-8") as csvfile:
|
|
94
|
-
reader = csv.reader(csvfile)
|
|
95
|
-
for row in reader:
|
|
96
|
-
row_num += 1
|
|
97
|
-
date, price = parse_row(row)
|
|
98
|
-
if row_num % 2 == 0:
|
|
99
|
-
euros.append(price)
|
|
100
|
-
else:
|
|
101
|
-
dollars.append(price)
|
|
102
|
-
dates.append(date)
|
|
103
|
-
|
|
104
|
-
datesp = []
|
|
105
|
-
for date_str in dates:
|
|
106
|
-
date = datetime.datetime.strptime(date_str[:-9], "%Y-%m-%d")
|
|
107
|
-
datesp.append(date)
|
|
108
|
-
|
|
109
|
-
return datesp, dollars, euros
|
|
110
|
-
|
|
111
|
-
def _plot_file(self):
|
|
89
|
+
def _edit_log_file(self):
|
|
90
|
+
"""Opens the file containing past price calculations."""
|
|
112
91
|
if not os.path.isfile(OUTPUT_FILE):
|
|
113
92
|
open(OUTPUT_FILE, "w", encoding="utf-8").close()
|
|
114
93
|
subprocess.call([TEXT_EDITOR, OUTPUT_FILE])
|
cs2tracker/constants.py
CHANGED
|
@@ -7,178 +7,177 @@ OUTPUT_FILE = f"{BASE_DIR}/data/output.csv"
|
|
|
7
7
|
CONFIG_FILE = f"{BASE_DIR}/data/config.ini"
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
"Antwerp Legends",
|
|
20
|
-
"Antwerp Challengers",
|
|
21
|
-
"Antwerp Contenders",
|
|
22
|
-
"Antwerp Champions Autographs",
|
|
23
|
-
"Antwerp Challengers Autographs",
|
|
24
|
-
"Antwerp Legends Autographs",
|
|
25
|
-
"Antwerp Contenders Autographs",
|
|
26
|
-
"Rio Legends",
|
|
27
|
-
"Rio Challengers",
|
|
28
|
-
"Rio Contenders",
|
|
29
|
-
"Rio Champions Autographs",
|
|
30
|
-
"Rio Challengers Autographs",
|
|
31
|
-
"Rio Legends Autographs",
|
|
32
|
-
"Rio Contenders Autographs",
|
|
33
|
-
"Paris Legends",
|
|
34
|
-
"Paris Challengers",
|
|
35
|
-
"Paris Contenders",
|
|
36
|
-
"Paris Champions Autographs",
|
|
37
|
-
"Paris Challengers Autographs",
|
|
38
|
-
"Paris Legends Autographs",
|
|
39
|
-
"Paris Contenders Autographs",
|
|
40
|
-
"Copenhagen Legends",
|
|
41
|
-
"Copenhagen Challengers",
|
|
42
|
-
"Copenhagen Contenders",
|
|
43
|
-
"Copenhagen Champions Autographs",
|
|
44
|
-
"Copenhagen Challengers Autographs",
|
|
45
|
-
"Copenhagen Legends Autographs",
|
|
46
|
-
"Copenhagen Contenders Autographs",
|
|
47
|
-
"Shanghai Legends",
|
|
48
|
-
"Shanghai Challengers",
|
|
49
|
-
"Shanghai Contenders",
|
|
50
|
-
"Shanghai Champions Autographs",
|
|
51
|
-
"Shanghai Challengers Autographs",
|
|
52
|
-
"Shanghai Legends Autographs",
|
|
53
|
-
"Shanghai Contenders Autographs",
|
|
54
|
-
"Austin Legends",
|
|
55
|
-
"Austin Challengers",
|
|
56
|
-
"Austin Contenders",
|
|
57
|
-
"Austin Champions Autographs",
|
|
58
|
-
"Austin Challengers Autographs",
|
|
59
|
-
"Austin Legends Autographs",
|
|
60
|
-
"Austin Contenders Autographs",
|
|
61
|
-
]
|
|
10
|
+
RMR_CAPSULES = {
|
|
11
|
+
"page": "https://steamcommunity.com/market/search?q=2020+rmr",
|
|
12
|
+
"items": [
|
|
13
|
+
"https://steamcommunity.com/market/listings/730/2020%20RMR%20Legends",
|
|
14
|
+
"https://steamcommunity.com/market/listings/730/2020%20RMR%20Challengers",
|
|
15
|
+
"https://steamcommunity.com/market/listings/730/2020%20RMR%20Contenders",
|
|
16
|
+
],
|
|
17
|
+
"names": ["RMR Legends", "RMR Challengers", "RMR Contenders"],
|
|
18
|
+
}
|
|
62
19
|
|
|
63
|
-
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
20
|
+
STOCKHOLM_CAPSULES = {
|
|
21
|
+
"page": "https://steamcommunity.com/market/search?q=stockholm+capsule",
|
|
22
|
+
"items": [
|
|
23
|
+
"https://steamcommunity.com/market/listings/730/Stockholm%202021%20Legends%20Sticker%20Capsule",
|
|
24
|
+
"https://steamcommunity.com/market/listings/730/Stockholm%202021%20Challengers%20Sticker%20Capsule",
|
|
25
|
+
"https://steamcommunity.com/market/listings/730/Stockholm%202021%20Contenders%20Sticker%20Capsule",
|
|
26
|
+
"https://steamcommunity.com/market/listings/730/Stockholm%202021%20Champions%20Autograph%20Capsule",
|
|
27
|
+
"https://steamcommunity.com/market/listings/730/Stockholm%202021%20Finalists%20Autograph%20Capsule",
|
|
28
|
+
],
|
|
29
|
+
"names": [
|
|
30
|
+
"Stockholm Legends",
|
|
31
|
+
"Stockholm Challengers",
|
|
32
|
+
"Stockholm Contenders",
|
|
33
|
+
"Stockholm Champions Autographs",
|
|
34
|
+
"Stockholm Finalists Autographs",
|
|
35
|
+
],
|
|
36
|
+
}
|
|
73
37
|
|
|
74
|
-
|
|
75
|
-
"https://steamcommunity.com/market/search?q=
|
|
76
|
-
"
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
38
|
+
ANTWERP_CAPSULES = {
|
|
39
|
+
"page": "https://steamcommunity.com/market/search?q=antwerp+capsule",
|
|
40
|
+
"items": [
|
|
41
|
+
"https://steamcommunity.com/market/listings/730/Antwerp%202022%20Legends%20Sticker%20Capsule",
|
|
42
|
+
"https://steamcommunity.com/market/listings/730/Antwerp%202022%20Challengers%20Sticker%20Capsule",
|
|
43
|
+
"https://steamcommunity.com/market/listings/730/Antwerp%202022%20Contenders%20Sticker%20Capsule",
|
|
44
|
+
"https://steamcommunity.com/market/listings/730/Antwerp%202022%20Champions%20Autograph%20Capsule",
|
|
45
|
+
"https://steamcommunity.com/market/listings/730/Antwerp%202022%20Challengers%20Autograph%20Capsule",
|
|
46
|
+
"https://steamcommunity.com/market/listings/730/Antwerp%202022%20Legends%20Autograph%20Capsule",
|
|
47
|
+
"https://steamcommunity.com/market/listings/730/Antwerp%202022%20Contenders%20Autograph%20Capsule",
|
|
48
|
+
],
|
|
49
|
+
"names": [
|
|
50
|
+
"Antwerp Legends",
|
|
51
|
+
"Antwerp Challengers",
|
|
52
|
+
"Antwerp Contenders",
|
|
53
|
+
"Antwerp Champions Autographs",
|
|
54
|
+
"Antwerp Challengers Autographs",
|
|
55
|
+
"Antwerp Legends Autographs",
|
|
56
|
+
"Antwerp Contenders Autographs",
|
|
57
|
+
],
|
|
58
|
+
}
|
|
84
59
|
|
|
85
|
-
|
|
86
|
-
"https://steamcommunity.com/market/
|
|
87
|
-
"
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
"
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
"https://steamcommunity.com/market/listings/730/Rio%202022%20Legends%20Autograph%20Capsule",
|
|
107
|
-
"https://steamcommunity.com/market/listings/730/Rio%202022%20Contenders%20Autograph%20Capsule",
|
|
108
|
-
"https://steamcommunity.com/market/listings/730/Paris%202023%20Legends%20Sticker%20Capsule",
|
|
109
|
-
"https://steamcommunity.com/market/listings/730/Paris%202023%20Challengers%20Sticker%20Capsule",
|
|
110
|
-
"https://steamcommunity.com/market/listings/730/Paris%202023%20Contenders%20Sticker%20Capsule",
|
|
111
|
-
"https://steamcommunity.com/market/listings/730/Paris%202023%20Champions%20Autograph%20Capsule",
|
|
112
|
-
"https://steamcommunity.com/market/listings/730/Paris%202023%20Challengers%20Autograph%20Capsule",
|
|
113
|
-
"https://steamcommunity.com/market/listings/730/Paris%202023%20Legends%20Autograph%20Capsule",
|
|
114
|
-
"https://steamcommunity.com/market/listings/730/Paris%202023%20Contenders%20Autograph%20Capsule",
|
|
115
|
-
"https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Legends%20Sticker%20Capsule",
|
|
116
|
-
"https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Challengers%20Sticker%20Capsule",
|
|
117
|
-
"https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Contenders%20Sticker%20Capsule",
|
|
118
|
-
"https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Champions%20Autograph%20Capsule",
|
|
119
|
-
"https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Challengers%20Autograph%20Capsule",
|
|
120
|
-
"https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Legends%20Autograph%20Capsule",
|
|
121
|
-
"https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Contenders%20Autograph%20Capsule",
|
|
122
|
-
"https://steamcommunity.com/market/listings/730/Shanghai%202024%20Legends%20Sticker%20Capsule",
|
|
123
|
-
"https://steamcommunity.com/market/listings/730/Shanghai%202024%20Challengers%20Sticker%20Capsule",
|
|
124
|
-
"https://steamcommunity.com/market/listings/730/Shanghai%202024%20Contenders%20Sticker%20Capsule",
|
|
125
|
-
"https://steamcommunity.com/market/listings/730/Shanghai%202024%20Champions%20Autograph%20Capsule",
|
|
126
|
-
"https://steamcommunity.com/market/listings/730/Shanghai%202024%20Challengers%20Autograph%20Capsule",
|
|
127
|
-
"https://steamcommunity.com/market/listings/730/Shanghai%202024%20Legends%20Autograph%20Capsule",
|
|
128
|
-
"https://steamcommunity.com/market/listings/730/Shanghai%202024%20Contenders%20Autograph%20Capsule",
|
|
129
|
-
"https://steamcommunity.com/market/listings/730/Austin%202025%20Legends%20Sticker%20Capsule",
|
|
130
|
-
"https://steamcommunity.com/market/listings/730/Austin%202025%20Challengers%20Sticker%20Capsule",
|
|
131
|
-
"https://steamcommunity.com/market/listings/730/Austin%202025%20Contenders%20Sticker%20Capsule",
|
|
132
|
-
"https://steamcommunity.com/market/listings/730/Austin%202025%20Champions%20Autograph%20Capsule",
|
|
133
|
-
"https://steamcommunity.com/market/listings/730/Austin%202025%20Challengers%20Autograph%20Capsule",
|
|
134
|
-
"https://steamcommunity.com/market/listings/730/Austin%202025%20Legends%20Autograph%20Capsule",
|
|
135
|
-
"https://steamcommunity.com/market/listings/730/Austin%202025%20Contenders%20Autograph%20Capsule",
|
|
136
|
-
]
|
|
60
|
+
RIO_CAPSULES = {
|
|
61
|
+
"page": "https://steamcommunity.com/market/search?q=rio+capsule",
|
|
62
|
+
"items": [
|
|
63
|
+
"https://steamcommunity.com/market/listings/730/Rio%202022%20Legends%20Sticker%20Capsule",
|
|
64
|
+
"https://steamcommunity.com/market/listings/730/Rio%202022%20Challengers%20Sticker%20Capsule",
|
|
65
|
+
"https://steamcommunity.com/market/listings/730/Rio%202022%20Contenders%20Sticker%20Capsule",
|
|
66
|
+
"https://steamcommunity.com/market/listings/730/Rio%202022%20Champions%20Autograph%20Capsule",
|
|
67
|
+
"https://steamcommunity.com/market/listings/730/Rio%202022%20Challengers%20Autograph%20Capsule",
|
|
68
|
+
"https://steamcommunity.com/market/listings/730/Rio%202022%20Legends%20Autograph%20Capsule",
|
|
69
|
+
"https://steamcommunity.com/market/listings/730/Rio%202022%20Contenders%20Autograph%20Capsule",
|
|
70
|
+
],
|
|
71
|
+
"names": [
|
|
72
|
+
"Rio Legends",
|
|
73
|
+
"Rio Challengers",
|
|
74
|
+
"Rio Contenders",
|
|
75
|
+
"Rio Champions Autographs",
|
|
76
|
+
"Rio Challengers Autographs",
|
|
77
|
+
"Rio Legends Autographs",
|
|
78
|
+
"Rio Contenders Autographs",
|
|
79
|
+
],
|
|
80
|
+
}
|
|
137
81
|
|
|
138
|
-
|
|
139
|
-
"
|
|
140
|
-
"
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
"
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
"
|
|
162
|
-
"
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
"
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
82
|
+
PARIS_CAPSULES = {
|
|
83
|
+
"page": "https://steamcommunity.com/market/search?q=paris+capsule",
|
|
84
|
+
"items": [
|
|
85
|
+
"https://steamcommunity.com/market/listings/730/Paris%202023%20Legends%20Sticker%20Capsule",
|
|
86
|
+
"https://steamcommunity.com/market/listings/730/Paris%202023%20Challengers%20Sticker%20Capsule",
|
|
87
|
+
"https://steamcommunity.com/market/listings/730/Paris%202023%20Contenders%20Sticker%20Capsule",
|
|
88
|
+
"https://steamcommunity.com/market/listings/730/Paris%202023%20Champions%20Autograph%20Capsule",
|
|
89
|
+
"https://steamcommunity.com/market/listings/730/Paris%202023%20Challengers%20Autograph%20Capsule",
|
|
90
|
+
"https://steamcommunity.com/market/listings/730/Paris%202023%20Legends%20Autograph%20Capsule",
|
|
91
|
+
"https://steamcommunity.com/market/listings/730/Paris%202023%20Contenders%20Autograph%20Capsule",
|
|
92
|
+
],
|
|
93
|
+
"names": [
|
|
94
|
+
"Paris Legends",
|
|
95
|
+
"Paris Challengers",
|
|
96
|
+
"Paris Contenders",
|
|
97
|
+
"Paris Champions Autographs",
|
|
98
|
+
"Paris Challengers Autographs",
|
|
99
|
+
"Paris Legends Autographs",
|
|
100
|
+
"Paris Contenders Autographs",
|
|
101
|
+
],
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
COPENHAGEN_CAPSULES = {
|
|
105
|
+
"page": "https://steamcommunity.com/market/search?q=copenhagen+capsule",
|
|
106
|
+
"items": [
|
|
107
|
+
"https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Legends%20Sticker%20Capsule",
|
|
108
|
+
"https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Challengers%20Sticker%20Capsule",
|
|
109
|
+
"https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Contenders%20Sticker%20Capsule",
|
|
110
|
+
"https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Champions%20Autograph%20Capsule",
|
|
111
|
+
"https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Challengers%20Autograph%20Capsule",
|
|
112
|
+
"https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Legends%20Autograph%20Capsule",
|
|
113
|
+
"https://steamcommunity.com/market/listings/730/Copenhagen%202024%20Contenders%20Autograph%20Capsule",
|
|
114
|
+
],
|
|
115
|
+
"names": [
|
|
116
|
+
"Copenhagen Legends",
|
|
117
|
+
"Copenhagen Challengers",
|
|
118
|
+
"Copenhagen Contenders",
|
|
119
|
+
"Copenhagen Champions Autographs",
|
|
120
|
+
"Copenhagen Challengers Autographs",
|
|
121
|
+
"Copenhagen Legends Autographs",
|
|
122
|
+
"Copenhagen Contenders Autographs",
|
|
123
|
+
],
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
SHANGHAI_CAPSULES = {
|
|
127
|
+
"page": "https://steamcommunity.com/market/search?q=shanghai+capsule",
|
|
128
|
+
"items": [
|
|
129
|
+
"https://steamcommunity.com/market/listings/730/Shanghai%202024%20Legends%20Sticker%20Capsule",
|
|
130
|
+
"https://steamcommunity.com/market/listings/730/Shanghai%202024%20Challengers%20Sticker%20Capsule",
|
|
131
|
+
"https://steamcommunity.com/market/listings/730/Shanghai%202024%20Contenders%20Sticker%20Capsule",
|
|
132
|
+
"https://steamcommunity.com/market/listings/730/Shanghai%202024%20Champions%20Autograph%20Capsule",
|
|
133
|
+
"https://steamcommunity.com/market/listings/730/Shanghai%202024%20Challengers%20Autograph%20Capsule",
|
|
134
|
+
"https://steamcommunity.com/market/listings/730/Shanghai%202024%20Legends%20Autograph%20Capsule",
|
|
135
|
+
"https://steamcommunity.com/market/listings/730/Shanghai%202024%20Contenders%20Autograph%20Capsule",
|
|
136
|
+
],
|
|
137
|
+
"names": [
|
|
138
|
+
"Shanghai Legends",
|
|
139
|
+
"Shanghai Challengers",
|
|
140
|
+
"Shanghai Contenders",
|
|
141
|
+
"Shanghai Champions Autographs",
|
|
142
|
+
"Shanghai Challengers Autographs",
|
|
143
|
+
"Shanghai Legends Autographs",
|
|
144
|
+
"Shanghai Contenders Autographs",
|
|
145
|
+
],
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
AUSTIN_CAPSULES = {
|
|
150
|
+
"page": "https://steamcommunity.com/market/search?q=austin+capsule",
|
|
151
|
+
"items": [
|
|
152
|
+
"https://steamcommunity.com/market/listings/730/Austin%202025%20Legends%20Sticker%20Capsule",
|
|
153
|
+
"https://steamcommunity.com/market/listings/730/Austin%202025%20Challengers%20Sticker%20Capsule",
|
|
154
|
+
"https://steamcommunity.com/market/listings/730/Austin%202025%20Contenders%20Sticker%20Capsule",
|
|
155
|
+
"https://steamcommunity.com/market/listings/730/Austin%202025%20Champions%20Autograph%20Capsule",
|
|
156
|
+
"https://steamcommunity.com/market/listings/730/Austin%202025%20Challengers%20Autograph%20Capsule",
|
|
157
|
+
"https://steamcommunity.com/market/listings/730/Austin%202025%20Legends%20Autograph%20Capsule",
|
|
158
|
+
"https://steamcommunity.com/market/listings/730/Austin%202025%20Contenders%20Autograph%20Capsule",
|
|
159
|
+
],
|
|
160
|
+
"names": [
|
|
161
|
+
"Austin Legends",
|
|
162
|
+
"Austin Challengers",
|
|
163
|
+
"Austin Contenders",
|
|
164
|
+
"Austin Champions Autographs",
|
|
165
|
+
"Austin Challengers Autographs",
|
|
166
|
+
"Austin Legends Autographs",
|
|
167
|
+
"Austin Contenders Autographs",
|
|
168
|
+
],
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
CAPSULE_INFO = {
|
|
172
|
+
"2020 RMR Sticker Capsule": RMR_CAPSULES,
|
|
173
|
+
"Stockholm Sticker Capsule": STOCKHOLM_CAPSULES,
|
|
174
|
+
"Antwerp Sticker Capsule": ANTWERP_CAPSULES,
|
|
175
|
+
"Rio Sticker Capsule": RIO_CAPSULES,
|
|
176
|
+
"Paris Sticker Capsule": PARIS_CAPSULES,
|
|
177
|
+
"Copenhagen Sticker Capsule": COPENHAGEN_CAPSULES,
|
|
178
|
+
"Shanghai Sticker Capsule": SHANGHAI_CAPSULES,
|
|
179
|
+
"Austin Sticker Capsule": AUSTIN_CAPSULES,
|
|
180
|
+
}
|
|
182
181
|
|
|
183
182
|
|
|
184
183
|
CASE_PAGES = [
|
cs2tracker/data/config.ini
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
[2020 RMR]
|
|
1
|
+
[2020 RMR Sticker Capsule]
|
|
2
2
|
RMR_Challengers = 0
|
|
3
3
|
RMR_Legends = 0
|
|
4
4
|
RMR_Contenders = 0
|
|
5
5
|
|
|
6
|
-
[Stockholm]
|
|
6
|
+
[Stockholm Sticker Capsule]
|
|
7
7
|
Stockholm_Challengers = 0
|
|
8
8
|
Stockholm_Legends = 0
|
|
9
9
|
Stockholm_Contenders = 0
|
|
10
10
|
Stockholm_Champions_Autographs = 0
|
|
11
11
|
Stockholm_Finalists_Autographs = 0
|
|
12
12
|
|
|
13
|
-
[Antwerp]
|
|
13
|
+
[Antwerp Sticker Capsule]
|
|
14
14
|
Antwerp_Challengers = 0
|
|
15
15
|
Antwerp_Legends = 0
|
|
16
16
|
Antwerp_Contenders = 0
|
|
@@ -19,7 +19,7 @@ Antwerp_Contenders_Autographs = 0
|
|
|
19
19
|
Antwerp_Challengers_Autographs = 0
|
|
20
20
|
Antwerp_Legends_Autographs = 0
|
|
21
21
|
|
|
22
|
-
[Rio]
|
|
22
|
+
[Rio Sticker Capsule]
|
|
23
23
|
Rio_Challengers = 0
|
|
24
24
|
Rio_Legends = 0
|
|
25
25
|
Rio_Contenders = 0
|
|
@@ -28,7 +28,7 @@ Rio_Contenders_Autographs = 0
|
|
|
28
28
|
Rio_Challengers_Autographs = 0
|
|
29
29
|
Rio_Legends_Autographs = 0
|
|
30
30
|
|
|
31
|
-
[Paris]
|
|
31
|
+
[Paris Sticker Capsule]
|
|
32
32
|
Paris_Challengers = 0
|
|
33
33
|
Paris_Legends = 0
|
|
34
34
|
Paris_Contenders = 0
|
|
@@ -37,7 +37,7 @@ Paris_Contenders_Autographs = 0
|
|
|
37
37
|
Paris_Challengers_Autographs = 0
|
|
38
38
|
Paris_Legends_Autographs = 0
|
|
39
39
|
|
|
40
|
-
[Copenhagen]
|
|
40
|
+
[Copenhagen Sticker Capsule]
|
|
41
41
|
Copenhagen_Challengers = 0
|
|
42
42
|
Copenhagen_Legends = 0
|
|
43
43
|
Copenhagen_Contenders = 0
|
|
@@ -46,7 +46,7 @@ Copenhagen_Contenders_Autographs = 0
|
|
|
46
46
|
Copenhagen_Challengers_Autographs = 0
|
|
47
47
|
Copenhagen_Legends_Autographs = 0
|
|
48
48
|
|
|
49
|
-
[Shanghai]
|
|
49
|
+
[Shanghai Sticker Capsule]
|
|
50
50
|
Shanghai_Challengers = 0
|
|
51
51
|
Shanghai_Legends = 0
|
|
52
52
|
Shanghai_Contenders = 0
|
|
@@ -55,7 +55,7 @@ Shanghai_Contenders_Autographs = 0
|
|
|
55
55
|
Shanghai_Challengers_Autographs = 0
|
|
56
56
|
Shanghai_Legends_Autographs = 0
|
|
57
57
|
|
|
58
|
-
[Austin]
|
|
58
|
+
[Austin Sticker Capsule]
|
|
59
59
|
Austin_Challengers = 0
|
|
60
60
|
Austin_Legends = 0
|
|
61
61
|
Austin_Contenders = 0
|
|
@@ -108,6 +108,6 @@ Kilowatt_Case = 0
|
|
|
108
108
|
Gallery_Case = 0
|
|
109
109
|
Fever_Case = 0
|
|
110
110
|
|
|
111
|
-
[
|
|
111
|
+
[Settings]
|
|
112
112
|
Use_Proxy = False
|
|
113
113
|
API_Key = None
|
cs2tracker/main.py
CHANGED
|
@@ -3,11 +3,18 @@ from datetime import datetime
|
|
|
3
3
|
import urllib3
|
|
4
4
|
from rich.console import Console
|
|
5
5
|
|
|
6
|
-
from ._version import version
|
|
7
|
-
from .application import Application
|
|
6
|
+
from cs2tracker._version import version # pylint: disable=E0611
|
|
7
|
+
from cs2tracker.application import Application
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def main():
|
|
11
|
+
"""
|
|
12
|
+
The main entry point for the CS2Tracker application.
|
|
13
|
+
|
|
14
|
+
Provides a console output with the application version and date, and initializes the
|
|
15
|
+
application.
|
|
16
|
+
"""
|
|
17
|
+
|
|
11
18
|
## disable warnings for proxy requests
|
|
12
19
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
13
20
|
|
|
@@ -25,7 +32,7 @@ def main():
|
|
|
25
32
|
|
|
26
33
|
|
|
27
34
|
"""
|
|
28
|
-
+ f"Version: v{version} - {datetime.today().strftime('%Y/%m/%d')} - Jannik Novak @
|
|
35
|
+
+ f"Version: v{version} - {datetime.today().strftime('%Y/%m/%d')} - Jannik Novak @ashiven\n"
|
|
29
36
|
)
|
|
30
37
|
|
|
31
38
|
application = Application()
|
cs2tracker/scraper.py
CHANGED
|
@@ -1,338 +1,305 @@
|
|
|
1
|
-
import configparser
|
|
2
1
|
import csv
|
|
3
|
-
import datetime
|
|
4
2
|
import os
|
|
5
3
|
import time
|
|
4
|
+
from configparser import ConfigParser
|
|
5
|
+
from datetime import datetime
|
|
6
6
|
|
|
7
|
-
import requests
|
|
8
7
|
from bs4 import BeautifulSoup
|
|
8
|
+
from bs4.element import Tag
|
|
9
9
|
from currency_converter import CurrencyConverter
|
|
10
|
+
from requests import RequestException, Session
|
|
10
11
|
from requests.adapters import HTTPAdapter, Retry
|
|
11
12
|
from rich.console import Console
|
|
12
|
-
from tenacity import retry, stop_after_attempt
|
|
13
|
+
from tenacity import RetryError, retry, stop_after_attempt
|
|
13
14
|
|
|
14
|
-
from .constants import (
|
|
15
|
-
|
|
16
|
-
CAPSULE_NAMES,
|
|
17
|
-
CAPSULE_NAMES_GENERIC,
|
|
18
|
-
CAPSULE_PAGES,
|
|
15
|
+
from cs2tracker.constants import (
|
|
16
|
+
CAPSULE_INFO,
|
|
19
17
|
CASE_HREFS,
|
|
20
|
-
CASE_NAMES,
|
|
21
18
|
CASE_PAGES,
|
|
22
19
|
CONFIG_FILE,
|
|
23
20
|
OUTPUT_FILE,
|
|
24
21
|
)
|
|
25
22
|
|
|
26
23
|
MAX_LINE_LEN = 72
|
|
24
|
+
SEPARATOR = "-"
|
|
25
|
+
PRICE_INFO = "Owned: {} Steam market price: ${} Total: ${}\n"
|
|
27
26
|
|
|
28
27
|
|
|
29
28
|
class Scraper:
|
|
30
29
|
def __init__(self):
|
|
31
|
-
|
|
32
|
-
self.
|
|
33
|
-
|
|
34
|
-
self.
|
|
35
|
-
|
|
36
|
-
self.
|
|
37
|
-
self.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
self.
|
|
42
|
-
self.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
self.session = requests.Session()
|
|
30
|
+
"""Initialize the Scraper class."""
|
|
31
|
+
self.console = Console()
|
|
32
|
+
self.parse_config()
|
|
33
|
+
self._start_session()
|
|
34
|
+
|
|
35
|
+
self.usd_total = 0
|
|
36
|
+
self.eur_total = 0
|
|
37
|
+
|
|
38
|
+
def parse_config(self):
|
|
39
|
+
"""Parse the configuration file to read settings and user-owned items."""
|
|
40
|
+
self.config = ConfigParser()
|
|
41
|
+
self.config.read(CONFIG_FILE)
|
|
42
|
+
|
|
43
|
+
def _start_session(self):
|
|
44
|
+
"""Start a requests session with custom headers and retry logic."""
|
|
45
|
+
self.session = Session()
|
|
48
46
|
self.session.headers.update(
|
|
49
47
|
{
|
|
50
48
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"
|
|
51
49
|
}
|
|
52
50
|
)
|
|
53
|
-
retries = Retry(
|
|
54
|
-
total=5, backoff_factor=0.1, status_forcelist=[500, 502, 503, 504, 520]
|
|
55
|
-
)
|
|
51
|
+
retries = Retry(total=5, backoff_factor=0.1, status_forcelist=[500, 502, 503, 504, 520])
|
|
56
52
|
self.session.mount("http://", HTTPAdapter(max_retries=retries))
|
|
57
53
|
self.session.mount("https://", HTTPAdapter(max_retries=retries))
|
|
58
54
|
|
|
59
|
-
self.console = Console()
|
|
60
|
-
|
|
61
|
-
config = self.parse_config()
|
|
62
|
-
self.set_config(config)
|
|
63
|
-
|
|
64
55
|
def scrape_prices(self):
|
|
65
|
-
for
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
elif "stockholm" in capsule_page_url:
|
|
75
|
-
capsule_name = "Stockholm"
|
|
76
|
-
capsule_quantities = self.stockholm_quantities
|
|
77
|
-
capsule_hrefs = CAPSULE_HREFS[3:8]
|
|
78
|
-
capsule_names_generic = CAPSULE_NAMES_GENERIC[0:4] + [
|
|
79
|
-
CAPSULE_NAMES_GENERIC[-1]
|
|
80
|
-
]
|
|
81
|
-
elif "antwerp" in capsule_page_url:
|
|
82
|
-
capsule_name = "Antwerp"
|
|
83
|
-
capsule_quantities = self.antwerp_quantities
|
|
84
|
-
capsule_hrefs = CAPSULE_HREFS[8:15]
|
|
85
|
-
capsule_names_generic = CAPSULE_NAMES_GENERIC[0:7]
|
|
86
|
-
elif "rio" in capsule_page_url:
|
|
87
|
-
capsule_name = "Rio"
|
|
88
|
-
capsule_quantities = self.rio_quantities
|
|
89
|
-
capsule_hrefs = CAPSULE_HREFS[15:22]
|
|
90
|
-
capsule_names_generic = CAPSULE_NAMES_GENERIC[0:7]
|
|
91
|
-
elif "paris" in capsule_page_url:
|
|
92
|
-
capsule_name = "Paris"
|
|
93
|
-
capsule_quantities = self.paris_quantities
|
|
94
|
-
capsule_hrefs = CAPSULE_HREFS[22:29]
|
|
95
|
-
capsule_names_generic = CAPSULE_NAMES_GENERIC[0:7]
|
|
96
|
-
elif "copenhagen" in capsule_page_url:
|
|
97
|
-
capsule_name = "Copenhagen"
|
|
98
|
-
capsule_quantities = self.copenhagen_quantities
|
|
99
|
-
capsule_hrefs = CAPSULE_HREFS[29:36]
|
|
100
|
-
capsule_names_generic = CAPSULE_NAMES_GENERIC[0:7]
|
|
101
|
-
elif "shanghai" in capsule_page_url:
|
|
102
|
-
capsule_name = "Shanghai"
|
|
103
|
-
capsule_quantities = self.shanghai_quantities
|
|
104
|
-
capsule_hrefs = CAPSULE_HREFS[36:43]
|
|
105
|
-
capsule_names_generic = CAPSULE_NAMES_GENERIC[0:7]
|
|
106
|
-
elif "austin" in capsule_page_url:
|
|
107
|
-
capsule_name = "Austin"
|
|
108
|
-
capsule_quantities = self.austin_quantities
|
|
109
|
-
capsule_hrefs = CAPSULE_HREFS[43:50]
|
|
110
|
-
capsule_names_generic = CAPSULE_NAMES_GENERIC[0:7]
|
|
111
|
-
|
|
112
|
-
self._scrape_prices_capsule(
|
|
113
|
-
capsule_page_url,
|
|
114
|
-
capsule_hrefs,
|
|
115
|
-
capsule_name,
|
|
116
|
-
capsule_names_generic,
|
|
117
|
-
capsule_quantities,
|
|
56
|
+
"""Scrape prices for capsules and cases, calculate totals in USD and EUR, and
|
|
57
|
+
print/save the results.
|
|
58
|
+
"""
|
|
59
|
+
capsule_usd_total = 0
|
|
60
|
+
try:
|
|
61
|
+
capsule_usd_total = self.scrape_capsule_section_prices()
|
|
62
|
+
except (RequestException, AttributeError, RetryError, ValueError):
|
|
63
|
+
self.console.print(
|
|
64
|
+
"[bold red]Failed to scrape capsule prices. (Consider using proxies to prevent rate limiting)\n"
|
|
118
65
|
)
|
|
119
66
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
67
|
+
case_usd_total = 0
|
|
68
|
+
try:
|
|
69
|
+
case_usd_total = self._scrape_case_prices()
|
|
70
|
+
except (RequestException, AttributeError, RetryError, ValueError):
|
|
71
|
+
self.console.print(
|
|
72
|
+
"[bold red]Failed to scrape case prices. (Consider using proxies to prevent rate limiting)\n"
|
|
73
|
+
)
|
|
123
74
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
self.
|
|
127
|
-
self.console.print(f"${self.total_price:.2f}")
|
|
75
|
+
self.usd_total += capsule_usd_total
|
|
76
|
+
self.usd_total += case_usd_total
|
|
77
|
+
self.eur_total = CurrencyConverter().convert(self.usd_total, "USD", "EUR")
|
|
128
78
|
|
|
129
|
-
self.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
self.
|
|
134
|
-
|
|
135
|
-
|
|
79
|
+
self._print_total()
|
|
80
|
+
self._save_price_log()
|
|
81
|
+
|
|
82
|
+
# reset totals for next run
|
|
83
|
+
self.usd_total, self.eur_total = 0, 0
|
|
84
|
+
|
|
85
|
+
def _print_total(self):
|
|
86
|
+
"""Print the total prices in USD and EUR, formatted with titles and
|
|
87
|
+
separators.
|
|
88
|
+
"""
|
|
89
|
+
usd_title = "USD Total".center(MAX_LINE_LEN, SEPARATOR)
|
|
90
|
+
self.console.print(f"[bold green]{usd_title}")
|
|
91
|
+
self.console.print(f"${self.usd_total:.2f}")
|
|
92
|
+
|
|
93
|
+
eur_title = "EUR Total".center(MAX_LINE_LEN, SEPARATOR)
|
|
94
|
+
self.console.print(f"[bold green]{eur_title}")
|
|
95
|
+
self.console.print(f"€{self.eur_total:.2f}")
|
|
96
|
+
|
|
97
|
+
end_string = SEPARATOR * MAX_LINE_LEN
|
|
136
98
|
self.console.print(f"[bold green]{end_string}\n")
|
|
137
99
|
|
|
138
|
-
def
|
|
139
|
-
|
|
140
|
-
date
|
|
100
|
+
def _save_price_log(self):
|
|
101
|
+
"""
|
|
102
|
+
Save the current date and total prices in USD and EUR to a CSV file.
|
|
141
103
|
|
|
104
|
+
This will append a new entry to the output file if no entry has been made for
|
|
105
|
+
today.
|
|
106
|
+
"""
|
|
142
107
|
if not os.path.isfile(OUTPUT_FILE):
|
|
143
108
|
open(OUTPUT_FILE, "w", encoding="utf-8").close()
|
|
144
109
|
|
|
145
|
-
with open(OUTPUT_FILE, "r", encoding="utf-8") as
|
|
146
|
-
|
|
110
|
+
with open(OUTPUT_FILE, "r", encoding="utf-8") as price_logs:
|
|
111
|
+
price_logs_reader = csv.reader(price_logs)
|
|
147
112
|
last_row = None
|
|
148
|
-
for row in
|
|
113
|
+
for row in price_logs_reader:
|
|
149
114
|
last_row = row
|
|
150
115
|
if last_row:
|
|
151
|
-
|
|
116
|
+
last_log_date = last_row[0][:10]
|
|
152
117
|
else:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return config
|
|
118
|
+
last_log_date = ""
|
|
119
|
+
|
|
120
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
121
|
+
if last_log_date != today:
|
|
122
|
+
with open(OUTPUT_FILE, "a", newline="", encoding="utf-8") as price_logs:
|
|
123
|
+
price_logs_writer = csv.writer(price_logs)
|
|
124
|
+
price_logs_writer.writerow([today, f"{self.usd_total:.2f}$"])
|
|
125
|
+
price_logs_writer.writerow([today, f"{self.eur_total:.2f}€"])
|
|
126
|
+
|
|
127
|
+
def read_price_log(self):
|
|
128
|
+
"""
|
|
129
|
+
Parse the output file to extract dates, dollar prices, and euro prices. This
|
|
130
|
+
data is used for drawing the plot of past prices.
|
|
131
|
+
|
|
132
|
+
:return: A tuple containing three lists: dates, dollar prices, and euro prices.
|
|
133
|
+
"""
|
|
134
|
+
if not os.path.isfile(OUTPUT_FILE):
|
|
135
|
+
open(OUTPUT_FILE, "w", encoding="utf-8").close()
|
|
172
136
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
for capsule_name in CAPSULE_NAMES:
|
|
191
|
-
config_capsule_name = capsule_name.replace(" ", "_")
|
|
192
|
-
if "RMR" in capsule_name:
|
|
193
|
-
self.rmr_quantities.append(
|
|
194
|
-
int(config.get("2020 RMR", config_capsule_name))
|
|
195
|
-
)
|
|
196
|
-
elif "Stockholm" in capsule_name:
|
|
197
|
-
self.stockholm_quantities.append(
|
|
198
|
-
int(config.get("Stockholm", config_capsule_name))
|
|
199
|
-
)
|
|
200
|
-
elif "Antwerp" in capsule_name:
|
|
201
|
-
self.antwerp_quantities.append(
|
|
202
|
-
int(config.get("Antwerp", config_capsule_name))
|
|
203
|
-
)
|
|
204
|
-
elif "Rio" in capsule_name:
|
|
205
|
-
self.rio_quantities.append(int(config.get("Rio", config_capsule_name)))
|
|
206
|
-
elif "Paris" in capsule_name:
|
|
207
|
-
self.paris_quantities.append(
|
|
208
|
-
int(config.get("Paris", config_capsule_name))
|
|
209
|
-
)
|
|
210
|
-
elif "Copenhagen" in capsule_name:
|
|
211
|
-
self.copenhagen_quantities.append(
|
|
212
|
-
int(config.get("Copenhagen", config_capsule_name))
|
|
213
|
-
)
|
|
214
|
-
elif "Shanghai" in capsule_name:
|
|
215
|
-
self.shanghai_quantities.append(
|
|
216
|
-
int(config.get("Shanghai", config_capsule_name))
|
|
217
|
-
)
|
|
218
|
-
elif "Austin" in capsule_name:
|
|
219
|
-
self.austin_quantities.append(
|
|
220
|
-
int(config.get("Austin", config_capsule_name))
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
for case_name in CASE_NAMES:
|
|
224
|
-
config_case_name = case_name.replace(" ", "_")
|
|
225
|
-
self.case_quantities.append(int(config.get("Cases", config_case_name)))
|
|
137
|
+
dates, dollars, euros = [], [], []
|
|
138
|
+
with open(OUTPUT_FILE, "r", newline="", encoding="utf-8") as price_logs:
|
|
139
|
+
price_logs_reader = csv.reader(price_logs)
|
|
140
|
+
for row in price_logs_reader:
|
|
141
|
+
date, price_with_currency = row
|
|
142
|
+
date = datetime.strptime(date, "%Y-%m-%d")
|
|
143
|
+
price = float(price_with_currency.rstrip("$€"))
|
|
144
|
+
if price_with_currency.endswith("€"):
|
|
145
|
+
euros.append(price)
|
|
146
|
+
else:
|
|
147
|
+
dollars.append(price)
|
|
148
|
+
# Only append every second date since the dates are the same for euros and dollars
|
|
149
|
+
# and we want the length of dates to match the lengths of dollars and euros
|
|
150
|
+
dates.append(date)
|
|
151
|
+
|
|
152
|
+
return dates, dollars, euros
|
|
226
153
|
|
|
227
154
|
@retry(stop=stop_after_attempt(10))
|
|
228
155
|
def _get_page(self, url):
|
|
229
|
-
|
|
156
|
+
"""
|
|
157
|
+
Get the page content from the given URL, using a proxy if configured. If the
|
|
158
|
+
request fails, it will retry up to 10 times.
|
|
159
|
+
|
|
160
|
+
:param url: The URL to fetch the page from.
|
|
161
|
+
:return: The HTTP response object containing the page content.
|
|
162
|
+
:raises RequestException: If the request fails.
|
|
163
|
+
:raises RetryError: If the retry limit is reached.
|
|
164
|
+
"""
|
|
165
|
+
use_proxy = self.config.getboolean("Settings", "Use_Proxy", fallback=False)
|
|
166
|
+
api_key = self.config.get("Settings", "API_Key", fallback=None)
|
|
167
|
+
if use_proxy and api_key:
|
|
230
168
|
page = self.session.get(
|
|
231
169
|
url=url,
|
|
232
170
|
proxies={
|
|
233
|
-
"http": f"http://{
|
|
234
|
-
"https": f"http://{
|
|
171
|
+
"http": f"http://{api_key}:@smartproxy.crawlbase.com:8012",
|
|
172
|
+
"https": f"http://{api_key}:@smartproxy.crawlbase.com:8012",
|
|
235
173
|
},
|
|
236
174
|
verify=False,
|
|
237
175
|
)
|
|
238
176
|
else:
|
|
239
177
|
page = self.session.get(url)
|
|
240
178
|
|
|
179
|
+
if not page.ok or not page.content:
|
|
180
|
+
status = page.status_code
|
|
181
|
+
self.console.print(f"[bold red][!] Failed to load page ({status}). Retrying...\n")
|
|
182
|
+
raise RequestException(f"Failed to load page: {url}")
|
|
183
|
+
|
|
241
184
|
return page
|
|
242
185
|
|
|
243
|
-
def
|
|
186
|
+
def _parse_capsule_price(self, capsule_page, capsule_href):
|
|
187
|
+
"""
|
|
188
|
+
Parse the price of a capsule from the given page and href.
|
|
189
|
+
|
|
190
|
+
:param capsule_page: The HTTP response object containing the capsule page
|
|
191
|
+
content.
|
|
192
|
+
:param capsule_href: The href of the capsule listing to find the price for.
|
|
193
|
+
:return: The price of the capsule as a float.
|
|
194
|
+
:raises ValueError: If the capsule listing or price span cannot be found.
|
|
195
|
+
"""
|
|
196
|
+
capsule_soup = BeautifulSoup(capsule_page.content, "html.parser")
|
|
197
|
+
capsule_listing = capsule_soup.find("a", attrs={"href": f"{capsule_href}"})
|
|
198
|
+
if not isinstance(capsule_listing, Tag):
|
|
199
|
+
raise ValueError(f"Failed to find capsule listing: {capsule_href}")
|
|
200
|
+
|
|
201
|
+
price_span = capsule_listing.find("span", attrs={"class": "normal_price"})
|
|
202
|
+
if not isinstance(price_span, Tag):
|
|
203
|
+
raise ValueError(f"Failed to find price span in capsule listing: {capsule_href}")
|
|
204
|
+
|
|
205
|
+
price_str = price_span.text.split()[2]
|
|
206
|
+
price = float(price_str.replace("$", ""))
|
|
207
|
+
|
|
208
|
+
return price
|
|
209
|
+
|
|
210
|
+
def _scrape_capsule_prices(
|
|
244
211
|
self,
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
capsule_name,
|
|
248
|
-
capsule_names_generic,
|
|
249
|
-
capsule_quantities,
|
|
212
|
+
capsule_section,
|
|
213
|
+
capsule_info,
|
|
250
214
|
):
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
215
|
+
"""
|
|
216
|
+
Scrape prices for a specific capsule section, printing the details to the
|
|
217
|
+
console.
|
|
218
|
+
|
|
219
|
+
:param capsule_section: The section name in the config for the capsule.
|
|
220
|
+
:param capsule_info: A dictionary containing information about the capsule
|
|
221
|
+
section,
|
|
222
|
+
"""
|
|
223
|
+
capsule_title = capsule_section.center(MAX_LINE_LEN, SEPARATOR)
|
|
224
|
+
self.console.print(f"[bold magenta]{capsule_title}")
|
|
225
|
+
|
|
226
|
+
capsule_price_total = 0
|
|
227
|
+
capsule_page = capsule_info["page"]
|
|
228
|
+
capsule_names = capsule_info["names"]
|
|
229
|
+
capsule_hrefs = capsule_info["items"]
|
|
230
|
+
capsule_page = self._get_page(capsule_page)
|
|
231
|
+
for capsule_name, capsule_href in zip(capsule_names, capsule_hrefs):
|
|
232
|
+
config_capsule_name = capsule_name.replace(" ", "_")
|
|
233
|
+
owned = self.config.getint(capsule_section, config_capsule_name, fallback=0)
|
|
234
|
+
if owned == 0:
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
price_usd = self._parse_capsule_price(capsule_page, capsule_href)
|
|
238
|
+
price_usd_owned = round(float(owned * price_usd), 2)
|
|
239
|
+
|
|
240
|
+
self.console.print(f"[bold deep_sky_blue4]{capsule_name}")
|
|
241
|
+
self.console.print(PRICE_INFO.format(owned, price_usd, price_usd_owned))
|
|
242
|
+
capsule_price_total += price_usd_owned
|
|
243
|
+
|
|
244
|
+
return capsule_price_total
|
|
245
|
+
|
|
246
|
+
def scrape_capsule_section_prices(self):
|
|
247
|
+
"""Scrape prices for all capsule sections defined in the configuration."""
|
|
248
|
+
capsule_usd_total = 0
|
|
249
|
+
for capsule_section, capsule_info in CAPSULE_INFO.items():
|
|
250
|
+
# only scrape capsule sections where the user owns at least one item
|
|
251
|
+
if any(int(owned) > 0 for _, owned in self.config.items(capsule_section)):
|
|
252
|
+
capsule_usd_total += self._scrape_capsule_prices(capsule_section, capsule_info)
|
|
253
|
+
|
|
254
|
+
return capsule_usd_total
|
|
255
|
+
|
|
256
|
+
def _parse_case_price(self, case_page, case_href):
|
|
257
|
+
"""
|
|
258
|
+
Parse the price of a case from the given page and href.
|
|
259
|
+
|
|
260
|
+
:param case_page: The HTTP response object containing the case page content.
|
|
261
|
+
:param case_href: The href of the case listing to find the price for.
|
|
262
|
+
:return: The price of the case as a float.
|
|
263
|
+
:raises ValueError: If the case listing or price span cannot be found.
|
|
264
|
+
"""
|
|
265
|
+
case_soup = BeautifulSoup(case_page.content, "html.parser")
|
|
266
|
+
case_listing = case_soup.find("a", attrs={"href": case_href})
|
|
267
|
+
if not isinstance(case_listing, Tag):
|
|
268
|
+
raise ValueError(f"Failed to find case listing: {case_href}")
|
|
269
|
+
|
|
270
|
+
price_class = case_listing.find("span", attrs={"class": "normal_price"})
|
|
271
|
+
if not isinstance(price_class, Tag):
|
|
272
|
+
raise ValueError(f"Failed to find price class in case listing: {case_href}")
|
|
273
|
+
|
|
274
|
+
price_str = price_class.text.split()[2]
|
|
275
|
+
price = float(price_str.replace("$", ""))
|
|
276
|
+
|
|
277
|
+
return price
|
|
278
|
+
|
|
279
|
+
def _scrape_case_prices(self):
|
|
280
|
+
"""
|
|
281
|
+
Scrape prices for all cases defined in the configuration.
|
|
282
|
+
|
|
283
|
+
For each case, it prints the case name, owned count, price per item, and total
|
|
284
|
+
price for owned items.
|
|
285
|
+
"""
|
|
286
|
+
case_price_total = 0
|
|
287
|
+
for case_index, (config_case_name, owned) in enumerate(self.config.items("Cases")):
|
|
288
|
+
if int(owned) == 0:
|
|
289
|
+
continue
|
|
290
|
+
|
|
291
|
+
case_name = config_case_name.replace("_", " ").title()
|
|
292
|
+
case_title = case_name.center(MAX_LINE_LEN, SEPARATOR)
|
|
293
|
+
self.console.print(f"[bold magenta]{case_title}")
|
|
294
|
+
|
|
295
|
+
case_page = self._get_page(CASE_PAGES[case_index])
|
|
296
|
+
price_usd = self._parse_case_price(case_page, CASE_HREFS[case_index])
|
|
297
|
+
price_usd_owned = round(float(int(owned) * price_usd), 2)
|
|
298
|
+
|
|
299
|
+
self.console.print(PRICE_INFO.format(owned, price_usd, price_usd_owned))
|
|
300
|
+
case_price_total += price_usd_owned
|
|
301
|
+
|
|
302
|
+
if not self.config.getboolean("Settings", "Use_Proxy", fallback=False):
|
|
303
|
+
time.sleep(1)
|
|
304
|
+
|
|
305
|
+
return case_price_total
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cs2tracker
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.2
|
|
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
|
|
@@ -11,6 +11,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
11
11
|
Requires-Python: >=3.11
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE.md
|
|
14
|
+
Requires-Dist: numpy==1.26.4
|
|
14
15
|
Requires-Dist: beautifulsoup4==4.11.1
|
|
15
16
|
Requires-Dist: CurrencyConverter==0.17.9
|
|
16
17
|
Requires-Dist: matplotlib==3.7.0
|
|
@@ -20,16 +21,21 @@ Requires-Dist: tenacity==8.2.2
|
|
|
20
21
|
Requires-Dist: urllib3==2.1.0
|
|
21
22
|
Dynamic: license-file
|
|
22
23
|
|
|
24
|
+
<div align="center">
|
|
25
|
+
|
|
23
26
|
[](https://opensource.org/licenses/MIT)
|
|
27
|
+
[](https://github.com/ashiven/cs2tracker/releases)
|
|
24
28
|
[](https://badge.fury.io/py/cs2tracker)
|
|
29
|
+
[](https://github.com/ashiven/cs2tracker/issues)
|
|
30
|
+
[](https://github.com/ashiven/cs2tracker/pulls)
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
</div>
|
|
25
34
|
|
|
26
35
|
## About
|
|
27
36
|
|
|
28
37
|
**CS2Tracker** is a tool that can be used to keep track of the steam market prices of your CS2 investment.
|
|
29
38
|
|
|
30
|
-

|
|
31
|
-
|
|
32
|
-
|
|
33
39
|
## Getting Started
|
|
34
40
|
|
|
35
41
|
### Prerequisites
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
cs2tracker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
cs2tracker/__main__.py,sha256=Ub--oSMv48YzfWF1CZqYlkn1-HvZ7Bhxoc7urn1oY6o,249
|
|
3
|
+
cs2tracker/_version.py,sha256=R_mUEqlkeWeuDiw8DRbHmYUvRxDtmQQJqhTpV8pds-Y,511
|
|
4
|
+
cs2tracker/application.py,sha256=8nPUPcPOnatg98_XfSpWjD06-V0jDoy7CT_DNSGuszk,3431
|
|
5
|
+
cs2tracker/constants.py,sha256=8jqaMM9eKEmqNTGWQ_h9EiSqYTIEPOAhe5l1zg2ycko,14953
|
|
6
|
+
cs2tracker/main.py,sha256=T4X-oLYfltnu0ALBTIJagZTLFmWMhxZrfD1pfFLAUpg,1310
|
|
7
|
+
cs2tracker/scraper.py,sha256=XiRjYSLkBd_DFjJ-1951D_3iIaL5jDMMRWCqgWGBMJ0,12201
|
|
8
|
+
cs2tracker/data/config.ini,sha256=zieID1dhlEAquPCXbpmvsnvxJAnrDqHSNrS6AIj1F-Q,2637
|
|
9
|
+
cs2tracker-2.1.2.dist-info/licenses/LICENSE.md,sha256=G5wqQ_8KGA808kVuF-Fpu_Yhteg8K_5ux9n2v8eQK7s,1069
|
|
10
|
+
cs2tracker-2.1.2.dist-info/METADATA,sha256=bS8LgXrOUv0_t84kiUfU6Vybyoq3U8Q4tIqeeKJMMrs,2781
|
|
11
|
+
cs2tracker-2.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
+
cs2tracker-2.1.2.dist-info/entry_points.txt,sha256=K8IwDIkg8QztSB9g9c89B9jR_2pG4QyJGrNs4z5RcZw,63
|
|
13
|
+
cs2tracker-2.1.2.dist-info/top_level.txt,sha256=2HB4xDDOxaU5BDc_yvdi9UlYLgL768n8aR-hRhFM6VQ,11
|
|
14
|
+
cs2tracker-2.1.2.dist-info/RECORD,,
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
cs2tracker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
cs2tracker/__main__.py,sha256=gvsnabJSDMbzKEcB7i3ET7AoV5OqYwUjIrkLfmN0R-U,111
|
|
3
|
-
cs2tracker/_version.py,sha256=e93DN6a88mHp4XiW8PUMNpgcL72dYKhIYIiM2vzzNTM,511
|
|
4
|
-
cs2tracker/application.py,sha256=sOOsZxigUSkjoAqwhUKcSw5IiZXrXoheYJmhvXJ5gZ8,3739
|
|
5
|
-
cs2tracker/constants.py,sha256=GmCiwmzZblQT8mTXvmuQX-Bho7isBQTsRw5YmETvh_E,14881
|
|
6
|
-
cs2tracker/main.py,sha256=aB-xglyrrlKwx-WSrsw5YvZUbK5cVKIQ_WuhCJJy3Eo,1085
|
|
7
|
-
cs2tracker/scraper.py,sha256=W5GeqlA0XTGc3uNvzqxYjkNVbyJVNPiOAWXsIBN70bQ,13184
|
|
8
|
-
cs2tracker/data/config.ini,sha256=j0aXl-MkZILsJ0cj6w-6b5HbqLuLl73cewVpk204Ick,2514
|
|
9
|
-
cs2tracker-2.1.1.dist-info/licenses/LICENSE.md,sha256=G5wqQ_8KGA808kVuF-Fpu_Yhteg8K_5ux9n2v8eQK7s,1069
|
|
10
|
-
cs2tracker-2.1.1.dist-info/METADATA,sha256=6MLVDmUHwe7yBJcuv6eWvLCXMihmg7yMUEojiqkDZNo,2328
|
|
11
|
-
cs2tracker-2.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
-
cs2tracker-2.1.1.dist-info/entry_points.txt,sha256=K8IwDIkg8QztSB9g9c89B9jR_2pG4QyJGrNs4z5RcZw,63
|
|
13
|
-
cs2tracker-2.1.1.dist-info/top_level.txt,sha256=2HB4xDDOxaU5BDc_yvdi9UlYLgL768n8aR-hRhFM6VQ,11
|
|
14
|
-
cs2tracker-2.1.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|