Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Der-Henning
GitHub Repository: Der-Henning/tgtg
Path: blob/main/tgtg_scanner/scanner.py
725 views
1
import logging
2
import sys
3
from random import random
4
from time import sleep
5
from typing import Dict, List, NoReturn, Union
6
7
from progress.spinner import Spinner
8
9
from tgtg_scanner.errors import TgtgAPIError
10
from tgtg_scanner.models import (
11
Config,
12
Cron,
13
Favorites,
14
Item,
15
Location,
16
Metrics,
17
Reservations,
18
)
19
from tgtg_scanner.notifiers import Notifiers
20
from tgtg_scanner.tgtg import TgtgClient
21
22
log = logging.getLogger("tgtg")
23
24
25
class Activity:
26
"""Activity class that creates a spinner if active is True"""
27
28
def __init__(self, active: bool):
29
self.active = active
30
self.spinner = None
31
if self.active:
32
self.spinner = Spinner("Scanning... ")
33
34
def next(self) -> None:
35
"""Next function that updates the spinner"""
36
if self.spinner:
37
self.spinner.next()
38
39
def flush(self) -> None:
40
"""Flush function that flushes the spinner"""
41
if self.spinner:
42
sys.stdout.write("\x1b[80D\x1b[K")
43
sys.stdout.flush()
44
45
46
class Scanner:
47
"""Main Scanner class"""
48
49
def __init__(self, config: Config):
50
self.config = config
51
self.metrics = Metrics(self.config.metrics_port)
52
self.item_ids = set(self.config.item_ids)
53
self.cron = self.config.schedule_cron
54
self.state: Dict[str, Item] = {}
55
self.notifiers: Union[Notifiers, None] = None
56
self.location: Union[Location, None] = None
57
self.tgtg_client = TgtgClient(
58
email=self.config.tgtg.username,
59
timeout=self.config.tgtg.timeout,
60
access_token_lifetime=self.config.tgtg.access_token_lifetime,
61
max_polling_tries=self.config.tgtg.max_polling_tries,
62
polling_wait_time=self.config.tgtg.polling_wait_time,
63
access_token=self.config.tgtg.access_token,
64
refresh_token=self.config.tgtg.refresh_token,
65
datadome_cookie=self.config.tgtg.datadome,
66
base_url=self.config.tgtg.base_url,
67
)
68
self.reservations = Reservations(self.tgtg_client)
69
self.favorites = Favorites(self.tgtg_client)
70
71
def _get_test_item(self) -> Item:
72
"""
73
Returns an item for test notifications
74
"""
75
items = sorted(self._get_favorites(), key=lambda x: x.items_available, reverse=True)
76
77
if items:
78
return items[0]
79
items = sorted(
80
[
81
Item(item, self.location, self.config.locale)
82
for item in self.tgtg_client.get_items(favorites_only=False, latitude=53.5511, longitude=9.9937, radius=50)
83
],
84
key=lambda x: x.items_available,
85
reverse=True,
86
)
87
88
return items[0]
89
90
def _job(self) -> None:
91
"""
92
Job iterates over all monitored items
93
"""
94
if self.notifiers is None:
95
raise RuntimeError("Notifiers not initialized!")
96
97
items: list[Item] = []
98
for item_id in self.item_ids:
99
try:
100
if item_id != "":
101
item_dict = self.tgtg_client.get_item(item_id)
102
items.append(Item(item_dict, self.location, self.config.locale))
103
except TgtgAPIError as err:
104
log.error(err)
105
items += self._get_favorites()
106
for item in items:
107
self._check_item(item)
108
109
amounts = {item_id: item.items_available for item_id, item in self.state.items() if item is not None}
110
log.debug("new State: %s", amounts)
111
self.reservations.make_orders(self.state, self.notifiers.send)
112
113
if len(self.state) == 0:
114
log.warning("No items in observation! Did you add any favorites?")
115
116
self.config.save_tokens(
117
self.tgtg_client.access_token,
118
self.tgtg_client.refresh_token,
119
self.tgtg_client.datadome_cookie,
120
)
121
122
def _get_favorites(self) -> list[Item]:
123
"""
124
Get favorites as list of Items
125
126
Returns:
127
List: List of items
128
"""
129
try:
130
items = self.get_favorites()
131
except TgtgAPIError as err:
132
log.error(err)
133
return []
134
return [Item(item, self.location, self.config.locale) for item in items]
135
136
def _check_item(self, item: Item) -> None:
137
"""
138
Checks if the available item amount raised from zero to something
139
and triggers notifications.
140
"""
141
state_item = self.state.get(item.item_id)
142
if state_item is not None:
143
if state_item.items_available == item.items_available:
144
return
145
log.info("%s - new amount: %s", item.display_name, item.items_available)
146
if state_item.items_available == 0 and item.items_available > 0:
147
self._send_messages(item)
148
self.metrics.send_notifications.labels(item.item_id, item.display_name).inc()
149
self.metrics.update(item)
150
self.state[item.item_id] = item
151
152
def _send_messages(self, item: Item) -> None:
153
"""
154
Send notifications for Item
155
"""
156
if self.notifiers is None:
157
raise RuntimeError("Notifiers not initialized!")
158
159
log.info(
160
"Sending notifications for %s - %s bags available",
161
item.display_name,
162
item.items_available,
163
)
164
self.notifiers.send(item)
165
166
def run(self) -> NoReturn:
167
"""
168
Main Loop of the Scanner
169
"""
170
# test tgtg API
171
self.tgtg_client.login()
172
self.config.save_tokens(
173
self.tgtg_client.access_token,
174
self.tgtg_client.refresh_token,
175
self.tgtg_client.datadome_cookie,
176
)
177
# activate location service
178
self.location = Location(
179
self.config.location.enabled,
180
self.config.location.google_maps_api_key,
181
self.config.location.origin_address,
182
)
183
# activate and test notifiers
184
if self.config.metrics:
185
self.metrics.enable_metrics()
186
self.notifiers = Notifiers(self.config, self.reservations, self.favorites)
187
self.notifiers.start()
188
if not self.config.disable_tests and self.notifiers.notifier_count > 0:
189
log.info("Sending test Notifications ...")
190
self.notifiers.send(self._get_test_item())
191
# start scanner
192
log.info("Scanner started ...")
193
running = True
194
if self.cron != Cron("* * * * *"):
195
log.info("Active on schedule: %s", self.cron.get_description(self.config.locale))
196
activity = Activity(self.config.activity and not (self.config.docker or self.config.quiet))
197
while True:
198
if self.cron.is_now:
199
if not running:
200
log.info("Scanner reenabled by cron schedule.")
201
running = True
202
try:
203
self._job()
204
except Exception:
205
log.error("Job Error! - %s", sys.exc_info())
206
finally:
207
sleep_time = self.config.sleep_time * (0.9 + 0.2 * random())
208
for _ in range(int(sleep_time)):
209
activity.next()
210
sleep(sleep_time / int(sleep_time))
211
activity.flush()
212
elif running:
213
log.info("Scanner disabled by cron schedule.")
214
running = False
215
else:
216
sleep(60)
217
218
def stop(self) -> None:
219
"""
220
Stop scanner.
221
"""
222
if self.notifiers:
223
self.notifiers.stop()
224
225
def get_credentials(self) -> dict:
226
"""Returns current tgtg credentials.
227
228
Returns:
229
dict: dictionary containing access token, refresh token,
230
user id and datadome cookie
231
"""
232
return self.tgtg_client.get_credentials()
233
234
def get_items(self, lat, lng, radius) -> List[dict]:
235
"""Get items by geographic position.
236
237
Args:
238
lat (float): latitude
239
lng (float): longitude
240
radius (int): radius in meter
241
242
Returns:
243
List: List of found items
244
"""
245
return self.tgtg_client.get_items(
246
favorites_only=False,
247
latitude=lat,
248
longitude=lng,
249
radius=radius,
250
)
251
252
def get_favorites(self) -> List[dict]:
253
"""Returns favorites of the current tgtg account
254
255
Returns:
256
List: List of items
257
"""
258
return self.tgtg_client.get_favorites()
259
260
def set_favorite(self, item_id: str) -> None:
261
"""Add item to favorites.
262
263
Args:
264
item_id (str): Item ID
265
"""
266
self.tgtg_client.set_favorite(item_id=item_id, is_favorite=True)
267
268
def unset_favorite(self, item_id: str) -> None:
269
"""Remove item from favorites.
270
271
Args:
272
item_id (str): Item ID
273
"""
274
self.tgtg_client.set_favorite(item_id=item_id, is_favorite=False)
275
276
def unset_all_favorites(self) -> None:
277
"""Remove all items from favorites."""
278
item_ids = [item.get("item", {}).get("item_id") for item in self.get_favorites()]
279
for item_id in item_ids:
280
self.unset_favorite(item_id)
281
282
283
if __name__ == "__main__":
284
print("Please use __main__.py.")
285
286