Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Der-Henning
GitHub Repository: Der-Henning/tgtg
Path: blob/main/tgtg_scanner/scanner.py
1494 views
1
import logging
2
import sys
3
from random import random
4
from time import sleep
5
from typing import NoReturn
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: Notifiers | None = None
56
self.location: 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
apk_version=self.config.tgtg.apk_version,
68
user_agent=self.config.tgtg.user_agent,
69
port=self.config.port,
70
)
71
self.reservations = Reservations(self.tgtg_client)
72
self.favorites = Favorites(self.tgtg_client)
73
74
def _get_test_item(self) -> Item:
75
"""Returns an item for test notifications."""
76
items = sorted(self._get_favorites(), key=lambda x: x.items_available, reverse=True)
77
78
if items:
79
return items[0]
80
items = sorted(
81
[
82
Item(item, self.location, self.config.locale, self.config.time_format)
83
for item in self.tgtg_client.get_items(favorites_only=False, latitude=53.5511, longitude=9.9937, radius=50)
84
],
85
key=lambda x: x.items_available,
86
reverse=True,
87
)
88
89
return items[0]
90
91
def _job(self) -> None:
92
"""Job iterates over all monitored items."""
93
if self.notifiers is None:
94
raise RuntimeError("Notifiers not initialized!")
95
96
items: list[Item] = []
97
for item_id in self.item_ids:
98
try:
99
if item_id != "":
100
item_dict = self.tgtg_client.get_item(item_id)
101
items.append(Item(item_dict, self.location, self.config.locale, self.config.time_format))
102
except TgtgAPIError as err:
103
log.error(err)
104
items += self._get_favorites()
105
for item in items:
106
self._check_item(item)
107
108
amounts = {item_id: item.items_available for item_id, item in self.state.items() if item is not None}
109
log.debug("new State: %s", amounts)
110
self.reservations.make_orders(self.state, self.notifiers.send)
111
112
if len(self.state) == 0:
113
log.warning("No items in observation! Did you add any favorites?")
114
115
self.config.save_tokens(
116
self.tgtg_client.access_token,
117
self.tgtg_client.refresh_token,
118
self.tgtg_client.datadome_cookie,
119
)
120
121
def _get_favorites(self) -> list[Item]:
122
"""Get favorites as list of Items.
123
124
Returns:
125
List: List of items
126
127
"""
128
try:
129
items = self.get_favorites()
130
except TgtgAPIError as err:
131
log.error(err)
132
return []
133
return [Item(item, self.location, self.config.locale, self.config.time_format) for item in items]
134
135
def _check_item(self, item: Item) -> None:
136
"""Checks if the available item amount raised from zero to something or price changed
137
and triggers notifications.
138
"""
139
state_item = self.state.get(item.item_id)
140
if state_item is not None:
141
item._previous_price = state_item._price
142
send_notification = False
143
if state_item.items_available != item.items_available:
144
log.info("%s - amount changed from %s to %s", item.display_name, state_item.items_available, item.items_available)
145
if state_item.items_available == 0:
146
send_notification = True
147
if state_item.price != item.price:
148
log.info("%s - price changed from %ss to %s", item.display_name, state_item.price, item.price)
149
if self.config.price_monitoring and item.items_available > 0 and item._price < state_item._price:
150
send_notification = True
151
if send_notification:
152
self._send_messages(item)
153
self.metrics.send_notifications.labels(item.item_id, item.display_name).inc()
154
self.metrics.update(item)
155
self.state[item.item_id] = item
156
157
def _send_messages(self, item: Item) -> None:
158
"""Send notifications for Item."""
159
if self.notifiers is None:
160
raise RuntimeError("Notifiers not initialized!")
161
162
log.info(
163
"Sending notifications for %s - %s bags available",
164
item.display_name,
165
item.items_available,
166
)
167
self.notifiers.send(item)
168
169
def run(self) -> NoReturn:
170
"""Main Loop of the Scanner."""
171
# test tgtg API
172
self.tgtg_client.login()
173
self.config.save_tokens(
174
self.tgtg_client.access_token,
175
self.tgtg_client.refresh_token,
176
self.tgtg_client.datadome_cookie,
177
)
178
# activate location service
179
self.location = Location(
180
self.config.location.enabled,
181
self.config.location.google_maps_api_key,
182
self.config.location.origin_address,
183
)
184
# activate and test notifiers
185
if self.config.metrics:
186
self.metrics.enable_metrics()
187
self.notifiers = Notifiers(self.config, self.reservations, self.favorites)
188
self.notifiers.start()
189
if not self.config.disable_tests and self.notifiers.notifier_count > 0:
190
log.info("Sending test Notifications ...")
191
self.notifiers.send(self._get_test_item())
192
# start scanner
193
log.info("Scanner started ...")
194
running = True
195
if self.cron != Cron("* * * * *"):
196
log.info("Active on schedule: %s", self.cron.get_description(self.config.locale))
197
activity = Activity(self.config.activity and not (self.config.docker or self.config.quiet))
198
while True:
199
if self.cron.is_now:
200
if not running:
201
log.info("Scanner reenabled by cron schedule.")
202
running = True
203
try:
204
self._job()
205
except Exception:
206
log.error("Job Error! - %s", sys.exc_info())
207
finally:
208
sleep_time = self.config.sleep_time * (0.9 + 0.2 * random())
209
for _ in range(int(sleep_time)):
210
activity.next()
211
sleep(sleep_time / int(sleep_time))
212
activity.flush()
213
elif running:
214
log.info("Scanner disabled by cron schedule.")
215
running = False
216
else:
217
sleep(60)
218
219
def stop(self) -> None:
220
"""Stop scanner."""
221
if self.notifiers:
222
self.notifiers.stop()
223
224
def get_credentials(self) -> dict:
225
"""Returns current tgtg credentials.
226
227
Returns:
228
dict: dictionary containing access token, refresh token,
229
user id and datadome cookie
230
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
"""
246
return self.tgtg_client.get_items(
247
favorites_only=False,
248
latitude=lat,
249
longitude=lng,
250
radius=radius,
251
)
252
253
def get_favorites(self) -> list[dict]:
254
"""Returns favorites of the current tgtg account.
255
256
Returns:
257
List: List of items
258
259
"""
260
return self.tgtg_client.get_favorites()
261
262
def set_favorite(self, item_id: str) -> None:
263
"""Add item to favorites.
264
265
Args:
266
item_id (str): Item ID
267
268
"""
269
self.tgtg_client.set_favorite(item_id=item_id, is_favorite=True)
270
271
def unset_favorite(self, item_id: str) -> None:
272
"""Remove item from favorites.
273
274
Args:
275
item_id (str): Item ID
276
277
"""
278
self.tgtg_client.set_favorite(item_id=item_id, is_favorite=False)
279
280
def unset_all_favorites(self) -> None:
281
"""Remove all items from favorites."""
282
item_ids = [item.get("item", {}).get("item_id") for item in self.get_favorites()]
283
for item_id in item_ids:
284
self.unset_favorite(item_id)
285
286
287
if __name__ == "__main__":
288
print("Please use __main__.py.")
289
290