cs2tracker 2.1.14__py3-none-any.whl → 2.1.16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cs2tracker might be problematic. Click here for more details.
- cs2tracker/_version.py +2 -2
- cs2tracker/app/__init__.py +0 -3
- cs2tracker/app/{application.py → app.py} +77 -45
- cs2tracker/app/editor_frame.py +223 -108
- cs2tracker/app/history_frame.py +61 -0
- cs2tracker/app/scraper_frame.py +13 -7
- cs2tracker/{util/validated_config.py → config.py} +97 -52
- cs2tracker/constants.py +20 -3
- cs2tracker/data/config.ini +24 -21
- cs2tracker/data/convert_inventory.js +108 -28
- cs2tracker/data/get_inventory.js +34 -13
- cs2tracker/logs.py +143 -0
- cs2tracker/main.py +3 -3
- cs2tracker/scraper/__init__.py +0 -9
- cs2tracker/scraper/background_task.py +80 -6
- cs2tracker/scraper/discord_notifier.py +34 -32
- cs2tracker/scraper/{parsers.py → parser.py} +35 -32
- cs2tracker/scraper/scraper.py +113 -83
- cs2tracker/util/__init__.py +0 -9
- cs2tracker/util/currency_conversion.py +84 -0
- cs2tracker/util/padded_console.py +5 -0
- cs2tracker/util/tkinter.py +55 -0
- {cs2tracker-2.1.14.dist-info → cs2tracker-2.1.16.dist-info}/METADATA +15 -16
- cs2tracker-2.1.16.dist-info/RECORD +31 -0
- cs2tracker/util/price_logs.py +0 -100
- cs2tracker-2.1.14.dist-info/RECORD +0 -28
- {cs2tracker-2.1.14.dist-info → cs2tracker-2.1.16.dist-info}/WHEEL +0 -0
- {cs2tracker-2.1.14.dist-info → cs2tracker-2.1.16.dist-info}/entry_points.txt +0 -0
- {cs2tracker-2.1.14.dist-info → cs2tracker-2.1.16.dist-info}/licenses/LICENSE +0 -0
- {cs2tracker-2.1.14.dist-info → cs2tracker-2.1.16.dist-info}/top_level.txt +0 -0
cs2tracker/data/get_inventory.js
CHANGED
|
@@ -22,15 +22,24 @@ const password = args[8];
|
|
|
22
22
|
const twoFactorCode = args[9];
|
|
23
23
|
|
|
24
24
|
const paddedLog = (...args) => {
|
|
25
|
-
console.log(" [
|
|
25
|
+
console.log(" [INFO] ", ...args);
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
const originalConsoleError = console.error;
|
|
29
29
|
console.error = (...args) => {
|
|
30
|
-
originalConsoleError("
|
|
30
|
+
originalConsoleError(" [ERROR] " + args.join(" "));
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
(async () => {
|
|
34
|
+
const closeWithError = (message) => {
|
|
35
|
+
console.error(message);
|
|
36
|
+
console.error("This window will automatically close in 10 seconds.");
|
|
37
|
+
setTimeout(() => {
|
|
38
|
+
user.logOff();
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}, 10000);
|
|
41
|
+
};
|
|
42
|
+
|
|
34
43
|
let user = new SteamUser();
|
|
35
44
|
|
|
36
45
|
paddedLog("Logging into Steam...");
|
|
@@ -41,25 +50,36 @@ console.error = (...args) => {
|
|
|
41
50
|
twoFactorCode: twoFactorCode,
|
|
42
51
|
});
|
|
43
52
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
const LOGIN_TIMEOUT_MS = 15000;
|
|
54
|
+
let loginTimeout = setTimeout(() => {
|
|
55
|
+
closeWithError(
|
|
56
|
+
"Login timed out. Please check your credentials and try again.",
|
|
57
|
+
);
|
|
58
|
+
}, LOGIN_TIMEOUT_MS);
|
|
59
|
+
|
|
60
|
+
user.on("steamGuard", (_domain, _callback, lastCodeWrong) => {
|
|
61
|
+
if (lastCodeWrong) {
|
|
62
|
+
closeWithError(
|
|
63
|
+
"The Steam Guard code you entered was incorrect. Please try again.",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
48
66
|
});
|
|
49
67
|
|
|
50
68
|
user.on("loggedOn", (_details, _parental) => {
|
|
69
|
+
clearTimeout(loginTimeout);
|
|
51
70
|
paddedLog("Logged into Steam.");
|
|
52
71
|
user.gamesPlayed([730]);
|
|
72
|
+
paddedLog("Connecting to CS2 Game Coordinator...");
|
|
53
73
|
});
|
|
54
74
|
|
|
55
|
-
|
|
75
|
+
user.on("error", (err) => {
|
|
76
|
+
closeWithError(`Steam Error: ${err.message}`);
|
|
77
|
+
});
|
|
56
78
|
|
|
57
|
-
|
|
79
|
+
let cs2 = new CS2(user);
|
|
58
80
|
|
|
59
81
|
cs2.on("error", (err) => {
|
|
60
|
-
|
|
61
|
-
user.logOff();
|
|
62
|
-
process.exit(1);
|
|
82
|
+
closeWithError(`CS2 Error: ${err.message}`);
|
|
63
83
|
});
|
|
64
84
|
|
|
65
85
|
let nameConverter = new ItemNameConverter();
|
|
@@ -96,8 +116,6 @@ console.error = (...args) => {
|
|
|
96
116
|
process.exit(0);
|
|
97
117
|
});
|
|
98
118
|
|
|
99
|
-
// TODO: The inventory may contain items that are not marketable or tradable,
|
|
100
|
-
// so we have to make sure to process these items correctly in the main app.
|
|
101
119
|
async function processInventory() {
|
|
102
120
|
try {
|
|
103
121
|
// filter out items that have the casket_id property set from the inventory
|
|
@@ -165,6 +183,9 @@ console.error = (...args) => {
|
|
|
165
183
|
function filterItems(items) {
|
|
166
184
|
let filteredItems = [];
|
|
167
185
|
items.forEach((item) => {
|
|
186
|
+
if (!item.item_tradable) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
168
189
|
if (
|
|
169
190
|
(item.item_type === "case" && importCases) ||
|
|
170
191
|
(item.item_type === "sticker capsule" && importStickerCapsules) ||
|
cs2tracker/logs.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
from cs2tracker.config import get_config
|
|
5
|
+
from cs2tracker.constants import OUTPUT_FILE
|
|
6
|
+
from cs2tracker.scraper.parser import Parser
|
|
7
|
+
from cs2tracker.util.currency_conversion import convert, to_symbol
|
|
8
|
+
|
|
9
|
+
config = get_config()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PriceLogs:
|
|
13
|
+
@classmethod
|
|
14
|
+
def _append_latest_calculation(cls, date, usd_totals):
|
|
15
|
+
"""Append the first price calculation of the day."""
|
|
16
|
+
with open(OUTPUT_FILE, "a", newline="", encoding="utf-8") as price_logs:
|
|
17
|
+
price_logs_writer = csv.writer(price_logs)
|
|
18
|
+
price_entries_today = [f"{usd_total:.2f}$" for usd_total in usd_totals]
|
|
19
|
+
price_logs_writer.writerow([date] + price_entries_today)
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def _replace_latest_calculation(cls, date, usd_totals):
|
|
23
|
+
"""Replace the last calculation of today with the most recent one of today."""
|
|
24
|
+
with open(OUTPUT_FILE, "r+", newline="", encoding="utf-8") as price_logs:
|
|
25
|
+
price_logs_reader = csv.reader(price_logs)
|
|
26
|
+
rows = list(price_logs_reader)
|
|
27
|
+
rows_without_today = rows[:-1]
|
|
28
|
+
price_logs.seek(0)
|
|
29
|
+
price_logs.truncate()
|
|
30
|
+
|
|
31
|
+
price_logs_writer = csv.writer(price_logs)
|
|
32
|
+
price_logs_writer.writerows(rows_without_today)
|
|
33
|
+
price_entries_today = [f"{usd_total:.2f}$" for usd_total in usd_totals]
|
|
34
|
+
price_logs_writer.writerow([date] + price_entries_today)
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def save(cls, usd_totals):
|
|
38
|
+
"""
|
|
39
|
+
Save the current date and total prices in USD to a CSV file.
|
|
40
|
+
|
|
41
|
+
This will append a new entry to the output file if no entry has been made for
|
|
42
|
+
today.
|
|
43
|
+
|
|
44
|
+
:param usd_totals: The total prices in USD to save.
|
|
45
|
+
:raises FileNotFoundError: If the output file does not exist.
|
|
46
|
+
:raises IOError: If there is an error writing to the output file.
|
|
47
|
+
"""
|
|
48
|
+
with open(OUTPUT_FILE, "r", encoding="utf-8") as price_logs:
|
|
49
|
+
price_logs_reader = csv.reader(price_logs)
|
|
50
|
+
rows = list(price_logs_reader)
|
|
51
|
+
last_log_date = rows[-1][0] if rows else ""
|
|
52
|
+
|
|
53
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
54
|
+
if last_log_date != today:
|
|
55
|
+
cls._append_latest_calculation(today, usd_totals)
|
|
56
|
+
else:
|
|
57
|
+
cls._replace_latest_calculation(today, usd_totals)
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def read(cls, newest_first=False, with_symbols=False):
|
|
61
|
+
"""
|
|
62
|
+
Parse the output file to extract dates, dollar prices, and the converted
|
|
63
|
+
currency prices. This data is used for drawing the plot of past prices.
|
|
64
|
+
|
|
65
|
+
:param newest_first: If True, the dates and totals will be returned in reverse
|
|
66
|
+
order
|
|
67
|
+
:param with_symbols: If True, the prices will be formatted with currency symbols
|
|
68
|
+
:return: A tuple containing dates and a dictionary of totals for each price
|
|
69
|
+
source.
|
|
70
|
+
:raises FileNotFoundError: If the output file does not exist.
|
|
71
|
+
:raises IOError: If there is an error reading the output file.
|
|
72
|
+
"""
|
|
73
|
+
conversion_currency = config.conversion_currency
|
|
74
|
+
dates = []
|
|
75
|
+
totals = {
|
|
76
|
+
price_source: {"USD": [], conversion_currency: []} for price_source in Parser.SOURCES
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
with open(OUTPUT_FILE, "r", encoding="utf-8") as price_logs:
|
|
80
|
+
price_logs_reader = csv.reader(price_logs)
|
|
81
|
+
for row in price_logs_reader:
|
|
82
|
+
date, *usd_totals = row
|
|
83
|
+
date = datetime.strptime(date, "%Y-%m-%d")
|
|
84
|
+
|
|
85
|
+
usd_totals = [float(price_usd.rstrip("$")) for price_usd in usd_totals]
|
|
86
|
+
converted_totals = [
|
|
87
|
+
convert(price_usd, "USD", conversion_currency) for price_usd in usd_totals
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
dates.append(date)
|
|
91
|
+
for price_source_index, price_source in enumerate(Parser.SOURCES):
|
|
92
|
+
totals[price_source]["USD"].append(usd_totals[price_source_index])
|
|
93
|
+
totals[price_source][conversion_currency].append(
|
|
94
|
+
converted_totals[price_source_index]
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if newest_first:
|
|
98
|
+
dates.reverse()
|
|
99
|
+
for price_source in Parser.SOURCES:
|
|
100
|
+
totals[price_source]["USD"].reverse()
|
|
101
|
+
totals[price_source][conversion_currency].reverse()
|
|
102
|
+
|
|
103
|
+
if with_symbols:
|
|
104
|
+
for price_source in Parser.SOURCES:
|
|
105
|
+
totals[price_source]["USD"] = [
|
|
106
|
+
f"${price:.2f}" for price in totals[price_source]["USD"]
|
|
107
|
+
]
|
|
108
|
+
totals[price_source][conversion_currency] = [
|
|
109
|
+
f"{to_symbol(conversion_currency)}{price:.2f}"
|
|
110
|
+
for price in totals[price_source][conversion_currency]
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
return dates, totals
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def validate_file(cls, log_file_path):
|
|
117
|
+
# pylint: disable=expression-not-assigned
|
|
118
|
+
"""
|
|
119
|
+
Ensures that the provided price log file has the right format. This should be
|
|
120
|
+
used before importing a price log file to ensure it is valid.
|
|
121
|
+
|
|
122
|
+
:param log_file_path: The path to the price log file to validate.
|
|
123
|
+
:return: True if the price log file is valid, False otherwise.
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
with open(log_file_path, "r", encoding="utf-8") as price_logs:
|
|
127
|
+
price_logs_reader = csv.reader(price_logs)
|
|
128
|
+
for row in price_logs_reader:
|
|
129
|
+
date_str, *usd_totals = row
|
|
130
|
+
datetime.strptime(date_str, "%Y-%m-%d")
|
|
131
|
+
[float(price_usd.rstrip("$")) for price_usd in usd_totals]
|
|
132
|
+
except (FileNotFoundError, IOError, ValueError, TypeError):
|
|
133
|
+
return False
|
|
134
|
+
except Exception:
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
def empty(cls):
|
|
141
|
+
"""Checks if the price history is empty and returns True if it is."""
|
|
142
|
+
with open(OUTPUT_FILE, "r", encoding="utf-8") as price_logs:
|
|
143
|
+
return len(list(price_logs)) == 0
|
cs2tracker/main.py
CHANGED
|
@@ -2,10 +2,10 @@ import sys
|
|
|
2
2
|
|
|
3
3
|
import urllib3
|
|
4
4
|
|
|
5
|
-
from cs2tracker.app import Application
|
|
5
|
+
from cs2tracker.app.app import Application
|
|
6
6
|
from cs2tracker.constants import AUTHOR_STRING, BANNER, OS, OSType
|
|
7
|
-
from cs2tracker.scraper import Scraper
|
|
8
|
-
from cs2tracker.util import get_console
|
|
7
|
+
from cs2tracker.scraper.scraper import Scraper
|
|
8
|
+
from cs2tracker.util.padded_console import get_console
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def main():
|
cs2tracker/scraper/__init__.py
CHANGED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
from cs2tracker.scraper.background_task import ( # noqa: F401 # pylint:disable=unused-import
|
|
2
|
-
BackgroundTask,
|
|
3
|
-
)
|
|
4
|
-
from cs2tracker.scraper.discord_notifier import ( # noqa: F401 # pylint:disable=unused-import
|
|
5
|
-
DiscordNotifier,
|
|
6
|
-
)
|
|
7
|
-
from cs2tracker.scraper.scraper import ( # noqa: F401 # pylint:disable=unused-import
|
|
8
|
-
Scraper,
|
|
9
|
-
)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
-
from subprocess import DEVNULL, call
|
|
2
|
+
from subprocess import DEVNULL, STDOUT, CalledProcessError, call, check_output, run
|
|
3
3
|
|
|
4
4
|
from cs2tracker.constants import (
|
|
5
5
|
BATCH_FILE,
|
|
@@ -9,7 +9,7 @@ from cs2tracker.constants import (
|
|
|
9
9
|
RUNNING_IN_EXE,
|
|
10
10
|
OSType,
|
|
11
11
|
)
|
|
12
|
-
from cs2tracker.util import get_console
|
|
12
|
+
from cs2tracker.util.padded_console import get_console
|
|
13
13
|
|
|
14
14
|
WIN_BACKGROUND_TASK_NAME = "CS2Tracker Daily Calculation"
|
|
15
15
|
WIN_BACKGROUND_TASK_SCHEDULE = "DAILY"
|
|
@@ -18,6 +18,18 @@ WIN_BACKGROUND_TASK_CMD = (
|
|
|
18
18
|
f"powershell -WindowStyle Hidden -Command \"Start-Process '{BATCH_FILE}' -WindowStyle Hidden\""
|
|
19
19
|
)
|
|
20
20
|
|
|
21
|
+
LINUX_BACKGROUND_TASK_SCHEDULE = "0 12 * * *"
|
|
22
|
+
LINUX_BACKGROUND_TASK_CMD = (
|
|
23
|
+
f"bash -c 'cd {PROJECT_DIR} && {PYTHON_EXECUTABLE} -m cs2tracker --only-scrape'"
|
|
24
|
+
)
|
|
25
|
+
LINUX_BACKGROUND_TASK_CMD_EXE = f"bash -c 'cd {PROJECT_DIR} && {PYTHON_EXECUTABLE} --only-scrape'"
|
|
26
|
+
|
|
27
|
+
if RUNNING_IN_EXE:
|
|
28
|
+
LINUX_CRON_JOB = f"{LINUX_BACKGROUND_TASK_SCHEDULE} {LINUX_BACKGROUND_TASK_CMD_EXE}"
|
|
29
|
+
else:
|
|
30
|
+
LINUX_CRON_JOB = f"{LINUX_BACKGROUND_TASK_SCHEDULE} {LINUX_BACKGROUND_TASK_CMD}"
|
|
31
|
+
|
|
32
|
+
|
|
21
33
|
console = get_console()
|
|
22
34
|
|
|
23
35
|
|
|
@@ -34,8 +46,17 @@ class BackgroundTask:
|
|
|
34
46
|
return_code = call(cmd, stdout=DEVNULL, stderr=DEVNULL)
|
|
35
47
|
found = return_code == 0
|
|
36
48
|
return found
|
|
49
|
+
elif OS == OSType.LINUX:
|
|
50
|
+
try:
|
|
51
|
+
existing_jobs = (
|
|
52
|
+
check_output(["crontab", "-l"], stderr=STDOUT).decode("utf-8").strip()
|
|
53
|
+
)
|
|
54
|
+
except CalledProcessError:
|
|
55
|
+
existing_jobs = ""
|
|
56
|
+
|
|
57
|
+
found = LINUX_CRON_JOB in existing_jobs.splitlines()
|
|
58
|
+
return found
|
|
37
59
|
else:
|
|
38
|
-
# TODO: implement finder for cron jobs
|
|
39
60
|
return False
|
|
40
61
|
|
|
41
62
|
@classmethod
|
|
@@ -83,17 +104,69 @@ class BackgroundTask:
|
|
|
83
104
|
]
|
|
84
105
|
return_code = call(cmd, stdout=DEVNULL, stderr=DEVNULL)
|
|
85
106
|
if return_code == 0:
|
|
86
|
-
console.
|
|
107
|
+
console.info("Background task enabled.")
|
|
87
108
|
else:
|
|
88
109
|
console.error("Failed to enable background task.")
|
|
89
110
|
else:
|
|
90
111
|
cmd = ["schtasks", "/delete", "/tn", WIN_BACKGROUND_TASK_NAME, "/f"]
|
|
91
112
|
return_code = call(cmd, stdout=DEVNULL, stderr=DEVNULL)
|
|
92
113
|
if return_code == 0:
|
|
93
|
-
console.
|
|
114
|
+
console.info("Background task disabled.")
|
|
94
115
|
else:
|
|
95
116
|
console.error("Failed to disable background task.")
|
|
96
117
|
|
|
118
|
+
@classmethod
|
|
119
|
+
def _toggle_linux(cls, enabled: bool):
|
|
120
|
+
"""
|
|
121
|
+
Create or delete a daily background task that runs the scraper on Linux.
|
|
122
|
+
|
|
123
|
+
:param enabled: If True, the task will be created; if False, the task will be
|
|
124
|
+
deleted.
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
existing_jobs = check_output(["crontab", "-l"], stderr=STDOUT).decode("utf-8").strip()
|
|
128
|
+
except CalledProcessError:
|
|
129
|
+
existing_jobs = ""
|
|
130
|
+
|
|
131
|
+
cron_lines = existing_jobs.splitlines()
|
|
132
|
+
|
|
133
|
+
if enabled and LINUX_CRON_JOB not in cron_lines:
|
|
134
|
+
updated_jobs = (
|
|
135
|
+
existing_jobs + "\n" + LINUX_CRON_JOB + "\n"
|
|
136
|
+
if existing_jobs
|
|
137
|
+
else LINUX_CRON_JOB + "\n"
|
|
138
|
+
)
|
|
139
|
+
try:
|
|
140
|
+
run(
|
|
141
|
+
["crontab", "-"],
|
|
142
|
+
input=updated_jobs.encode("utf-8"),
|
|
143
|
+
stdout=DEVNULL,
|
|
144
|
+
stderr=DEVNULL,
|
|
145
|
+
check=True,
|
|
146
|
+
)
|
|
147
|
+
console.info("Background task enabled.")
|
|
148
|
+
except CalledProcessError:
|
|
149
|
+
console.error("Failed to enable background task.")
|
|
150
|
+
|
|
151
|
+
elif not enabled and LINUX_CRON_JOB in cron_lines:
|
|
152
|
+
updated_jobs = "\n".join(
|
|
153
|
+
line for line in cron_lines if line.strip() != LINUX_CRON_JOB
|
|
154
|
+
).strip()
|
|
155
|
+
try:
|
|
156
|
+
if updated_jobs:
|
|
157
|
+
run(
|
|
158
|
+
["crontab", "-"],
|
|
159
|
+
input=(updated_jobs + "\n").encode("utf-8"),
|
|
160
|
+
stdout=DEVNULL,
|
|
161
|
+
stderr=DEVNULL,
|
|
162
|
+
check=True,
|
|
163
|
+
)
|
|
164
|
+
else:
|
|
165
|
+
run(["crontab", "-r"], stdout=DEVNULL, stderr=DEVNULL, check=True)
|
|
166
|
+
console.info("Background task disabled.")
|
|
167
|
+
except CalledProcessError:
|
|
168
|
+
console.error("Failed to disable background task.")
|
|
169
|
+
|
|
97
170
|
@classmethod
|
|
98
171
|
def toggle(cls, enabled: bool):
|
|
99
172
|
"""
|
|
@@ -104,6 +177,7 @@ class BackgroundTask:
|
|
|
104
177
|
"""
|
|
105
178
|
if OS == OSType.WINDOWS:
|
|
106
179
|
cls._toggle_windows(enabled)
|
|
180
|
+
elif OS == OSType.LINUX:
|
|
181
|
+
cls._toggle_linux(enabled)
|
|
107
182
|
else:
|
|
108
|
-
# TODO: implement toggle for cron jobs
|
|
109
183
|
pass
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import requests
|
|
2
2
|
from requests.exceptions import RequestException
|
|
3
3
|
|
|
4
|
-
from cs2tracker.
|
|
4
|
+
from cs2tracker.config import get_config
|
|
5
|
+
from cs2tracker.logs import PriceLogs
|
|
6
|
+
from cs2tracker.util.padded_console import get_console
|
|
5
7
|
|
|
6
8
|
DC_WEBHOOK_USERNAME = "CS2Tracker"
|
|
7
9
|
DC_WEBHOOK_AVATAR_URL = "https://img.icons8.com/?size=100&id=uWQJp2tLXUH6&format=png&color=000000"
|
|
8
10
|
DC_RECENT_HISTORY_LIMIT = 5
|
|
9
11
|
|
|
10
12
|
console = get_console()
|
|
13
|
+
config = get_config()
|
|
11
14
|
|
|
12
15
|
|
|
13
16
|
class DiscordNotifier:
|
|
@@ -19,42 +22,41 @@ class DiscordNotifier:
|
|
|
19
22
|
|
|
20
23
|
:return: A list of embeds for the Discord message.
|
|
21
24
|
"""
|
|
22
|
-
dates,
|
|
23
|
-
dates, usd_prices, eur_prices = reversed(dates), reversed(usd_prices), reversed(eur_prices)
|
|
25
|
+
dates, totals = PriceLogs.read(newest_first=True, with_symbols=True)
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
date_field = [
|
|
28
|
+
{
|
|
29
|
+
"name": "Date",
|
|
30
|
+
"value": "\n".join([date.strftime("%Y-%m-%d") for date in dates][:DC_RECENT_HISTORY_LIMIT]), # type: ignore
|
|
31
|
+
"inline": True,
|
|
32
|
+
},
|
|
33
|
+
]
|
|
34
|
+
price_fields = [
|
|
35
|
+
{
|
|
36
|
+
"name": f"{price_source.name.title()} (USD | {config.conversion_currency})",
|
|
37
|
+
"value": "\n".join(
|
|
38
|
+
[
|
|
39
|
+
f"{usd_total} | {converted_total}"
|
|
40
|
+
for usd_total, converted_total in zip(
|
|
41
|
+
totals[price_source]["USD"][:DC_RECENT_HISTORY_LIMIT],
|
|
42
|
+
totals[price_source][config.conversion_currency][
|
|
43
|
+
:DC_RECENT_HISTORY_LIMIT
|
|
44
|
+
],
|
|
45
|
+
)
|
|
46
|
+
]
|
|
47
|
+
),
|
|
48
|
+
"inline": True,
|
|
49
|
+
}
|
|
50
|
+
for price_source in totals
|
|
51
|
+
][
|
|
52
|
+
:2
|
|
53
|
+
] # Limit to the first two price sources because Discord can only display 3 fields per line (Date + 2 Price Sources)
|
|
36
54
|
|
|
37
55
|
embeds = [
|
|
38
56
|
{
|
|
39
|
-
"title": "📊 Recent
|
|
57
|
+
"title": "📊 Recent Investment History",
|
|
40
58
|
"color": 5814783,
|
|
41
|
-
"fields":
|
|
42
|
-
{
|
|
43
|
-
"name": "Date",
|
|
44
|
-
"value": date_history,
|
|
45
|
-
"inline": True,
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
"name": "USD Total",
|
|
49
|
-
"value": usd_history,
|
|
50
|
-
"inline": True,
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
"name": "EUR Total",
|
|
54
|
-
"value": eur_history,
|
|
55
|
-
"inline": True,
|
|
56
|
-
},
|
|
57
|
-
],
|
|
59
|
+
"fields": date_field + price_fields,
|
|
58
60
|
}
|
|
59
61
|
]
|
|
60
62
|
|
|
@@ -5,9 +5,9 @@ from urllib.parse import unquote
|
|
|
5
5
|
from bs4 import BeautifulSoup
|
|
6
6
|
from bs4.element import Tag
|
|
7
7
|
|
|
8
|
+
from cs2tracker.config import get_config
|
|
8
9
|
from cs2tracker.constants import CAPSULE_PAGES
|
|
9
|
-
from cs2tracker.util import get_console
|
|
10
|
-
from cs2tracker.util.validated_config import get_config
|
|
10
|
+
from cs2tracker.util.padded_console import get_console
|
|
11
11
|
|
|
12
12
|
config = get_config()
|
|
13
13
|
console = get_console()
|
|
@@ -17,9 +17,11 @@ class PriceSource(Enum):
|
|
|
17
17
|
STEAM = "steam"
|
|
18
18
|
BUFF163 = "buff163"
|
|
19
19
|
SKINPORT = "skinport"
|
|
20
|
+
YOUPIN898 = "youpin"
|
|
21
|
+
CSFLOAT = "csfloat"
|
|
20
22
|
|
|
21
23
|
|
|
22
|
-
class
|
|
24
|
+
class BaseParser(ABC):
|
|
23
25
|
@classmethod
|
|
24
26
|
@abstractmethod
|
|
25
27
|
def get_item_page_url(cls, item_href, source=PriceSource.STEAM) -> str:
|
|
@@ -46,7 +48,7 @@ class Parser(ABC):
|
|
|
46
48
|
"""
|
|
47
49
|
|
|
48
50
|
|
|
49
|
-
class SteamParser(
|
|
51
|
+
class SteamParser(BaseParser):
|
|
50
52
|
STEAM_MARKET_SEARCH_PAGE_BASE_URL = "https://steamcommunity.com/market/search?q={}"
|
|
51
53
|
PRICE_INFO = "Owned: {:<10} {} price: ${:<10} Total: ${:<10}"
|
|
52
54
|
NEEDS_TIMEOUT = True
|
|
@@ -60,7 +62,7 @@ class SteamParser(Parser):
|
|
|
60
62
|
# Therefore, if the provided item is a sticker capsule we return a search page defined in CAPSULE_PAGES
|
|
61
63
|
# where all of the sticker capsules of one section are listed
|
|
62
64
|
for section in config.sections():
|
|
63
|
-
if section in ("
|
|
65
|
+
if section in ("Skins", "Stickers", "Cases", "User Settings", "App Settings"):
|
|
64
66
|
continue
|
|
65
67
|
if any(item_href == option for option in config.options(section)):
|
|
66
68
|
return CAPSULE_PAGES[section]
|
|
@@ -89,24 +91,7 @@ class SteamParser(Parser):
|
|
|
89
91
|
return price
|
|
90
92
|
|
|
91
93
|
|
|
92
|
-
class
|
|
93
|
-
SKINLEDGER_PRICE_LIST = ""
|
|
94
|
-
PRICE_INFO = "Owned: {:<10} {} price: ${:<10} Total: ${:<10}"
|
|
95
|
-
NEEDS_TIMEOUT = False
|
|
96
|
-
SOURCES = [PriceSource.STEAM, PriceSource.BUFF163, PriceSource.SKINPORT]
|
|
97
|
-
|
|
98
|
-
@classmethod
|
|
99
|
-
def get_item_page_url(cls, item_href, source=PriceSource.STEAM) -> str:
|
|
100
|
-
_ = source
|
|
101
|
-
return super().get_item_page_url(item_href)
|
|
102
|
-
|
|
103
|
-
@classmethod
|
|
104
|
-
def parse_item_price(cls, item_page, item_href, source=PriceSource.STEAM) -> float:
|
|
105
|
-
_, _ = item_href, source
|
|
106
|
-
return super().parse_item_price(item_page, item_href)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
class ClashParser(Parser):
|
|
94
|
+
class ClashParser(BaseParser):
|
|
110
95
|
CLASH_ITEM_API_BASE_URL = "https://inventory.clash.gg/api/GetItemPrice?id={}"
|
|
111
96
|
PRICE_INFO = "Owned: {:<10} {} price: ${:<10} Total: ${:<10}"
|
|
112
97
|
NEEDS_TIMEOUT = True
|
|
@@ -138,11 +123,11 @@ class ClashParser(Parser):
|
|
|
138
123
|
return price
|
|
139
124
|
|
|
140
125
|
|
|
141
|
-
class
|
|
126
|
+
class CSGOTraderParser(BaseParser):
|
|
142
127
|
CSGOTRADER_PRICE_LIST = "https://prices.csgotrader.app/latest/{}.json"
|
|
143
128
|
PRICE_INFO = "Owned: {:<10} {:<10}: ${:<10} Total: ${:<10}"
|
|
144
129
|
NEEDS_TIMEOUT = False
|
|
145
|
-
SOURCES = [PriceSource.STEAM, PriceSource.BUFF163, PriceSource.
|
|
130
|
+
SOURCES = [PriceSource.STEAM, PriceSource.BUFF163, PriceSource.CSFLOAT]
|
|
146
131
|
|
|
147
132
|
@classmethod
|
|
148
133
|
def get_item_page_url(cls, item_href, source=PriceSource.STEAM):
|
|
@@ -154,6 +139,7 @@ class CSGOTrader(Parser):
|
|
|
154
139
|
|
|
155
140
|
@classmethod
|
|
156
141
|
def parse_item_price(cls, item_page, item_href, source=PriceSource.STEAM):
|
|
142
|
+
# pylint: disable=too-many-branches
|
|
157
143
|
_ = source
|
|
158
144
|
|
|
159
145
|
price_list = item_page.json()
|
|
@@ -167,13 +153,14 @@ class CSGOTrader(Parser):
|
|
|
167
153
|
raise ValueError(f"CSGOTrader: Could not find item price info: {url_decoded_name}")
|
|
168
154
|
|
|
169
155
|
if source == PriceSource.STEAM:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
price
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
156
|
+
for timestamp in ("last_24h", "last_7d", "last_30d", "last_90d"):
|
|
157
|
+
price = price_info.get(timestamp)
|
|
158
|
+
if price:
|
|
159
|
+
break
|
|
160
|
+
else:
|
|
161
|
+
raise ValueError(
|
|
162
|
+
f"CSGOTrader: Could not find steam price info for the past 3 months: {url_decoded_name}"
|
|
163
|
+
)
|
|
177
164
|
elif source == PriceSource.BUFF163:
|
|
178
165
|
price = price_info.get("starting_at")
|
|
179
166
|
if not price:
|
|
@@ -183,6 +170,18 @@ class CSGOTrader(Parser):
|
|
|
183
170
|
raise ValueError(
|
|
184
171
|
f"CSGOTrader: Could not find recent buff163 price: {url_decoded_name}"
|
|
185
172
|
)
|
|
173
|
+
elif source == PriceSource.YOUPIN898:
|
|
174
|
+
price = price_info
|
|
175
|
+
if not price:
|
|
176
|
+
raise ValueError(
|
|
177
|
+
f"CSGOTrader: Could not find recent youpin898 price: {url_decoded_name}"
|
|
178
|
+
)
|
|
179
|
+
elif source == PriceSource.CSFLOAT:
|
|
180
|
+
price = price_info.get("price")
|
|
181
|
+
if not price:
|
|
182
|
+
raise ValueError(
|
|
183
|
+
f"CSGOTrader: Could not find recent csfloat price: {url_decoded_name}"
|
|
184
|
+
)
|
|
186
185
|
else:
|
|
187
186
|
price = price_info.get("starting_at")
|
|
188
187
|
if not price:
|
|
@@ -190,3 +189,7 @@ class CSGOTrader(Parser):
|
|
|
190
189
|
|
|
191
190
|
price = float(price)
|
|
192
191
|
return price
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# Default parser used by the scraper
|
|
195
|
+
Parser = CSGOTraderParser
|