cs2tracker 2.1.4__py3-none-any.whl → 2.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cs2tracker might be problematic. Click here for more details.
- cs2tracker/_version.py +2 -2
- cs2tracker/application.py +140 -50
- cs2tracker/constants.py +63 -8
- cs2tracker/data/config.ini +7 -7
- cs2tracker/data/output.csv +0 -0
- cs2tracker/main.py +13 -22
- cs2tracker/padded_console.py +22 -0
- cs2tracker/scraper.py +50 -31
- {cs2tracker-2.1.4.dist-info → cs2tracker-2.1.5.dist-info}/METADATA +14 -8
- cs2tracker-2.1.5.dist-info/RECORD +16 -0
- cs2tracker-2.1.4.dist-info/RECORD +0 -14
- {cs2tracker-2.1.4.dist-info → cs2tracker-2.1.5.dist-info}/WHEEL +0 -0
- {cs2tracker-2.1.4.dist-info → cs2tracker-2.1.5.dist-info}/entry_points.txt +0 -0
- {cs2tracker-2.1.4.dist-info → cs2tracker-2.1.5.dist-info}/licenses/LICENSE.md +0 -0
- {cs2tracker-2.1.4.dist-info → cs2tracker-2.1.5.dist-info}/top_level.txt +0 -0
cs2tracker/_version.py
CHANGED
cs2tracker/application.py
CHANGED
|
@@ -1,15 +1,37 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import subprocess
|
|
3
1
|
import tkinter as tk
|
|
2
|
+
from subprocess import Popen
|
|
3
|
+
from threading import Thread
|
|
4
4
|
from typing import cast
|
|
5
5
|
|
|
6
6
|
import matplotlib.pyplot as plt
|
|
7
7
|
from matplotlib.axes import Axes
|
|
8
8
|
from matplotlib.dates import DateFormatter
|
|
9
9
|
|
|
10
|
-
from cs2tracker.constants import
|
|
10
|
+
from cs2tracker.constants import (
|
|
11
|
+
CONFIG_FILE,
|
|
12
|
+
OS,
|
|
13
|
+
OUTPUT_FILE,
|
|
14
|
+
PYTHON_EXECUTABLE,
|
|
15
|
+
RUNNING_IN_EXE,
|
|
16
|
+
TEXT_EDITOR,
|
|
17
|
+
OSType,
|
|
18
|
+
)
|
|
11
19
|
from cs2tracker.scraper import Scraper
|
|
12
20
|
|
|
21
|
+
WINDOW_TITLE = "CS2Tracker"
|
|
22
|
+
WINDOW_SIZE = "450x380"
|
|
23
|
+
BACKGROUND_COLOR = "#1e1e1e"
|
|
24
|
+
BUTTON_COLOR = "#3c3f41"
|
|
25
|
+
BUTTON_HOVER_COLOR = "#505354"
|
|
26
|
+
BUTTON_ACTIVE_COLOR = "#5c5f61"
|
|
27
|
+
FONT_STYLE = "Segoe UI"
|
|
28
|
+
FONT_COLOR = "white"
|
|
29
|
+
|
|
30
|
+
SCRAPER_WINDOW_TITLE = "CS2Tracker"
|
|
31
|
+
SCRAPER_WINDOW_HEIGHT = 40
|
|
32
|
+
SCRAPER_WINDOW_WIDTH = 80
|
|
33
|
+
SCRAPER_WINDOW_BACKGROUND_COLOR = "Black"
|
|
34
|
+
|
|
13
35
|
|
|
14
36
|
class Application:
|
|
15
37
|
def __init__(self):
|
|
@@ -22,88 +44,156 @@ class Application:
|
|
|
22
44
|
application_window = self._configure_window()
|
|
23
45
|
application_window.mainloop()
|
|
24
46
|
|
|
47
|
+
def _add_button(self, frame, text, command):
|
|
48
|
+
"""Create and style a button for the main application window."""
|
|
49
|
+
button_style = {
|
|
50
|
+
"font": (FONT_STYLE, 12),
|
|
51
|
+
"fg": FONT_COLOR,
|
|
52
|
+
"bg": BUTTON_COLOR,
|
|
53
|
+
"activebackground": BUTTON_ACTIVE_COLOR,
|
|
54
|
+
}
|
|
55
|
+
button = tk.Button(frame, text=text, command=command, **button_style)
|
|
56
|
+
button.pack(pady=5, fill="x")
|
|
57
|
+
button.bind("<Enter>", lambda _: button.config(bg=BUTTON_HOVER_COLOR))
|
|
58
|
+
button.bind("<Leave>", lambda _: button.config(bg=BUTTON_COLOR))
|
|
59
|
+
return button
|
|
60
|
+
|
|
25
61
|
def _configure_window(self):
|
|
26
|
-
"""Configure the main application window
|
|
27
|
-
|
|
62
|
+
"""Configure the main application window UI and add buttons for the main
|
|
63
|
+
functionalities.
|
|
28
64
|
"""
|
|
29
65
|
window = tk.Tk()
|
|
30
|
-
window.title(
|
|
31
|
-
window.geometry(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
66
|
+
window.title(WINDOW_TITLE)
|
|
67
|
+
window.geometry(WINDOW_SIZE)
|
|
68
|
+
window.configure(bg=BACKGROUND_COLOR)
|
|
69
|
+
|
|
70
|
+
frame = tk.Frame(window, bg=BACKGROUND_COLOR, padx=30, pady=30)
|
|
71
|
+
frame.pack(expand=True, fill="both")
|
|
72
|
+
|
|
73
|
+
label = tk.Label(
|
|
74
|
+
frame,
|
|
75
|
+
text=f"Welcome to {WINDOW_TITLE}!",
|
|
76
|
+
font=(FONT_STYLE, 16, "bold"),
|
|
77
|
+
fg=FONT_COLOR,
|
|
78
|
+
bg=BACKGROUND_COLOR,
|
|
39
79
|
)
|
|
80
|
+
label.pack(pady=(0, 30))
|
|
81
|
+
|
|
82
|
+
self._add_button(frame, "Run!", self.scrape_prices)
|
|
83
|
+
self._add_button(frame, "Edit Config", self._edit_config)
|
|
84
|
+
self._add_button(frame, "Show History (Chart)", self._draw_plot)
|
|
85
|
+
self._add_button(frame, "Show History (File)", self._edit_log_file)
|
|
86
|
+
|
|
40
87
|
background_checkbox_value = tk.BooleanVar(value=self.scraper.identify_background_task())
|
|
41
88
|
background_checkbox = tk.Checkbutton(
|
|
42
|
-
|
|
89
|
+
frame,
|
|
43
90
|
text="Daily Background Calculation",
|
|
44
|
-
command=lambda: self._toggle_background_task(background_checkbox_value.get()),
|
|
45
91
|
variable=background_checkbox_value,
|
|
92
|
+
command=lambda: self._toggle_background_task(background_checkbox_value.get()),
|
|
93
|
+
bg=BACKGROUND_COLOR,
|
|
94
|
+
fg=FONT_COLOR,
|
|
95
|
+
selectcolor=BUTTON_COLOR,
|
|
96
|
+
activebackground=BACKGROUND_COLOR,
|
|
97
|
+
font=(FONT_STYLE, 10),
|
|
46
98
|
)
|
|
47
|
-
|
|
48
|
-
label.grid(row=0, column=0, pady=50, sticky="NSEW")
|
|
49
|
-
run_button.grid(row=1, column=0, pady=10, sticky="NSEW")
|
|
50
|
-
edit_button.grid(row=2, column=0, pady=10, sticky="NSEW")
|
|
51
|
-
plot_button.grid(row=3, column=0, pady=10, sticky="NSEW")
|
|
52
|
-
plot_file_button.grid(row=4, column=0, pady=10, sticky="NSEW")
|
|
53
|
-
background_checkbox.grid(row=5, column=0, pady=10, sticky="NS")
|
|
54
|
-
|
|
55
|
-
window.grid_columnconfigure(0, weight=1)
|
|
56
|
-
window.grid_rowconfigure(1, weight=1)
|
|
57
|
-
window.grid_rowconfigure(2, weight=1)
|
|
58
|
-
window.grid_rowconfigure(3, weight=1)
|
|
59
|
-
window.grid_rowconfigure(4, weight=1)
|
|
60
|
-
window.grid_rowconfigure(5, weight=1)
|
|
61
|
-
|
|
62
|
-
label.grid_configure(sticky="NSEW")
|
|
63
|
-
run_button.grid_configure(sticky="NSEW")
|
|
64
|
-
edit_button.grid_configure(sticky="NSEW")
|
|
65
|
-
plot_button.grid_configure(sticky="NSEW")
|
|
66
|
-
plot_file_button.grid_configure(sticky="NSEW")
|
|
67
|
-
background_checkbox.grid_configure(sticky="NSEW")
|
|
99
|
+
background_checkbox.pack(pady=20)
|
|
68
100
|
|
|
69
101
|
return window
|
|
70
102
|
|
|
71
|
-
def
|
|
103
|
+
def _construct_scraper_command(self):
|
|
104
|
+
"""Construct the command to run the scraper in a new window."""
|
|
105
|
+
if OS == OSType.WINDOWS:
|
|
106
|
+
set_utf8_encoding = "[Console]::InputEncoding = [Console]::OutputEncoding = [System.Text.Encoding]::UTF8;"
|
|
107
|
+
get_size = "$size = $Host.UI.RawUI.WindowSize;"
|
|
108
|
+
set_size = "$Host.UI.RawUI.WindowSize = $size;"
|
|
109
|
+
set_window_title = f"$Host.UI.RawUI.WindowTitle = '{SCRAPER_WINDOW_TITLE}';"
|
|
110
|
+
set_window_width = f"$size.Width = {SCRAPER_WINDOW_WIDTH};"
|
|
111
|
+
set_window_height = f"$size.Height = {SCRAPER_WINDOW_HEIGHT};"
|
|
112
|
+
set_background_color = (
|
|
113
|
+
f"$Host.UI.RawUI.BackgroundColor = '{SCRAPER_WINDOW_BACKGROUND_COLOR}';"
|
|
114
|
+
)
|
|
115
|
+
clear = "Clear-Host;"
|
|
116
|
+
|
|
117
|
+
if RUNNING_IN_EXE:
|
|
118
|
+
# The python executable is set as the executable itself in PyInstaller
|
|
119
|
+
scraper_cmd = f"{PYTHON_EXECUTABLE} --only-scrape | Out-Host -Paging"
|
|
120
|
+
else:
|
|
121
|
+
scraper_cmd = f"{PYTHON_EXECUTABLE} -m cs2tracker --only-scrape"
|
|
122
|
+
|
|
123
|
+
cmd = (
|
|
124
|
+
'start powershell -NoExit -Command "& {'
|
|
125
|
+
+ set_utf8_encoding
|
|
126
|
+
+ set_window_title
|
|
127
|
+
+ get_size
|
|
128
|
+
+ set_window_width
|
|
129
|
+
+ set_window_height
|
|
130
|
+
+ set_size
|
|
131
|
+
+ set_background_color
|
|
132
|
+
+ clear
|
|
133
|
+
+ scraper_cmd
|
|
134
|
+
+ '}"'
|
|
135
|
+
)
|
|
136
|
+
return cmd
|
|
137
|
+
else:
|
|
138
|
+
# TODO: Implement for Linux
|
|
139
|
+
return ""
|
|
140
|
+
|
|
141
|
+
def scrape_prices(self):
|
|
72
142
|
"""Scrape prices from the configured sources, print the total, and save the
|
|
73
143
|
results to a file.
|
|
74
144
|
"""
|
|
75
|
-
|
|
145
|
+
if OS == OSType.WINDOWS:
|
|
146
|
+
scraper_cmd = self._construct_scraper_command()
|
|
147
|
+
Popen(scraper_cmd, shell=True)
|
|
148
|
+
else:
|
|
149
|
+
# TODO: implement external window for Linux
|
|
150
|
+
self.scraper.scrape_prices()
|
|
76
151
|
|
|
77
152
|
def _edit_config(self):
|
|
78
153
|
"""Edit the configuration file using the specified text editor."""
|
|
79
|
-
|
|
80
|
-
|
|
154
|
+
_popen_and_call(
|
|
155
|
+
popen_args={"args": [TEXT_EDITOR, CONFIG_FILE], "shell": True},
|
|
156
|
+
callback=self.scraper.parse_config,
|
|
157
|
+
)
|
|
81
158
|
|
|
82
159
|
def _draw_plot(self):
|
|
83
160
|
"""Draw a plot of the scraped prices over time."""
|
|
84
161
|
dates, dollars, euros = self.scraper.read_price_log()
|
|
85
162
|
|
|
86
|
-
fig, ax_raw = plt.subplots()
|
|
87
|
-
|
|
163
|
+
fig, ax_raw = plt.subplots(figsize=(10, 8), num="CS2Tracker Price History")
|
|
164
|
+
fig.suptitle("CS2Tracker Price History", fontsize=16)
|
|
165
|
+
fig.autofmt_xdate()
|
|
88
166
|
|
|
167
|
+
ax = cast(Axes, ax_raw)
|
|
89
168
|
ax.plot(dates, dollars, label="Dollars")
|
|
90
169
|
ax.plot(dates, euros, label="Euros")
|
|
91
|
-
ax.set_xlabel("Date")
|
|
92
|
-
ax.set_ylabel("Price")
|
|
93
170
|
ax.legend()
|
|
94
|
-
|
|
95
171
|
date_formatter = DateFormatter("%d-%m-%Y")
|
|
96
172
|
ax.xaxis.set_major_formatter(date_formatter)
|
|
97
|
-
fig.autofmt_xdate()
|
|
98
173
|
|
|
99
174
|
plt.show()
|
|
100
175
|
|
|
101
176
|
def _edit_log_file(self):
|
|
102
177
|
"""Opens the file containing past price calculations."""
|
|
103
|
-
|
|
104
|
-
open(OUTPUT_FILE, "w", encoding="utf-8").close()
|
|
105
|
-
subprocess.call([TEXT_EDITOR, OUTPUT_FILE])
|
|
178
|
+
Popen([TEXT_EDITOR, OUTPUT_FILE], shell=True)
|
|
106
179
|
|
|
107
180
|
def _toggle_background_task(self, enabled: bool):
|
|
108
181
|
"""Toggle whether a daily price calculation should run in the background."""
|
|
109
182
|
self.scraper.toggle_background_task(enabled)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _popen_and_call(popen_args, callback):
|
|
186
|
+
"""
|
|
187
|
+
Runs the given args in a subprocess.Popen, and then calls the function callback when
|
|
188
|
+
the subprocess completes.
|
|
189
|
+
|
|
190
|
+
Source: https://stackoverflow.com/questions/2581817/python-subprocess-callback-when-cmd-exits
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
def process_and_callback(popen_args, callback):
|
|
194
|
+
process = Popen(**popen_args)
|
|
195
|
+
process.wait()
|
|
196
|
+
callback()
|
|
197
|
+
|
|
198
|
+
thread = Thread(target=process_and_callback, args=(popen_args, callback), daemon=True)
|
|
199
|
+
thread.start()
|
cs2tracker/constants.py
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
|
+
import enum
|
|
1
2
|
import os
|
|
2
3
|
import sys
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from shutil import copy
|
|
3
6
|
|
|
4
|
-
|
|
7
|
+
try:
|
|
8
|
+
from cs2tracker._version import version # pylint: disable=E0611
|
|
9
|
+
except ImportError:
|
|
10
|
+
version = "0.0.0"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OSType(enum.Enum):
|
|
14
|
+
WINDOWS = "Windows"
|
|
15
|
+
LINUX = "Linux"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
OS = OSType.WINDOWS if sys.platform.startswith("win") else OSType.LINUX
|
|
19
|
+
TEXT_EDITOR = "notepad" if OS == OSType.WINDOWS else "nano"
|
|
5
20
|
PYTHON_EXECUTABLE = sys.executable
|
|
21
|
+
|
|
22
|
+
|
|
6
23
|
MODULE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
7
24
|
PROJECT_DIR = os.path.dirname(MODULE_DIR)
|
|
8
25
|
OUTPUT_FILE = os.path.join(MODULE_DIR, "data", "output.csv")
|
|
@@ -10,6 +27,44 @@ CONFIG_FILE = os.path.join(MODULE_DIR, "data", "config.ini")
|
|
|
10
27
|
BATCH_FILE = os.path.join(MODULE_DIR, "data", "cs2tracker_scraper.bat")
|
|
11
28
|
|
|
12
29
|
|
|
30
|
+
RUNNING_IN_EXE = getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS")
|
|
31
|
+
|
|
32
|
+
if RUNNING_IN_EXE:
|
|
33
|
+
MEIPASS_DIR = sys._MEIPASS # type: ignore pylint: disable=protected-access
|
|
34
|
+
MODULE_DIR = MEIPASS_DIR
|
|
35
|
+
PROJECT_DIR = MEIPASS_DIR
|
|
36
|
+
CONFIG_FILE_SOURCE = os.path.join(MODULE_DIR, "data", "config.ini")
|
|
37
|
+
OUTPUT_FILE_SOURCE = os.path.join(MODULE_DIR, "data", "output.csv")
|
|
38
|
+
|
|
39
|
+
APP_DATA_DIR = os.path.join(os.path.expanduser("~"), "AppData", "Local")
|
|
40
|
+
DATA_DIR = os.path.join(APP_DATA_DIR, "cs2tracker", "data")
|
|
41
|
+
os.makedirs(DATA_DIR, exist_ok=True)
|
|
42
|
+
|
|
43
|
+
CONFIG_FILE = os.path.join(DATA_DIR, "config.ini")
|
|
44
|
+
OUTPUT_FILE = os.path.join(DATA_DIR, "output.csv")
|
|
45
|
+
BATCH_FILE = os.path.join(DATA_DIR, "cs2tracker_scraper.bat")
|
|
46
|
+
if not os.path.exists(CONFIG_FILE):
|
|
47
|
+
copy(CONFIG_FILE_SOURCE, CONFIG_FILE)
|
|
48
|
+
if not os.path.exists(OUTPUT_FILE):
|
|
49
|
+
copy(OUTPUT_FILE_SOURCE, OUTPUT_FILE)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
BANNER = """
|
|
53
|
+
__ _____ _____ ______ ____ ____ __ __ _ ___ ____
|
|
54
|
+
/ ] / ___/| T| T| \\ / T / ]| l/ ] / _]| \\
|
|
55
|
+
/ / ( \\_ l__/ || || D )Y o | / / | ' / / [_ | D )
|
|
56
|
+
/ / \\__ T| __jl_j l_j| / | | / / | \\ Y _]| /
|
|
57
|
+
/ \\_ / \\ || / | | | | \\ | _ |/ \\_ | Y| [_ | \\
|
|
58
|
+
\\ | \\ || | | | | . Y| | |\\ || . || T| . Y
|
|
59
|
+
\\____j \\___jl_____j l__j l__j\\_jl__j__j \\____jl__j\\_jl_____jl__j\\_j
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
AUTHOR_STRING = (
|
|
64
|
+
f"Version: v{version} - {datetime.today().strftime('%Y/%m/%d')} - Jannik Novak @ashiven\n"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
13
68
|
RMR_CAPSULES = {
|
|
14
69
|
"page": "https://steamcommunity.com/market/search?q=2020+rmr",
|
|
15
70
|
"items": [
|
|
@@ -172,13 +227,13 @@ AUSTIN_CAPSULES = {
|
|
|
172
227
|
|
|
173
228
|
CAPSULE_INFO = {
|
|
174
229
|
"2020 RMR Sticker Capsule": RMR_CAPSULES,
|
|
175
|
-
"Stockholm Sticker Capsule": STOCKHOLM_CAPSULES,
|
|
176
|
-
"Antwerp Sticker Capsule": ANTWERP_CAPSULES,
|
|
177
|
-
"Rio Sticker Capsule": RIO_CAPSULES,
|
|
178
|
-
"Paris Sticker Capsule": PARIS_CAPSULES,
|
|
179
|
-
"Copenhagen Sticker Capsule": COPENHAGEN_CAPSULES,
|
|
180
|
-
"Shanghai Sticker Capsule": SHANGHAI_CAPSULES,
|
|
181
|
-
"Austin Sticker Capsule": AUSTIN_CAPSULES,
|
|
230
|
+
"Stockholm 2021 Sticker Capsule": STOCKHOLM_CAPSULES,
|
|
231
|
+
"Antwerp 2022 Sticker Capsule": ANTWERP_CAPSULES,
|
|
232
|
+
"Rio 2022 Sticker Capsule": RIO_CAPSULES,
|
|
233
|
+
"Paris 2023 Sticker Capsule": PARIS_CAPSULES,
|
|
234
|
+
"Copenhagen 2024 Sticker Capsule": COPENHAGEN_CAPSULES,
|
|
235
|
+
"Shanghai 2024 Sticker Capsule": SHANGHAI_CAPSULES,
|
|
236
|
+
"Austin 2025 Sticker Capsule": AUSTIN_CAPSULES,
|
|
182
237
|
}
|
|
183
238
|
|
|
184
239
|
|
cs2tracker/data/config.ini
CHANGED
|
@@ -7,14 +7,14 @@ RMR_Challengers = 0
|
|
|
7
7
|
RMR_Legends = 0
|
|
8
8
|
RMR_Contenders = 0
|
|
9
9
|
|
|
10
|
-
[Stockholm Sticker Capsule]
|
|
10
|
+
[Stockholm 2021 Sticker Capsule]
|
|
11
11
|
Stockholm_Challengers = 0
|
|
12
12
|
Stockholm_Legends = 0
|
|
13
13
|
Stockholm_Contenders = 0
|
|
14
14
|
Stockholm_Champions_Autographs = 0
|
|
15
15
|
Stockholm_Finalists_Autographs = 0
|
|
16
16
|
|
|
17
|
-
[Antwerp Sticker Capsule]
|
|
17
|
+
[Antwerp 2022 Sticker Capsule]
|
|
18
18
|
Antwerp_Challengers = 0
|
|
19
19
|
Antwerp_Legends = 0
|
|
20
20
|
Antwerp_Contenders = 0
|
|
@@ -23,7 +23,7 @@ Antwerp_Contenders_Autographs = 0
|
|
|
23
23
|
Antwerp_Challengers_Autographs = 0
|
|
24
24
|
Antwerp_Legends_Autographs = 0
|
|
25
25
|
|
|
26
|
-
[Rio Sticker Capsule]
|
|
26
|
+
[Rio 2022 Sticker Capsule]
|
|
27
27
|
Rio_Challengers = 0
|
|
28
28
|
Rio_Legends = 0
|
|
29
29
|
Rio_Contenders = 0
|
|
@@ -32,7 +32,7 @@ Rio_Contenders_Autographs = 0
|
|
|
32
32
|
Rio_Challengers_Autographs = 0
|
|
33
33
|
Rio_Legends_Autographs = 0
|
|
34
34
|
|
|
35
|
-
[Paris Sticker Capsule]
|
|
35
|
+
[Paris 2023 Sticker Capsule]
|
|
36
36
|
Paris_Challengers = 0
|
|
37
37
|
Paris_Legends = 0
|
|
38
38
|
Paris_Contenders = 0
|
|
@@ -41,7 +41,7 @@ Paris_Contenders_Autographs = 0
|
|
|
41
41
|
Paris_Challengers_Autographs = 0
|
|
42
42
|
Paris_Legends_Autographs = 0
|
|
43
43
|
|
|
44
|
-
[Copenhagen Sticker Capsule]
|
|
44
|
+
[Copenhagen 2024 Sticker Capsule]
|
|
45
45
|
Copenhagen_Challengers = 0
|
|
46
46
|
Copenhagen_Legends = 0
|
|
47
47
|
Copenhagen_Contenders = 0
|
|
@@ -50,7 +50,7 @@ Copenhagen_Contenders_Autographs = 0
|
|
|
50
50
|
Copenhagen_Challengers_Autographs = 0
|
|
51
51
|
Copenhagen_Legends_Autographs = 0
|
|
52
52
|
|
|
53
|
-
[Shanghai Sticker Capsule]
|
|
53
|
+
[Shanghai 2024 Sticker Capsule]
|
|
54
54
|
Shanghai_Challengers = 0
|
|
55
55
|
Shanghai_Legends = 0
|
|
56
56
|
Shanghai_Contenders = 0
|
|
@@ -59,7 +59,7 @@ Shanghai_Contenders_Autographs = 0
|
|
|
59
59
|
Shanghai_Challengers_Autographs = 0
|
|
60
60
|
Shanghai_Legends_Autographs = 0
|
|
61
61
|
|
|
62
|
-
[Austin Sticker Capsule]
|
|
62
|
+
[Austin 2025 Sticker Capsule]
|
|
63
63
|
Austin_Challengers = 0
|
|
64
64
|
Austin_Legends = 0
|
|
65
65
|
Austin_Contenders = 0
|
|
File without changes
|
cs2tracker/main.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
import sys
|
|
2
2
|
|
|
3
3
|
import urllib3
|
|
4
|
-
from rich.console import Console
|
|
5
4
|
|
|
6
|
-
from cs2tracker._version import version # pylint: disable=E0611
|
|
7
5
|
from cs2tracker.application import Application
|
|
6
|
+
from cs2tracker.constants import AUTHOR_STRING, BANNER
|
|
7
|
+
from cs2tracker.padded_console import PaddedConsole
|
|
8
|
+
from cs2tracker.scraper import Scraper
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def main():
|
|
@@ -18,25 +19,15 @@ def main():
|
|
|
18
19
|
# Disable warnings for proxy requests
|
|
19
20
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"[bold yellow]"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
\\ | \\ || | | | | . Y| | |\\ || . || T| . Y
|
|
31
|
-
\\____j \\___jl_____j l__j l__j\\_jl__j__j \\____jl__j\\_jl_____jl__j\\_j
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"""
|
|
35
|
-
+ f"Version: v{version} - {datetime.today().strftime('%Y/%m/%d')} - Jannik Novak @ashiven\n"
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
application = Application()
|
|
39
|
-
application.run()
|
|
22
|
+
if "--only-scrape" in sys.argv:
|
|
23
|
+
scraper = Scraper()
|
|
24
|
+
scraper.console.print(f"[bold yellow]{BANNER}\n{AUTHOR_STRING}\n")
|
|
25
|
+
scraper.scrape_prices()
|
|
26
|
+
else:
|
|
27
|
+
console = PaddedConsole()
|
|
28
|
+
console.print(f"[bold yellow]{BANNER}\n{AUTHOR_STRING}\n")
|
|
29
|
+
application = Application()
|
|
30
|
+
application.run()
|
|
40
31
|
|
|
41
32
|
|
|
42
33
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from rich.console import Console
|
|
2
|
+
from rich.padding import Padding
|
|
3
|
+
|
|
4
|
+
PADDING_BOTTOM = 0
|
|
5
|
+
PADDING_TOP = 0
|
|
6
|
+
PADDING_LEFT = 4
|
|
7
|
+
PADDING_RIGHT = 0
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PaddedConsole:
|
|
11
|
+
def __init__(self, padding=(PADDING_TOP, PADDING_RIGHT, PADDING_BOTTOM, PADDING_LEFT)):
|
|
12
|
+
"""Initialize a PaddedConsole with specified padding."""
|
|
13
|
+
self.console = Console()
|
|
14
|
+
self.padding = padding
|
|
15
|
+
|
|
16
|
+
def print(self, text):
|
|
17
|
+
"""Print text with padding to the console."""
|
|
18
|
+
self.console.print(Padding(text, self.padding))
|
|
19
|
+
|
|
20
|
+
def __getattr__(self, attr):
|
|
21
|
+
"""Ensure console methods can be called directly on PaddedConsole."""
|
|
22
|
+
return getattr(self.console, attr)
|
cs2tracker/scraper.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import csv
|
|
2
2
|
import os
|
|
3
|
-
import sys
|
|
4
3
|
import time
|
|
5
4
|
from configparser import ConfigParser
|
|
6
5
|
from datetime import datetime
|
|
@@ -11,24 +10,32 @@ from bs4.element import Tag
|
|
|
11
10
|
from currency_converter import CurrencyConverter
|
|
12
11
|
from requests import RequestException, Session
|
|
13
12
|
from requests.adapters import HTTPAdapter, Retry
|
|
14
|
-
from rich.console import Console
|
|
15
13
|
from tenacity import RetryError, retry, stop_after_attempt
|
|
16
14
|
|
|
17
15
|
from cs2tracker.constants import (
|
|
16
|
+
AUTHOR_STRING,
|
|
17
|
+
BANNER,
|
|
18
18
|
BATCH_FILE,
|
|
19
19
|
CAPSULE_INFO,
|
|
20
20
|
CASE_HREFS,
|
|
21
21
|
CASE_PAGES,
|
|
22
22
|
CONFIG_FILE,
|
|
23
|
+
OS,
|
|
23
24
|
OUTPUT_FILE,
|
|
24
25
|
PROJECT_DIR,
|
|
25
26
|
PYTHON_EXECUTABLE,
|
|
27
|
+
RUNNING_IN_EXE,
|
|
28
|
+
OSType,
|
|
26
29
|
)
|
|
30
|
+
from cs2tracker.padded_console import PaddedConsole
|
|
27
31
|
|
|
28
32
|
MAX_LINE_LEN = 72
|
|
29
33
|
SEPARATOR = "-"
|
|
30
34
|
PRICE_INFO = "Owned: {} Steam market price: ${} Total: ${}\n"
|
|
31
35
|
|
|
36
|
+
HTTP_PROXY_URL = "http://{}:@smartproxy.crawlbase.com:8012"
|
|
37
|
+
HTTPS_PROXY_URL = "http://{}:@smartproxy.crawlbase.com:8012"
|
|
38
|
+
|
|
32
39
|
WIN_BACKGROUND_TASK_NAME = "CS2Tracker Daily Calculation"
|
|
33
40
|
WIN_BACKGROUND_TASK_SCHEDULE = "DAILY"
|
|
34
41
|
WIN_BACKGROUND_TASK_TIME = "12:00"
|
|
@@ -40,7 +47,7 @@ WIN_BACKGROUND_TASK_CMD = (
|
|
|
40
47
|
class Scraper:
|
|
41
48
|
def __init__(self):
|
|
42
49
|
"""Initialize the Scraper class."""
|
|
43
|
-
self.console =
|
|
50
|
+
self.console = PaddedConsole()
|
|
44
51
|
self.parse_config()
|
|
45
52
|
self._start_session()
|
|
46
53
|
|
|
@@ -116,21 +123,33 @@ class Scraper:
|
|
|
116
123
|
This will append a new entry to the output file if no entry has been made for
|
|
117
124
|
today.
|
|
118
125
|
"""
|
|
119
|
-
if not os.path.isfile(OUTPUT_FILE):
|
|
120
|
-
open(OUTPUT_FILE, "w", encoding="utf-8").close()
|
|
121
|
-
|
|
122
126
|
with open(OUTPUT_FILE, "r", encoding="utf-8") as price_logs:
|
|
123
127
|
price_logs_reader = csv.reader(price_logs)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
last_log_date, _ = row
|
|
128
|
+
rows = list(price_logs_reader)
|
|
129
|
+
last_log_date, _, _ = rows[-1] if rows else ("", "", "")
|
|
127
130
|
|
|
128
131
|
today = datetime.now().strftime("%Y-%m-%d")
|
|
129
132
|
if last_log_date != today:
|
|
133
|
+
# Append first price calculation of the day
|
|
130
134
|
with open(OUTPUT_FILE, "a", newline="", encoding="utf-8") as price_logs:
|
|
131
135
|
price_logs_writer = csv.writer(price_logs)
|
|
132
|
-
price_logs_writer.writerow(
|
|
133
|
-
|
|
136
|
+
price_logs_writer.writerow(
|
|
137
|
+
[today, f"{self.usd_total:.2f}$", f"{self.eur_total:.2f}€"]
|
|
138
|
+
)
|
|
139
|
+
else:
|
|
140
|
+
# Replace the last calculation of today with the most recent one of today
|
|
141
|
+
with open(OUTPUT_FILE, "r+", newline="", encoding="utf-8") as price_logs:
|
|
142
|
+
price_logs_reader = csv.reader(price_logs)
|
|
143
|
+
rows = list(price_logs_reader)
|
|
144
|
+
rows_without_today = rows[:-1]
|
|
145
|
+
price_logs.seek(0)
|
|
146
|
+
price_logs.truncate()
|
|
147
|
+
|
|
148
|
+
price_logs_writer = csv.writer(price_logs)
|
|
149
|
+
price_logs_writer.writerows(rows_without_today)
|
|
150
|
+
price_logs_writer.writerow(
|
|
151
|
+
[today, f"{self.usd_total:.2f}$", f"{self.eur_total:.2f}€"]
|
|
152
|
+
)
|
|
134
153
|
|
|
135
154
|
def read_price_log(self):
|
|
136
155
|
"""
|
|
@@ -139,23 +158,18 @@ class Scraper:
|
|
|
139
158
|
|
|
140
159
|
:return: A tuple containing three lists: dates, dollar prices, and euro prices.
|
|
141
160
|
"""
|
|
142
|
-
if not os.path.isfile(OUTPUT_FILE):
|
|
143
|
-
open(OUTPUT_FILE, "w", encoding="utf-8").close()
|
|
144
|
-
|
|
145
161
|
dates, dollars, euros = [], [], []
|
|
146
162
|
with open(OUTPUT_FILE, "r", encoding="utf-8") as price_logs:
|
|
147
163
|
price_logs_reader = csv.reader(price_logs)
|
|
148
164
|
for row in price_logs_reader:
|
|
149
|
-
date,
|
|
165
|
+
date, price_usd, price_eur = row
|
|
150
166
|
date = datetime.strptime(date, "%Y-%m-%d")
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
# and we want the length of dates to match the lengths of dollars and euros
|
|
158
|
-
dates.append(date)
|
|
167
|
+
price_usd = float(price_usd.rstrip("$"))
|
|
168
|
+
price_eur = float(price_eur.rstrip("€"))
|
|
169
|
+
|
|
170
|
+
dates.append(date)
|
|
171
|
+
dollars.append(price_usd)
|
|
172
|
+
euros.append(price_eur)
|
|
159
173
|
|
|
160
174
|
return dates, dollars, euros
|
|
161
175
|
|
|
@@ -172,12 +186,13 @@ class Scraper:
|
|
|
172
186
|
"""
|
|
173
187
|
use_proxy = self.config.getboolean("Settings", "Use_Proxy", fallback=False)
|
|
174
188
|
api_key = self.config.get("Settings", "API_Key", fallback=None)
|
|
189
|
+
api_key = None if api_key in ("None", "") else api_key
|
|
175
190
|
if use_proxy and api_key:
|
|
176
191
|
page = self.session.get(
|
|
177
192
|
url=url,
|
|
178
193
|
proxies={
|
|
179
|
-
"http":
|
|
180
|
-
"https":
|
|
194
|
+
"http": HTTP_PROXY_URL.format(api_key),
|
|
195
|
+
"https": HTTPS_PROXY_URL.format(api_key),
|
|
181
196
|
},
|
|
182
197
|
verify=False,
|
|
183
198
|
)
|
|
@@ -315,7 +330,7 @@ class Scraper:
|
|
|
315
330
|
|
|
316
331
|
:return: True if a background task is found, False otherwise.
|
|
317
332
|
"""
|
|
318
|
-
if
|
|
333
|
+
if OS == OSType.WINDOWS:
|
|
319
334
|
cmd = ["schtasks", "/query", "/tn", WIN_BACKGROUND_TASK_NAME]
|
|
320
335
|
return_code = call(cmd, stdout=DEVNULL, stderr=DEVNULL)
|
|
321
336
|
found = return_code == 0
|
|
@@ -333,8 +348,13 @@ class Scraper:
|
|
|
333
348
|
"""
|
|
334
349
|
if enabled:
|
|
335
350
|
with open(BATCH_FILE, "w", encoding="utf-8") as batch_file:
|
|
336
|
-
|
|
337
|
-
|
|
351
|
+
if RUNNING_IN_EXE:
|
|
352
|
+
# The python executable is set to the executable itself
|
|
353
|
+
# for executables created with PyInstaller
|
|
354
|
+
batch_file.write(f"{PYTHON_EXECUTABLE} --only-scrape\n")
|
|
355
|
+
else:
|
|
356
|
+
batch_file.write(f"cd {PROJECT_DIR}\n")
|
|
357
|
+
batch_file.write(f"{PYTHON_EXECUTABLE} -m cs2tracker --only-scrape\n")
|
|
338
358
|
else:
|
|
339
359
|
if os.path.exists(BATCH_FILE):
|
|
340
360
|
os.remove(BATCH_FILE)
|
|
@@ -380,7 +400,7 @@ class Scraper:
|
|
|
380
400
|
:param enabled: If True, the task will be created; if False, the task will be
|
|
381
401
|
deleted.
|
|
382
402
|
"""
|
|
383
|
-
if
|
|
403
|
+
if OS == OSType.WINDOWS:
|
|
384
404
|
self._toggle_background_task_windows(enabled)
|
|
385
405
|
else:
|
|
386
406
|
# TODO: implement toggle for cron jobs
|
|
@@ -388,7 +408,6 @@ class Scraper:
|
|
|
388
408
|
|
|
389
409
|
|
|
390
410
|
if __name__ == "__main__":
|
|
391
|
-
# If this file is run as a script, create a Scraper instance and run the
|
|
392
|
-
# scrape_prices method.
|
|
393
411
|
scraper = Scraper()
|
|
412
|
+
scraper.console.print(f"[bold yellow]{BANNER}\n{AUTHOR_STRING}\n")
|
|
394
413
|
scraper.scrape_prices()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cs2tracker
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.5
|
|
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
|
|
@@ -40,11 +40,17 @@ Dynamic: license-file
|
|
|
40
40
|
|
|
41
41
|
### Prerequisites
|
|
42
42
|
|
|
43
|
-
-
|
|
44
|
-
-
|
|
43
|
+
- Download and install the latest versions of [Python](https://www.python.org/downloads/) and [Pip](https://pypi.org/project/pip/). (Required for Linux)
|
|
44
|
+
- Register for the [Crawlbase Smart Proxy API](https://crawlbase.com/) and retrieve your API key. (Optional)
|
|
45
45
|
|
|
46
46
|
### Setup
|
|
47
47
|
|
|
48
|
+
#### Windows Executable (No color support)
|
|
49
|
+
|
|
50
|
+
- Simply download the [latest executable](https://github.com/ashiven/cs2tracker/releases) and run it.
|
|
51
|
+
|
|
52
|
+
#### Install via Pip
|
|
53
|
+
|
|
48
54
|
1. Install the program via pip:
|
|
49
55
|
|
|
50
56
|
```bash
|
|
@@ -58,11 +64,11 @@ Dynamic: license-file
|
|
|
58
64
|
|
|
59
65
|
### Options
|
|
60
66
|
|
|
61
|
-
-
|
|
62
|
-
-
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
67
|
+
- `Run!` to gather the current market prices of your items and calculate the total amount in USD and EUR.
|
|
68
|
+
- `Edit Config` to change the specific numbers of each item you own and then save the config file.
|
|
69
|
+
- `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
|
+
- `Daily Background Calculation` 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
|
+
- If you want to prevent your requests from being rate limited by the steamcommunity server, register for an API key on [Crawlbase](crawlbase.com) and enter it into the `API_Key` field in the config file. This will route every request through a different proxy server.
|
|
66
72
|
|
|
67
73
|
---
|
|
68
74
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
cs2tracker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
cs2tracker/__main__.py,sha256=Ub--oSMv48YzfWF1CZqYlkn1-HvZ7Bhxoc7urn1oY6o,249
|
|
3
|
+
cs2tracker/_version.py,sha256=ezuEPMmZBPsFTNec3uKYV6IfWrjNpfiIok1e_GQvZGQ,511
|
|
4
|
+
cs2tracker/application.py,sha256=pFmPRmm9OdYRLLl6qnmkHyV7oLI-Le4YSO26VX7eP4U,7180
|
|
5
|
+
cs2tracker/constants.py,sha256=er-YFaEe8WachO_q-RU6vik5SUcFEtacHxGYhD15Stk,17104
|
|
6
|
+
cs2tracker/main.py,sha256=kiTADEXXnX1y3kh0zII91mjadztMB145RBteXM2WG3I,915
|
|
7
|
+
cs2tracker/padded_console.py,sha256=lPEa34p-8LTmTbpf-2S5uYPaA2UmsIOPq2_UoVhMRgU,674
|
|
8
|
+
cs2tracker/scraper.py,sha256=hrL01_HWHpZZgkevj5JCmET0o2T1wSbmsxapuK11gv4,16202
|
|
9
|
+
cs2tracker/data/config.ini,sha256=q-T7_W3krPVJJoMcRJGGczNRvCZw-wJfNobpsdUEDNs,2672
|
|
10
|
+
cs2tracker/data/output.csv,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
cs2tracker-2.1.5.dist-info/licenses/LICENSE.md,sha256=G5wqQ_8KGA808kVuF-Fpu_Yhteg8K_5ux9n2v8eQK7s,1069
|
|
12
|
+
cs2tracker-2.1.5.dist-info/METADATA,sha256=zFEptkI_0AXDALL3uGqz0pTsRGn5OQMmnn-qAL6azfs,3126
|
|
13
|
+
cs2tracker-2.1.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
+
cs2tracker-2.1.5.dist-info/entry_points.txt,sha256=K8IwDIkg8QztSB9g9c89B9jR_2pG4QyJGrNs4z5RcZw,63
|
|
15
|
+
cs2tracker-2.1.5.dist-info/top_level.txt,sha256=2HB4xDDOxaU5BDc_yvdi9UlYLgL768n8aR-hRhFM6VQ,11
|
|
16
|
+
cs2tracker-2.1.5.dist-info/RECORD,,
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
cs2tracker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
cs2tracker/__main__.py,sha256=Ub--oSMv48YzfWF1CZqYlkn1-HvZ7Bhxoc7urn1oY6o,249
|
|
3
|
-
cs2tracker/_version.py,sha256=cuCBChX_WW6rxX7DfFGzi-EJBf76bHcqGsjRlHgeMvg,511
|
|
4
|
-
cs2tracker/application.py,sha256=CYuvEV2PlAKQ0MD_9vLViTonzspQFWJBGN6rzAOpAQY,4212
|
|
5
|
-
cs2tracker/constants.py,sha256=VG4QBedqHT9kjehWU-Uop7MNlD1VuERgL_VA4oN3obc,15127
|
|
6
|
-
cs2tracker/main.py,sha256=YmWXza0lmDDvLbOWzkJ6n0B1c-tq7Ub-IpwXSeIoKuQ,1309
|
|
7
|
-
cs2tracker/scraper.py,sha256=XoRtTPwN2Lxzqqghvcp3TOe9VdRge7Y_JTGOOJJyd3g,15457
|
|
8
|
-
cs2tracker/data/config.ini,sha256=LbdwEMFlgD8ubuEvGhYZdPtYsWhlho3nX3SLWi4T7Kg,2637
|
|
9
|
-
cs2tracker-2.1.4.dist-info/licenses/LICENSE.md,sha256=G5wqQ_8KGA808kVuF-Fpu_Yhteg8K_5ux9n2v8eQK7s,1069
|
|
10
|
-
cs2tracker-2.1.4.dist-info/METADATA,sha256=CfTvVmloYwmxtvnu0UUeOY6CzO7W3gsBNjGdMFxkBbk,2954
|
|
11
|
-
cs2tracker-2.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
-
cs2tracker-2.1.4.dist-info/entry_points.txt,sha256=K8IwDIkg8QztSB9g9c89B9jR_2pG4QyJGrNs4z5RcZw,63
|
|
13
|
-
cs2tracker-2.1.4.dist-info/top_level.txt,sha256=2HB4xDDOxaU5BDc_yvdi9UlYLgL768n8aR-hRhFM6VQ,11
|
|
14
|
-
cs2tracker-2.1.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|