cs2tracker 2.1.9__py3-none-any.whl → 2.1.10__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 CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '2.1.9'
21
- __version_tuple__ = version_tuple = (2, 1, 9)
20
+ __version__ = version = '2.1.10'
21
+ __version_tuple__ = version_tuple = (2, 1, 10)
cs2tracker/application.py CHANGED
@@ -3,13 +3,16 @@ import tkinter as tk
3
3
  from shutil import copy
4
4
  from subprocess import Popen
5
5
  from threading import Thread
6
- from tkinter import messagebox
6
+ from tkinter import messagebox, ttk
7
+ from tkinter.filedialog import askopenfilename, asksaveasfile
7
8
  from typing import cast
8
9
 
9
10
  import matplotlib.pyplot as plt
11
+ import sv_ttk
10
12
  from matplotlib.axes import Axes
11
13
  from matplotlib.dates import DateFormatter
12
14
 
15
+ from cs2tracker.background_task import BackgroundTask
13
16
  from cs2tracker.constants import (
14
17
  CONFIG_FILE,
15
18
  CONFIG_FILE_BACKUP,
@@ -19,20 +22,19 @@ from cs2tracker.constants import (
19
22
  POWERSHELL_COLORIZE_OUTPUT,
20
23
  PYTHON_EXECUTABLE,
21
24
  RUNNING_IN_EXE,
22
- TEXT_EDITOR,
23
25
  OSType,
24
26
  )
27
+ from cs2tracker.price_logs import PriceLogs
25
28
  from cs2tracker.scraper import Scraper
26
29
 
27
30
  APPLICATION_NAME = "CS2Tracker"
31
+ WINDOW_SIZE = "630x380"
28
32
 
29
- WINDOW_SIZE = "550x500"
30
- BACKGROUND_COLOR = "#1e1e1e"
31
- BUTTON_COLOR = "#3c3f41"
32
- BUTTON_HOVER_COLOR = "#505354"
33
- BUTTON_ACTIVE_COLOR = "#5c5f61"
34
- FONT_STYLE = "Segoe UI"
35
- FONT_COLOR = "white"
33
+ CONFIG_EDITOR_TITLE = "Config Editor"
34
+ CONFIG_EDITOR_SIZE = "800x750"
35
+
36
+ NEW_CUSTOM_ITEM_TITLE = "Add Custom Item"
37
+ NEW_CUSTOM_ITEM_SIZE = "500x200"
36
38
 
37
39
  SCRAPER_WINDOW_HEIGHT = 40
38
40
  SCRAPER_WINDOW_WIDTH = 120
@@ -42,85 +44,64 @@ SCRAPER_WINDOW_BACKGROUND_COLOR = "Black"
42
44
  class Application:
43
45
  def __init__(self):
44
46
  self.scraper = Scraper()
47
+ self.application_window = None
48
+ self.config_editor_window = None
45
49
 
46
50
  def run(self):
47
51
  """Run the main application window with buttons for scraping prices, editing the
48
52
  configuration, showing history in a chart, and editing the log file.
49
53
  """
50
- application_window = self._configure_window()
51
- application_window.mainloop()
52
-
53
- def _add_button(self, frame, text, command):
54
- """Create and style a button for the main application window."""
55
- button_style = {
56
- "font": (FONT_STYLE, 12),
57
- "fg": FONT_COLOR,
58
- "bg": BUTTON_COLOR,
59
- "activebackground": BUTTON_ACTIVE_COLOR,
60
- }
61
- button = tk.Button(frame, text=text, command=command, **button_style)
62
- button.pack(pady=5, fill="x")
63
- button.bind("<Enter>", lambda _: button.config(bg=BUTTON_HOVER_COLOR))
64
- button.bind("<Leave>", lambda _: button.config(bg=BUTTON_COLOR))
65
- return button
66
-
67
- def _add_checkbox(self, frame, text, variable, command):
68
- checkbox = tk.Checkbutton(
54
+ self.application_window = self._configure_window()
55
+
56
+ sv_ttk.use_dark_theme()
57
+
58
+ self.application_window.mainloop()
59
+
60
+ def _add_button(self, frame, text, command, row):
61
+ """Create and style a button for the button frame."""
62
+ grid_pos = {"row": row, "column": 0, "sticky": "ew", "padx": 10, "pady": 10}
63
+ button = ttk.Button(frame, text=text, command=command)
64
+ button.grid(**grid_pos)
65
+
66
+ def _configure_button_frame(self, main_frame):
67
+ """Configure the button frame of the application main frame."""
68
+ button_frame = ttk.Frame(main_frame, style="Card.TFrame", padding=15)
69
+ button_frame.columnconfigure(0, weight=1)
70
+ button_frame.grid(row=0, column=0, padx=10, pady=(7, 20), sticky="nsew")
71
+
72
+ self._add_button(button_frame, "Run!", self.scrape_prices, 0)
73
+ self._add_button(button_frame, "Edit Config", self._edit_config, 1)
74
+ self._add_button(button_frame, "Reset Config", self._reset_config, 2)
75
+ self._add_button(button_frame, "Show History", self._draw_plot, 3)
76
+ self._add_button(button_frame, "Export History", self._export_log_file, 4)
77
+ self._add_button(button_frame, "Import History", self._import_log_file, 5)
78
+
79
+ def _add_checkbox(
80
+ self, frame, text, variable, command, row
81
+ ): # pylint: disable=too-many-arguments,too-many-positional-arguments
82
+ """Create and style a checkbox for the checkbox frame."""
83
+ grid_pos = {"row": row, "column": 0, "sticky": "w", "padx": (10, 0), "pady": 5}
84
+ checkbox = ttk.Checkbutton(
69
85
  frame,
70
86
  text=text,
71
87
  variable=variable,
72
88
  command=command,
73
- bg=BACKGROUND_COLOR,
74
- fg=FONT_COLOR,
75
- selectcolor=BUTTON_COLOR,
76
- activebackground=BACKGROUND_COLOR,
77
- font=(FONT_STYLE, 10),
78
- anchor="w",
79
- padx=20,
80
- )
81
- checkbox.pack(fill="x", anchor="w", pady=2)
82
-
83
- def _configure_window(self):
84
- """Configure the main application window UI and add buttons for the main
85
- functionalities.
86
- """
87
- window = tk.Tk()
88
- window.title(APPLICATION_NAME)
89
- window.geometry(WINDOW_SIZE)
90
- window.configure(bg=BACKGROUND_COLOR)
91
- if OS == OSType.WINDOWS:
92
- app_id = "cs2tracker.unique.id"
93
- ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id)
94
- icon = tk.PhotoImage(file=ICON_FILE)
95
- window.wm_iconphoto(False, icon)
96
-
97
- frame = tk.Frame(window, bg=BACKGROUND_COLOR, padx=30, pady=30)
98
- frame.pack(expand=True, fill="both")
99
-
100
- label = tk.Label(
101
- frame,
102
- text=f"Welcome to {APPLICATION_NAME}!",
103
- font=(FONT_STYLE, 16, "bold"),
104
- fg=FONT_COLOR,
105
- bg=BACKGROUND_COLOR,
89
+ style="Switch.TCheckbutton",
106
90
  )
107
- label.pack(pady=(0, 30))
108
-
109
- self._add_button(frame, "Run!", self.scrape_prices)
110
- self._add_button(frame, "Edit Config", self._edit_config)
111
- self._add_button(frame, "Reset Config", self._confirm_reset_config)
112
- self._add_button(frame, "Show History (Chart)", self._draw_plot)
113
- self._add_button(frame, "Show History (File)", self._edit_log_file)
91
+ checkbox.grid(**grid_pos)
114
92
 
115
- checkbox_frame = tk.Frame(frame, bg=BACKGROUND_COLOR)
116
- checkbox_frame.pack(pady=(20, 0), fill="x")
93
+ def _configure_checkbox_frame(self, main_frame):
94
+ """Configure the checkbox frame for background tasks and settings."""
95
+ checkbox_frame = ttk.LabelFrame(main_frame, text="Settings", padding=15)
96
+ checkbox_frame.grid(row=0, column=1, padx=10, pady=(0, 20), sticky="nsew")
117
97
 
118
- background_checkbox_value = tk.BooleanVar(value=self.scraper.identify_background_task())
98
+ background_checkbox_value = tk.BooleanVar(value=BackgroundTask.identify())
119
99
  self._add_checkbox(
120
100
  checkbox_frame,
121
- "Daily Background Calculations",
101
+ "Background Task",
122
102
  background_checkbox_value,
123
103
  lambda: self._toggle_background_task(background_checkbox_value.get()),
104
+ 0,
124
105
  )
125
106
 
126
107
  discord_webhook_checkbox_value = tk.BooleanVar(
@@ -130,9 +111,12 @@ class Application:
130
111
  )
131
112
  self._add_checkbox(
132
113
  checkbox_frame,
133
- "Receive Discord Notifications",
114
+ "Discord Notifications",
134
115
  discord_webhook_checkbox_value,
135
- lambda: self._toggle_discord_webhook(discord_webhook_checkbox_value.get()),
116
+ lambda: discord_webhook_checkbox_value.set(
117
+ self._toggle_discord_webhook(discord_webhook_checkbox_value.get())
118
+ ),
119
+ 1,
136
120
  )
137
121
 
138
122
  use_proxy_checkbox_value = tk.BooleanVar(
@@ -142,9 +126,48 @@ class Application:
142
126
  checkbox_frame,
143
127
  "Proxy Requests",
144
128
  use_proxy_checkbox_value,
145
- lambda: self._toggle_use_proxy(use_proxy_checkbox_value.get()),
129
+ lambda: use_proxy_checkbox_value.set(
130
+ self._toggle_use_proxy(use_proxy_checkbox_value.get())
131
+ ),
132
+ 2,
146
133
  )
147
134
 
135
+ dark_theme_checkbox_value = tk.BooleanVar(value=True)
136
+ self._add_checkbox(
137
+ checkbox_frame, "Dark Theme", dark_theme_checkbox_value, sv_ttk.toggle_theme, 3
138
+ )
139
+
140
+ def _configure_main_frame(self, window):
141
+ """Configure the main frame of the application window with buttons and
142
+ checkboxes.
143
+ """
144
+ main_frame = ttk.Frame(window, padding=15)
145
+ main_frame.columnconfigure(0, weight=1)
146
+ main_frame.columnconfigure(1, weight=1)
147
+ main_frame.rowconfigure(0, weight=1)
148
+
149
+ self._configure_button_frame(main_frame)
150
+ self._configure_checkbox_frame(main_frame)
151
+
152
+ main_frame.pack(expand=True, fill="both")
153
+
154
+ def _configure_window(self):
155
+ """Configure the main application window UI and add buttons for the main
156
+ functionalities.
157
+ """
158
+ window = tk.Tk()
159
+ window.title(APPLICATION_NAME)
160
+ window.geometry(WINDOW_SIZE)
161
+
162
+ if OS == OSType.WINDOWS:
163
+ app_id = "cs2tracker.unique.id"
164
+ ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id)
165
+
166
+ icon = tk.PhotoImage(file=ICON_FILE)
167
+ window.wm_iconphoto(True, icon)
168
+
169
+ self._configure_main_frame(window)
170
+
148
171
  return window
149
172
 
150
173
  def _construct_scraper_command_windows(self):
@@ -200,57 +223,280 @@ class Application:
200
223
  # TODO: implement external window for Linux
201
224
  self.scraper.scrape_prices()
202
225
 
203
- def _edit_config(self):
204
- """Edit the configuration file using the specified text editor."""
205
- _popen_and_call(
206
- popen_args={"args": [TEXT_EDITOR, CONFIG_FILE], "shell": True},
207
- callback=self.scraper.parse_config,
226
+ def _make_tree_editable(self, editor_frame, tree):
227
+ """
228
+ Add a binding to the treeview that allows double-clicking on a cell to edit its
229
+ value.
230
+
231
+ Source: https://stackoverflow.com/questions/75787251/create-an-editable-tkinter-treeview-with-keyword-connection
232
+ """
233
+
234
+ def set_cell_value(event):
235
+ def save_edit(event):
236
+ tree.set(row, column=column, value=event.widget.get())
237
+ event.widget.destroy()
238
+
239
+ try:
240
+ row = tree.identify_row(event.y)
241
+ column = tree.identify_column(event.x)
242
+ item_text = tree.set(row, column)
243
+ if item_text.strip() == "":
244
+ left_item_text = tree.item(row, "text")
245
+ # Don't allow editing of section headers
246
+ if any(left_item_text == section for section in self.scraper.config.sections()):
247
+ return
248
+ x, y, w, h = tree.bbox(row, column)
249
+ entryedit = ttk.Entry(editor_frame)
250
+ entryedit.place(x=x, y=y, width=w, height=h + 3) # type: ignore
251
+ entryedit.insert("end", item_text)
252
+ entryedit.bind("<Return>", save_edit)
253
+ entryedit.focus_set()
254
+ entryedit.grab_set()
255
+ except Exception:
256
+ pass
257
+
258
+ def destroy_entries(_):
259
+ """Destroy any entry widgets in the treeview when the mouse wheel is
260
+ used.
261
+ """
262
+ for widget in editor_frame.winfo_children():
263
+ if isinstance(widget, ttk.Entry):
264
+ widget.destroy()
265
+
266
+ def destroy_entry(event):
267
+ """Destroy the entry widget if the user clicks outside of it."""
268
+ if isinstance(event.widget, ttk.Entry):
269
+ event.widget.destroy()
270
+
271
+ tree.bind("<Double-1>", set_cell_value)
272
+ self.config_editor_window.bind("<MouseWheel>", destroy_entries) # type: ignore
273
+ self.config_editor_window.bind("<Button-1>", destroy_entry) # type: ignore
274
+
275
+ def _configure_treeview(self, editor_frame):
276
+ """Add a treeview to the editor frame to display and edit configuration
277
+ options.
278
+ """
279
+ scrollbar = ttk.Scrollbar(editor_frame)
280
+ scrollbar.pack(side="right", fill="y", padx=(5, 0))
281
+
282
+ tree = ttk.Treeview(
283
+ editor_frame,
284
+ columns=(1,),
285
+ height=10,
286
+ selectmode="browse",
287
+ yscrollcommand=scrollbar.set,
208
288
  )
289
+ scrollbar.config(command=tree.yview)
290
+
291
+ tree.column("#0", anchor="w", width=200)
292
+ tree.column(1, anchor="w", width=25)
293
+ tree.heading("#0", text="Option")
294
+ tree.heading(1, text="Value")
295
+
296
+ for section in self.scraper.config.sections():
297
+ if section == "App Settings":
298
+ continue
299
+ section_level = tree.insert("", "end", iid=section, text=section)
300
+ for config_option, value in self.scraper.config.items(section):
301
+ title_option = config_option.replace("_", " ").title()
302
+ tree.insert(section_level, "end", text=title_option, values=(value,))
303
+
304
+ self._make_tree_editable(editor_frame, tree)
305
+
306
+ return tree
307
+
308
+ def _configure_save_button(self, button_frame, tree):
309
+ """Save updated options and values from the treeview back to the config file."""
310
+
311
+ def save_config():
312
+ for child in tree.get_children():
313
+ for item in tree.get_children(child):
314
+ title_option = tree.item(item, "text")
315
+ config_option = title_option.lower().replace(" ", "_")
316
+ value = tree.item(item, "values")[0]
317
+ section = tree.parent(item)
318
+ section_name = tree.item(section, "text")
319
+ if section_name == "Custom Items":
320
+ # custom items are already saved upon creation (Saving them again would result in duplicates)
321
+ continue
322
+ self.scraper.config.set(section_name, config_option, value)
323
+
324
+ self.scraper.config.write_to_file()
325
+ if self.scraper.config.valid:
326
+ messagebox.showinfo(
327
+ "Config Saved", "The configuration has been saved successfully."
328
+ )
329
+ else:
330
+ messagebox.showerror(
331
+ "Config Error",
332
+ f"The configuration is invalid. ({self.scraper.config.last_error})",
333
+ )
334
+
335
+ save_button = ttk.Button(button_frame, text="Save", command=save_config)
336
+ save_button.pack(side="left", expand=True, padx=5)
337
+
338
+ def _configure_custom_item_button(self, button_frame, tree):
339
+ """Add a button that opens an entry dialog to add a custom item to the
340
+ configuration.
341
+ """
209
342
 
210
- def _confirm_reset_config(self):
211
- confirm = messagebox.askokcancel(
212
- "Reset Config", "Are you sure you want to reset the config file?"
343
+ def add_custom_item(item_url, item_owned):
344
+ """Add a custom item to the configuration."""
345
+ if not item_url or not item_owned:
346
+ messagebox.showerror("Input Error", "All fields must be filled out.")
347
+ return
348
+
349
+ try:
350
+ if int(item_owned) < 0:
351
+ raise ValueError("Owned count must be a non-negative integer.")
352
+ except ValueError as error:
353
+ messagebox.showerror("Input Error", f"Invalid owned count: {error}")
354
+ return
355
+
356
+ self.scraper.config.set("Custom Items", item_url, item_owned)
357
+ self.scraper.config.write_to_file()
358
+ if self.scraper.config.valid:
359
+ tree.insert("Custom Items", "end", text=item_url, values=(item_owned,))
360
+ messagebox.showinfo("Custom Item Added", "Custom item has been added successfully.")
361
+ else:
362
+ self.scraper.config.remove_option("Custom Items", item_url)
363
+ messagebox.showerror(
364
+ "Config Error",
365
+ f"The configuration is invalid. ({self.scraper.config.last_error})",
366
+ )
367
+
368
+ def open_custom_item_dialog():
369
+ """Open a dialog to enter custom item details."""
370
+ dialog = tk.Toplevel(self.config_editor_window)
371
+ dialog.title(NEW_CUSTOM_ITEM_TITLE)
372
+ dialog.geometry(NEW_CUSTOM_ITEM_SIZE)
373
+
374
+ dialog_frame = ttk.Frame(dialog, padding=10)
375
+ dialog_frame.pack(expand=True, fill="both")
376
+
377
+ ttk.Label(dialog_frame, text="Item URL:").pack(pady=5)
378
+ item_url_entry = ttk.Entry(dialog_frame)
379
+ item_url_entry.pack(fill="x", padx=10)
380
+
381
+ ttk.Label(dialog_frame, text="Owned Count:").pack(pady=5)
382
+ item_owned_entry = ttk.Entry(dialog_frame)
383
+ item_owned_entry.pack(fill="x", padx=10)
384
+
385
+ add_button = ttk.Button(
386
+ dialog_frame,
387
+ text="Add",
388
+ command=lambda: add_custom_item(item_url_entry.get(), item_owned_entry.get()),
389
+ )
390
+ add_button.pack(pady=10)
391
+
392
+ custom_item_button = ttk.Button(
393
+ button_frame, text="Add Custom Item", command=open_custom_item_dialog
213
394
  )
214
- if confirm:
215
- self._reset_config()
395
+ custom_item_button.pack(side="left", expand=True, padx=5)
396
+
397
+ def _configure_editor_frame(self):
398
+ """Configure the main editor frame which displays the configuration options in a
399
+ structured way.
400
+ """
401
+ editor_frame = ttk.Frame(self.config_editor_window, padding=30)
402
+ editor_frame.pack(expand=True, fill="both")
403
+
404
+ tree = self._configure_treeview(editor_frame)
405
+ tree.pack(expand=True, fill="both")
406
+
407
+ button_frame = ttk.Frame(editor_frame, padding=10)
408
+
409
+ self._configure_save_button(button_frame, tree)
410
+ self._configure_custom_item_button(button_frame, tree)
411
+
412
+ button_frame.pack(side="bottom", padx=10, pady=(0, 10))
413
+
414
+ def _edit_config(self):
415
+ """Open a new window with a config editor GUI."""
416
+ self.config_editor_window = tk.Toplevel(self.application_window)
417
+ self.config_editor_window.geometry(CONFIG_EDITOR_SIZE)
418
+ self.config_editor_window.title(CONFIG_EDITOR_TITLE)
419
+
420
+ self._configure_editor_frame()
216
421
 
217
422
  def _reset_config(self):
218
423
  """Reset the configuration file to its default state."""
219
- copy(CONFIG_FILE_BACKUP, CONFIG_FILE)
220
- self.scraper.parse_config()
424
+ confirm = messagebox.askokcancel(
425
+ "Reset Config", "Are you sure you want to reset the configuration?"
426
+ )
427
+ if confirm:
428
+ copy(CONFIG_FILE_BACKUP, CONFIG_FILE)
429
+ self.scraper.load_config()
221
430
 
222
431
  def _draw_plot(self):
223
432
  """Draw a plot of the scraped prices over time."""
224
- dates, dollars, euros = self.scraper.read_price_log()
433
+ dates, usd_prices, eur_prices = PriceLogs.read()
225
434
 
226
435
  fig, ax_raw = plt.subplots(figsize=(10, 8), num="CS2Tracker Price History")
227
436
  fig.suptitle("CS2Tracker Price History", fontsize=16)
228
437
  fig.autofmt_xdate()
229
438
 
230
439
  ax = cast(Axes, ax_raw)
231
- ax.plot(dates, dollars, label="Dollars")
232
- ax.plot(dates, euros, label="Euros")
440
+ ax.plot(dates, usd_prices, label="Dollars")
441
+ ax.plot(dates, eur_prices, label="Euros")
233
442
  ax.legend()
234
- date_formatter = DateFormatter("%d-%m-%Y")
443
+ date_formatter = DateFormatter("%Y-%m-%d")
235
444
  ax.xaxis.set_major_formatter(date_formatter)
236
445
 
237
446
  plt.show()
238
447
 
239
- def _edit_log_file(self):
240
- """Opens the file containing past price calculations."""
241
- Popen([TEXT_EDITOR, OUTPUT_FILE], shell=True)
448
+ def _export_log_file(self):
449
+ """Lets the user export the log file to a different location."""
450
+ export_path = asksaveasfile(
451
+ title="Export Log File",
452
+ defaultextension=".csv",
453
+ filetypes=[("CSV files", "*.csv")],
454
+ )
455
+ if export_path:
456
+ copy(OUTPUT_FILE, export_path.name)
457
+
458
+ def _import_log_file(self):
459
+ """Lets the user import a log file from a different location."""
460
+ import_path = askopenfilename(
461
+ title="Import Log File",
462
+ defaultextension=".csv",
463
+ filetypes=[("CSV files", "*.csv")],
464
+ )
465
+ if not PriceLogs.validate_file(import_path):
466
+ return
467
+ copy(import_path, OUTPUT_FILE)
242
468
 
243
469
  def _toggle_background_task(self, enabled: bool):
244
470
  """Toggle whether a daily price calculation should run in the background."""
245
- self.scraper.toggle_background_task(enabled)
471
+ BackgroundTask.toggle(enabled)
246
472
 
247
473
  def _toggle_use_proxy(self, enabled: bool):
248
474
  """Toggle whether the scraper should use proxy servers for requests."""
475
+ proxy_api_key = self.scraper.config.get("User Settings", "proxy_api_key", fallback=None)
476
+ if not proxy_api_key and enabled:
477
+ messagebox.showerror(
478
+ "Config Error",
479
+ "You need to enter a valid crawlbase API key into the config file to use this feature.",
480
+ )
481
+ return False
482
+
249
483
  self.scraper.toggle_use_proxy(enabled)
484
+ return True
250
485
 
251
486
  def _toggle_discord_webhook(self, enabled: bool):
252
487
  """Toggle whether the scraper should send notifications to a Discord webhook."""
488
+ discord_webhook_url = self.scraper.config.get(
489
+ "User Settings", "discord_webhook_url", fallback=None
490
+ )
491
+ if not discord_webhook_url and enabled:
492
+ messagebox.showerror(
493
+ "Config Error",
494
+ "You need to enter a valid Discord webhook URL into the config file to use this feature.",
495
+ )
496
+ return False
497
+
253
498
  self.scraper.toggle_discord_webhook(enabled)
499
+ return True
254
500
 
255
501
 
256
502
  def _popen_and_call(popen_args, callback):
@@ -0,0 +1,109 @@
1
+ import os
2
+ from subprocess import DEVNULL, call
3
+
4
+ from cs2tracker.constants import (
5
+ BATCH_FILE,
6
+ OS,
7
+ PROJECT_DIR,
8
+ PYTHON_EXECUTABLE,
9
+ RUNNING_IN_EXE,
10
+ OSType,
11
+ )
12
+ from cs2tracker.padded_console import PaddedConsole
13
+
14
+ WIN_BACKGROUND_TASK_NAME = "CS2Tracker Daily Calculation"
15
+ WIN_BACKGROUND_TASK_SCHEDULE = "DAILY"
16
+ WIN_BACKGROUND_TASK_TIME = "12:00"
17
+ WIN_BACKGROUND_TASK_CMD = (
18
+ f"powershell -WindowStyle Hidden -Command \"Start-Process '{BATCH_FILE}' -WindowStyle Hidden\""
19
+ )
20
+
21
+ console = PaddedConsole()
22
+
23
+
24
+ class BackgroundTask:
25
+ @classmethod
26
+ def identify(cls):
27
+ """
28
+ Search the OS for a daily background task that runs the scraper.
29
+
30
+ :return: True if a background task is found, False otherwise.
31
+ """
32
+ if OS == OSType.WINDOWS:
33
+ cmd = ["schtasks", "/query", "/tn", WIN_BACKGROUND_TASK_NAME]
34
+ return_code = call(cmd, stdout=DEVNULL, stderr=DEVNULL)
35
+ found = return_code == 0
36
+ return found
37
+ else:
38
+ # TODO: implement finder for cron jobs
39
+ return False
40
+
41
+ @classmethod
42
+ def _toggle_batch_file(cls, enabled: bool):
43
+ """
44
+ Create or delete a batch file that runs the scraper.
45
+
46
+ :param enabled: If True, the batch file will be created; if False, the batch
47
+ file will be deleted.
48
+ """
49
+ if enabled:
50
+ with open(BATCH_FILE, "w", encoding="utf-8") as batch_file:
51
+ if RUNNING_IN_EXE:
52
+ # The python executable is set to the executable itself
53
+ # for executables created with PyInstaller
54
+ batch_file.write(f"{PYTHON_EXECUTABLE} --only-scrape\n")
55
+ else:
56
+ batch_file.write(f"cd {PROJECT_DIR}\n")
57
+ batch_file.write(f"{PYTHON_EXECUTABLE} -m cs2tracker --only-scrape\n")
58
+ else:
59
+ if os.path.exists(BATCH_FILE):
60
+ os.remove(BATCH_FILE)
61
+
62
+ @classmethod
63
+ def _toggle_windows(cls, enabled: bool):
64
+ """
65
+ Create or delete a daily background task that runs the scraper on Windows.
66
+
67
+ :param enabled: If True, the task will be created; if False, the task will be
68
+ deleted.
69
+ """
70
+ cls._toggle_batch_file(enabled)
71
+ if enabled:
72
+ cmd = [
73
+ "schtasks",
74
+ "/create",
75
+ "/tn",
76
+ WIN_BACKGROUND_TASK_NAME,
77
+ "/tr",
78
+ WIN_BACKGROUND_TASK_CMD,
79
+ "/sc",
80
+ WIN_BACKGROUND_TASK_SCHEDULE,
81
+ "/st",
82
+ WIN_BACKGROUND_TASK_TIME,
83
+ ]
84
+ return_code = call(cmd, stdout=DEVNULL, stderr=DEVNULL)
85
+ if return_code == 0:
86
+ console.print("[bold green][+] Background task enabled.")
87
+ else:
88
+ console.print("[bold red][!] Failed to enable background task.")
89
+ else:
90
+ cmd = ["schtasks", "/delete", "/tn", WIN_BACKGROUND_TASK_NAME, "/f"]
91
+ return_code = call(cmd, stdout=DEVNULL, stderr=DEVNULL)
92
+ if return_code == 0:
93
+ console.print("[bold green][-] Background task disabled.")
94
+ else:
95
+ console.print("[bold red][!] Failed to disable background task.")
96
+
97
+ @classmethod
98
+ def toggle(cls, enabled: bool):
99
+ """
100
+ Create or delete a daily background task that runs the scraper.
101
+
102
+ :param enabled: If True, the task will be created; if False, the task will be
103
+ deleted.
104
+ """
105
+ if OS == OSType.WINDOWS:
106
+ cls._toggle_windows(enabled)
107
+ else:
108
+ # TODO: implement toggle for cron jobs
109
+ pass
cs2tracker/constants.py CHANGED
@@ -18,7 +18,6 @@ class OSType(enum.Enum):
18
18
 
19
19
 
20
20
  OS = OSType.WINDOWS if sys.platform.startswith("win") else OSType.LINUX
21
- TEXT_EDITOR = "notepad" if OS == OSType.WINDOWS else "nano"
22
21
  PYTHON_EXECUTABLE = sys.executable
23
22
 
24
23
 
@@ -81,10 +80,10 @@ AUTHOR_STRING = (
81
80
 
82
81
  POWERSHELL_COLORIZE_OUTPUT = (
83
82
  "%{ "
84
- "if($_ -match 'Version|\\\\|_') { Write-Host $_ -ForegroundColor yellow } "
83
+ "if($_ -match '\\[!\\]') { Write-Host $_ -ForegroundColor red } "
84
+ "elseif($_ -match 'Version|\\\\|_') { Write-Host $_ -ForegroundColor yellow } "
85
85
  "elseif($_ -match 'USD|EUR|^[-|\\s]+$') { Write-Host $_ -ForegroundColor green } "
86
86
  "elseif($_ -match 'Case|Capsule|-[A-Za-z0-9]') { Write-Host $_ -ForegroundColor magenta } "
87
- "elseif($_ -match '\\[!\\]') { Write-Host $_ -ForegroundColor red } "
88
87
  "elseif($_ -match 'Legends|Challengers|Contenders|Champions|Finalists') { Write-Host $_ -ForegroundColor blue } "
89
88
  "else { Write-Host $_ } "
90
89
  "}"