cocorum 2.6.0__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.
- cocorum/__init__.py +675 -0
- cocorum/chatapi.py +770 -0
- cocorum/jsonhandles.py +95 -0
- cocorum/scraping.py +700 -0
- cocorum/servicephp.py +877 -0
- cocorum/static.py +146 -0
- cocorum/uploadphp.py +403 -0
- cocorum/utils.py +288 -0
- cocorum-2.6.0.dist-info/METADATA +109 -0
- cocorum-2.6.0.dist-info/RECORD +12 -0
- cocorum-2.6.0.dist-info/WHEEL +4 -0
- cocorum-2.6.0.dist-info/licenses/LICENSE.txt +674 -0
cocorum/__init__.py
ADDED
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""An unofficial Python wrapper for the Rumble.com APIs
|
|
3
|
+
|
|
4
|
+
A Python wrapper for the Rumble Live Stream API v1.0 (beta), with some quality of life additions, such as:
|
|
5
|
+
- Automatic refresh when past the refresh_rate delay when querying any non_static property.
|
|
6
|
+
- All timespamps are parsed to seconds since Epoch, UTC timezone.
|
|
7
|
+
- Chat has new_messages and new_rants properties that return only messages and rants since the last time they were read.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
Modules exported by this package:
|
|
11
|
+
|
|
12
|
+
- `chatapi`: Provide the ChatAPI object for interacting with a livestream chat.
|
|
13
|
+
- `servicephp`: Privide the ServicePHP object for interacting with the service.php API.
|
|
14
|
+
- `uploadphp`: Provide the UploadPHP object for uploading videos.
|
|
15
|
+
- `scraping`: Provide functions and the Scraper object for getting various data via HTML scraping.
|
|
16
|
+
- `jsonhandles`: Abstract classes for handling JSON data blocks.
|
|
17
|
+
- `utils`: Various utility functions for internal calculations and checks.
|
|
18
|
+
- `static`: Global data that does not change across the package.
|
|
19
|
+
|
|
20
|
+
Example usage:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
from cocorum import RumbleAPI
|
|
24
|
+
|
|
25
|
+
## API_URL is Rumble Live Stream API URL with key
|
|
26
|
+
api = RumbleAPI(API_URL, refresh_rate = 10)
|
|
27
|
+
|
|
28
|
+
print(api.username)
|
|
29
|
+
## Should display your Rumble username
|
|
30
|
+
|
|
31
|
+
print("Latest follower:", api.latest_follower)
|
|
32
|
+
## Should display your latest follower, or None if you have none.
|
|
33
|
+
|
|
34
|
+
if api.latest_subscriber:
|
|
35
|
+
print(api.latest_subscriber, f"subscribed for ${api.latest_subscriber.amount_dollars}")
|
|
36
|
+
## Should display your latest subscriber if you have one.
|
|
37
|
+
|
|
38
|
+
livestream = api.latest_livestream # None if there is no stream running
|
|
39
|
+
|
|
40
|
+
if livestream:
|
|
41
|
+
print(livestream.title)
|
|
42
|
+
print("Stream visibility is", livestream.visibility)
|
|
43
|
+
|
|
44
|
+
#We will use this later
|
|
45
|
+
STREAM_ID = livestream.stream_id
|
|
46
|
+
|
|
47
|
+
print("Stream ID is", STREAM_ID)
|
|
48
|
+
|
|
49
|
+
import time # We'll need this Python builtin for delays and knowing when to stop
|
|
50
|
+
|
|
51
|
+
# Get messages for one minute
|
|
52
|
+
start_time = time.time()
|
|
53
|
+
|
|
54
|
+
# Continue as long as we haven't been going for a whole minute, and the livestream is still live
|
|
55
|
+
while time.time() - start_time < 60 and livestream.is_live:
|
|
56
|
+
# For each new message...
|
|
57
|
+
for message in livestream.chat.new_messages:
|
|
58
|
+
# Display it
|
|
59
|
+
print(message.username, "said", message)
|
|
60
|
+
|
|
61
|
+
# Wait a bit, just to keep the loop from maxxing a CPU core
|
|
62
|
+
time.sleep(0.1)
|
|
63
|
+
```
|
|
64
|
+
S.D.G."""
|
|
65
|
+
|
|
66
|
+
import time
|
|
67
|
+
import warnings
|
|
68
|
+
import requests
|
|
69
|
+
|
|
70
|
+
#Make all submodules available from base name
|
|
71
|
+
from . import chatapi, servicephp, uploadphp, scraping, jsonhandles, utils, static
|
|
72
|
+
|
|
73
|
+
from .jsonhandles import JSONObj, JSONUserAction
|
|
74
|
+
|
|
75
|
+
class Follower(JSONUserAction):
|
|
76
|
+
"""Rumble follower"""
|
|
77
|
+
@property
|
|
78
|
+
def followed_on(self):
|
|
79
|
+
"""When the follower followed, in seconds since Epoch UTC"""
|
|
80
|
+
return utils.parse_timestamp(self["followed_on"])
|
|
81
|
+
|
|
82
|
+
class Subscriber(JSONUserAction):
|
|
83
|
+
"""Rumble subscriber"""
|
|
84
|
+
def __eq__(self, other):
|
|
85
|
+
"""Is this subscriber equal to another?
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
other (str, JSONUserAction, Subscriber): The other object to compare to.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Comparison (bool, None): Did it fit the criteria?
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
#Check if the compared string is our username
|
|
95
|
+
if isinstance(other, str):
|
|
96
|
+
return self.username == other
|
|
97
|
+
|
|
98
|
+
#check if the compared number is our amount in cents
|
|
99
|
+
# if isinstance(other, (int, float)):
|
|
100
|
+
# return self.amount_cents == other
|
|
101
|
+
|
|
102
|
+
#Check if the compared object's username matches our own, if it has one
|
|
103
|
+
if hasattr(other, "username"):
|
|
104
|
+
#Check if the compared object's cost amout matches our own, if it has one
|
|
105
|
+
if hasattr(other, "amount_cents"):
|
|
106
|
+
return self.amount_cents == other.amount_cents
|
|
107
|
+
|
|
108
|
+
#Other object has no amount_cents attribute
|
|
109
|
+
return self.username == other.username
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def user(self):
|
|
113
|
+
"""AFAIK this is being deprecated, use username instead"""
|
|
114
|
+
return self["user"]
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def amount_cents(self):
|
|
118
|
+
"""The total subscription amount in cents"""
|
|
119
|
+
return self["amount_cents"]
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def amount_dollars(self):
|
|
123
|
+
"""The subscription amount in dollars"""
|
|
124
|
+
return self["amount_dollars"]
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def subscribed_on(self):
|
|
128
|
+
"""When the subscriber subscribed, in seconds since Epoch UTC"""
|
|
129
|
+
return utils.parse_timestamp(self["subscribed_on"])
|
|
130
|
+
|
|
131
|
+
class StreamCategory(JSONObj):
|
|
132
|
+
"""Category of a Rumble stream"""
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def slug(self):
|
|
136
|
+
"""Return the category's slug, AKA it's ID"""
|
|
137
|
+
return self["slug"]
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def title(self):
|
|
141
|
+
"""Return the category's title"""
|
|
142
|
+
return self["title"]
|
|
143
|
+
|
|
144
|
+
def __eq__(self, other):
|
|
145
|
+
"""Is this category equal to another?
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
other (str, StreamCategory): Other object to compare to.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Comparison (bool, None): Did it fit the criteria?
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
#Check if the compared string is our slug or title
|
|
155
|
+
if isinstance(other, str):
|
|
156
|
+
return other in (self.slug, self.title)
|
|
157
|
+
|
|
158
|
+
#Check if the compared object has the same slug, if it has one
|
|
159
|
+
if hasattr(other, "slug"):
|
|
160
|
+
return self.slug == other.slug
|
|
161
|
+
|
|
162
|
+
def __str__(self):
|
|
163
|
+
"""The category in string form"""
|
|
164
|
+
return self.title
|
|
165
|
+
|
|
166
|
+
class Livestream():
|
|
167
|
+
"""Rumble livestream"""
|
|
168
|
+
def __init__(self, jsondata, api):
|
|
169
|
+
"""Rumble livestream
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
jsondata (dict): The JSON block for a single livestream.
|
|
173
|
+
api (RumbleAPI): The Rumble Live Stream API wrapper that spawned us.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
self._jsondata = jsondata
|
|
177
|
+
self.api = api
|
|
178
|
+
self.is_disappeared = False #The livestream is in the API listing
|
|
179
|
+
self.__chat = LiveChat(self)
|
|
180
|
+
|
|
181
|
+
def __eq__(self, other):
|
|
182
|
+
"""Is this stream equal to another?
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
other (str, int, Livestream): Object to compare to.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Comparison (bool, None): Did it fit the criteria?
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
#Check if the compared string is our stream ID
|
|
192
|
+
if isinstance(other, str):
|
|
193
|
+
return self.stream_id == other #or self.title == other
|
|
194
|
+
|
|
195
|
+
#check if the compared number is our chat ID (linked to stream ID)
|
|
196
|
+
if isinstance(other, (int, float)):
|
|
197
|
+
return self.stream_id_b10 == other
|
|
198
|
+
|
|
199
|
+
#Check if the compared object has the same stream ID
|
|
200
|
+
if hasattr(other, "stream_id"):
|
|
201
|
+
return self.stream_id == utils.ensure_b36(other.stream_id)
|
|
202
|
+
|
|
203
|
+
#Check if the compared object has the same chat ID
|
|
204
|
+
if hasattr(other, "stream_id_b10"):
|
|
205
|
+
return self.stream_id_b10 == other.stream_id_b10
|
|
206
|
+
|
|
207
|
+
def __str__(self):
|
|
208
|
+
"""The livestream in string form (it's ID in base 36)"""
|
|
209
|
+
return self.stream_id
|
|
210
|
+
|
|
211
|
+
def __getitem__(self, key):
|
|
212
|
+
"""Return a key from the JSON, refreshing if necessary
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
key (str): A valid JSON key.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
#The livestream has not disappeared from the API listing,
|
|
219
|
+
#the key requested is not a value that doesn't change,
|
|
220
|
+
#and it has been api.refresh rate since the last time we refreshed
|
|
221
|
+
if (not self.is_disappeared) and (key not in static.StaticAPIEndpoints.main) and (time.time() - self.api.last_refresh_time > self.api.refresh_rate):
|
|
222
|
+
self.api.refresh()
|
|
223
|
+
|
|
224
|
+
return self._jsondata[key]
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def stream_id(self):
|
|
228
|
+
"""The livestream ID in base 36"""
|
|
229
|
+
return self["id"]
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def stream_id_b36(self):
|
|
233
|
+
"""The livestream ID in base 36"""
|
|
234
|
+
return self.stream_id
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def stream_id_b10(self):
|
|
238
|
+
"""The livestream chat ID (stream ID in base 10)"""
|
|
239
|
+
return utils.base_36_to_10(self.stream_id)
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
def title(self):
|
|
243
|
+
"""The title of the livestream"""
|
|
244
|
+
return self["title"]
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def created_on(self):
|
|
248
|
+
"""When the livestream was created, in seconds since the Epock UTC"""
|
|
249
|
+
return utils.parse_timestamp(self["created_on"])
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def is_live(self):
|
|
253
|
+
"""Is the stream live?"""
|
|
254
|
+
return self["is_live"] and not self.is_disappeared
|
|
255
|
+
|
|
256
|
+
@property
|
|
257
|
+
def visibility(self):
|
|
258
|
+
"""Is the stream public, unlisted, or private?"""
|
|
259
|
+
return self["visibility"]
|
|
260
|
+
|
|
261
|
+
@property
|
|
262
|
+
def categories(self):
|
|
263
|
+
"""A list of our categories"""
|
|
264
|
+
data = self["categories"].copy().values()
|
|
265
|
+
return [StreamCategory(jsondata_block) for jsondata_block in data]
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def likes(self):
|
|
269
|
+
"""Number of likes on the stream"""
|
|
270
|
+
return self["likes"]
|
|
271
|
+
|
|
272
|
+
@property
|
|
273
|
+
def dislikes(self):
|
|
274
|
+
"""Number of dislikes on the stream"""
|
|
275
|
+
return self["dislikes"]
|
|
276
|
+
|
|
277
|
+
@property
|
|
278
|
+
def like_ratio(self):
|
|
279
|
+
"""Ratio of people who liked the stream to people who reacted total"""
|
|
280
|
+
try:
|
|
281
|
+
return self.likes / (self.likes + self.dislikes)
|
|
282
|
+
|
|
283
|
+
except ZeroDivisionError:
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
@property
|
|
287
|
+
def watching_now(self):
|
|
288
|
+
"""The number of people watching now"""
|
|
289
|
+
return self["watching_now"]
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def chat(self):
|
|
293
|
+
"""The livestream chat"""
|
|
294
|
+
return self.__chat
|
|
295
|
+
|
|
296
|
+
class ChatMessage(JSONUserAction):
|
|
297
|
+
"""A single message in a Rumble livestream chat"""
|
|
298
|
+
def __eq__(self, other):
|
|
299
|
+
"""Is this message equal to another?
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
other (str, ChatMessage): Object to compare to.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Comparison (bool, None): Did it fit the criteria?
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
#Check if the compared string is our message
|
|
309
|
+
if isinstance(other, str):
|
|
310
|
+
return self.text == other
|
|
311
|
+
|
|
312
|
+
# #Check if the compared message has the same username and text (not needed)
|
|
313
|
+
# if type(other) == type(self):
|
|
314
|
+
# return (self.username, self.text) == (other.username, other.text)
|
|
315
|
+
|
|
316
|
+
#Check if the compared object has the same text
|
|
317
|
+
if hasattr(other, "text"):
|
|
318
|
+
#Check if the compared object has the same username, if it has one
|
|
319
|
+
if hasattr(other, "username"):
|
|
320
|
+
return (self.username, self.text) == (other.username, other.text)
|
|
321
|
+
|
|
322
|
+
return self.text == other.text #the other object had no username attribute
|
|
323
|
+
|
|
324
|
+
def __str__(self):
|
|
325
|
+
"""Message as a string (its content)"""
|
|
326
|
+
return self.text
|
|
327
|
+
|
|
328
|
+
@property
|
|
329
|
+
def text(self):
|
|
330
|
+
"""The message text"""
|
|
331
|
+
return self["text"]
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def created_on(self):
|
|
335
|
+
"""When the message was created, in seconds since Epoch UTC"""
|
|
336
|
+
return utils.parse_timestamp(self["created_on"])
|
|
337
|
+
|
|
338
|
+
@property
|
|
339
|
+
def badges(self):
|
|
340
|
+
"""The user's badges"""
|
|
341
|
+
return tuple(self["badges"].values())
|
|
342
|
+
|
|
343
|
+
class Rant(ChatMessage):
|
|
344
|
+
"""A single rant in a Rumble livestream chat"""
|
|
345
|
+
def __eq__(self, other):
|
|
346
|
+
"""Is this category equal to another?
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
other (str, ChatMessage): Object to compare to.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
Comparison (bool, None): Did it fit the criteria?
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
#Check if the compared string is our message
|
|
356
|
+
if isinstance(other, str):
|
|
357
|
+
return self.text == other
|
|
358
|
+
|
|
359
|
+
# #Check if the compared rant has the same username, amount, and text (unneccesary?)
|
|
360
|
+
# if type(other) == type(self):
|
|
361
|
+
# return (self.username, self.amount_cents, self.text) == (other.username, other.amount_cents, other.text)
|
|
362
|
+
|
|
363
|
+
#Check if the compared object has the same text
|
|
364
|
+
if hasattr(other, "text"):
|
|
365
|
+
#Check if the compared object has the same username, if it has one
|
|
366
|
+
if hasattr(other, "username"):
|
|
367
|
+
#Check if the compared object has the same cost amount, if it has one
|
|
368
|
+
if hasattr(other, "amount_cents"):
|
|
369
|
+
return (self.username, self.amount_cents, self.text) == (other.username, other.amount_cents, other.text)
|
|
370
|
+
|
|
371
|
+
#Other object has no amount_cents attribute
|
|
372
|
+
return (self.username, self.text) == (other.username, other.text)
|
|
373
|
+
|
|
374
|
+
#Other object had no username attribute
|
|
375
|
+
return self.text == other.text
|
|
376
|
+
|
|
377
|
+
@property
|
|
378
|
+
def expires_on(self):
|
|
379
|
+
"""When the rant will expire, in seconds since the Epoch UTC"""
|
|
380
|
+
return self["expires_on"]
|
|
381
|
+
|
|
382
|
+
@property
|
|
383
|
+
def amount_cents(self):
|
|
384
|
+
"""The total rant amount in cents"""
|
|
385
|
+
return self["amount_cents"]
|
|
386
|
+
|
|
387
|
+
@property
|
|
388
|
+
def amount_dollars(self):
|
|
389
|
+
"""The rant amount in dollars"""
|
|
390
|
+
return self["amount_dollars"]
|
|
391
|
+
|
|
392
|
+
class LiveChat():
|
|
393
|
+
"""Reference for chat of a Rumble livestream"""
|
|
394
|
+
def __init__(self, stream):
|
|
395
|
+
"""Reference for chat of a Rumble livestream
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
stream (dict): The JSON block of a single Rumble livestream.
|
|
399
|
+
"""
|
|
400
|
+
|
|
401
|
+
self.stream = stream
|
|
402
|
+
self.api = stream.api
|
|
403
|
+
self.last_newmessage_time = 0 #Last time we were checked for "new" messages
|
|
404
|
+
self.last_newrant_time = 0 #Last time we were checked for "new" rants
|
|
405
|
+
|
|
406
|
+
def __getitem__(self, key):
|
|
407
|
+
"""Return a key from the stream's chat JSON"""
|
|
408
|
+
return self.stream["chat"][key]
|
|
409
|
+
|
|
410
|
+
@property
|
|
411
|
+
def latest_message(self):
|
|
412
|
+
"""The latest chat message"""
|
|
413
|
+
if not self["latest_message"]:
|
|
414
|
+
return None #No-one has chatted on this stream yet
|
|
415
|
+
return ChatMessage(self["latest_message"])
|
|
416
|
+
|
|
417
|
+
@property
|
|
418
|
+
def recent_messages(self):
|
|
419
|
+
"""Recent chat messages"""
|
|
420
|
+
data = self["recent_messages"].copy()
|
|
421
|
+
return [ChatMessage(jsondata_block) for jsondata_block in data]
|
|
422
|
+
|
|
423
|
+
@property
|
|
424
|
+
def new_messages(self):
|
|
425
|
+
"""Chat messages that are newer than the last time this was referenced"""
|
|
426
|
+
rem = self.recent_messages.copy()
|
|
427
|
+
rem.sort(key = lambda x: x.created_on) #Sort the messages so the newest ones are last
|
|
428
|
+
|
|
429
|
+
#There are no recent messages, or all messages are older than the last time we checked
|
|
430
|
+
if not rem or rem[-1].created_on < self.last_newmessage_time:
|
|
431
|
+
return []
|
|
432
|
+
|
|
433
|
+
i = 0
|
|
434
|
+
for i, m in enumerate(rem):
|
|
435
|
+
if m.created_on > self.last_newmessage_time:
|
|
436
|
+
break #i is now the index of the oldest new message
|
|
437
|
+
|
|
438
|
+
self.last_newmessage_time = time.time()
|
|
439
|
+
return rem[i:]
|
|
440
|
+
|
|
441
|
+
@property
|
|
442
|
+
def latest_rant(self):
|
|
443
|
+
"""The latest chat rant"""
|
|
444
|
+
if not self["latest_rant"]:
|
|
445
|
+
return None #No-one has ranted on this stream yet
|
|
446
|
+
return Rant(self["latest_rant"])
|
|
447
|
+
|
|
448
|
+
@property
|
|
449
|
+
def recent_rants(self):
|
|
450
|
+
"""Recent chat rants"""
|
|
451
|
+
data = self["recent_rants"].copy()
|
|
452
|
+
return [Rant(jsondata_block) for jsondata_block in data]
|
|
453
|
+
|
|
454
|
+
@property
|
|
455
|
+
def new_rants(self):
|
|
456
|
+
"""Chat rants that are newer than the last time this was referenced"""
|
|
457
|
+
rera = self.recent_rants.copy()
|
|
458
|
+
rera.sort(key = lambda x: x.created_on) #Sort the rants so the newest ones are last
|
|
459
|
+
|
|
460
|
+
#There are no recent rants, or all rants are older than the last time we checked
|
|
461
|
+
if not rera or rera[-1].created_on < self.last_newrant_time:
|
|
462
|
+
return []
|
|
463
|
+
|
|
464
|
+
i = 0
|
|
465
|
+
for i, r in enumerate(rera):
|
|
466
|
+
if r.created_on > self.last_newrant_time:
|
|
467
|
+
break #i is now the index of the oldest new rant
|
|
468
|
+
|
|
469
|
+
self.last_newrant_time = time.time()
|
|
470
|
+
return rera[i:]
|
|
471
|
+
|
|
472
|
+
class RumbleAPI():
|
|
473
|
+
"""Rumble Live Stream API wrapper"""
|
|
474
|
+
def __init__(self, api_url, refresh_rate = static.Delays.api_refresh_default):
|
|
475
|
+
"""Rumble Live Stream API wrapper
|
|
476
|
+
Args:
|
|
477
|
+
api_url (str): The Rumble API URL, with the key.
|
|
478
|
+
refresh_rate (int, float): How long to reuse queried data before refreshing.
|
|
479
|
+
Defaults to static.Delays.api_refresh_default.
|
|
480
|
+
"""
|
|
481
|
+
|
|
482
|
+
self.refresh_rate = refresh_rate
|
|
483
|
+
self.last_refresh_time = 0
|
|
484
|
+
self.last_newfollower_time = time.time()
|
|
485
|
+
self.last_newsubscriber_time = time.time()
|
|
486
|
+
self.__livestreams = {}
|
|
487
|
+
self._jsondata = {}
|
|
488
|
+
self.api_url = api_url
|
|
489
|
+
|
|
490
|
+
#Warn about refresh rate being below minimum
|
|
491
|
+
if self.refresh_rate < static.Delays.api_refresh_minimum:
|
|
492
|
+
warnings.warn(f"Cocorum set to over-refresh, rate of {self.refresh_rate} seconds (less than {static.Delays.api_refresh_minimum})." + \
|
|
493
|
+
"Superscript must self-limit or Rumble will reject queries!")
|
|
494
|
+
|
|
495
|
+
@property
|
|
496
|
+
def api_url(self):
|
|
497
|
+
"""Our API URL"""
|
|
498
|
+
return self.__api_url
|
|
499
|
+
|
|
500
|
+
@api_url.setter
|
|
501
|
+
def api_url(self, url):
|
|
502
|
+
"""Set a new API URL, and refresh
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
url (str): The new API URL to use.
|
|
506
|
+
"""
|
|
507
|
+
|
|
508
|
+
self.__api_url = url
|
|
509
|
+
self.refresh()
|
|
510
|
+
|
|
511
|
+
def __getitem__(self, key):
|
|
512
|
+
"""Return a key from the JSON, refreshing if necessary
|
|
513
|
+
|
|
514
|
+
Args:
|
|
515
|
+
key (str): A valid JSON key.
|
|
516
|
+
"""
|
|
517
|
+
|
|
518
|
+
#This is not a static key, and it's time to refresh our data
|
|
519
|
+
if key not in static.StaticAPIEndpoints.main and time.time() - self.last_refresh_time > self.refresh_rate:
|
|
520
|
+
self.refresh()
|
|
521
|
+
|
|
522
|
+
return self._jsondata[key]
|
|
523
|
+
|
|
524
|
+
def check_refresh(self):
|
|
525
|
+
"""Refresh only if we are past the refresh rate"""
|
|
526
|
+
if time.time() - self.last_refresh_time > self.refresh_rate:
|
|
527
|
+
self.refresh()
|
|
528
|
+
|
|
529
|
+
def refresh(self):
|
|
530
|
+
"""Reload data from the API"""
|
|
531
|
+
self.last_refresh_time = time.time()
|
|
532
|
+
response = requests.get(self.api_url, headers = static.RequestHeaders.user_agent, timeout = static.Delays.request_timeout)
|
|
533
|
+
assert response.status_code == 200, "Status code " + str(response.status_code)
|
|
534
|
+
|
|
535
|
+
self._jsondata = response.json()
|
|
536
|
+
|
|
537
|
+
#Remove livestream references that are no longer listed
|
|
538
|
+
listed_ids = [jsondata["id"] for jsondata in self._jsondata["livestreams"]]
|
|
539
|
+
for stream_id in self.__livestreams.copy():
|
|
540
|
+
if stream_id not in listed_ids:
|
|
541
|
+
self.__livestreams[stream_id].is_disappeared = True
|
|
542
|
+
del self.__livestreams[stream_id]
|
|
543
|
+
|
|
544
|
+
#Update livestream references' JSONs in-place
|
|
545
|
+
for jsondata in self._jsondata["livestreams"]:
|
|
546
|
+
try:
|
|
547
|
+
#Update the JSON of the stored livestream
|
|
548
|
+
self.__livestreams[jsondata["id"]]._jsondata = jsondata
|
|
549
|
+
|
|
550
|
+
except KeyError: #The livestream has not been stored yet
|
|
551
|
+
self.__livestreams[jsondata["id"]] = Livestream(jsondata, self)
|
|
552
|
+
|
|
553
|
+
@property
|
|
554
|
+
def data_timestamp(self):
|
|
555
|
+
"""The timestamp on the last data refresh"""
|
|
556
|
+
#Definitely don't ever trigger a refresh on this
|
|
557
|
+
return self._jsondata["now"]
|
|
558
|
+
|
|
559
|
+
@property
|
|
560
|
+
def api_type(self):
|
|
561
|
+
"""Type of API URL in use, user or channel"""
|
|
562
|
+
return self["type"]
|
|
563
|
+
|
|
564
|
+
@property
|
|
565
|
+
def user_id(self):
|
|
566
|
+
"""The user ID in base 36"""
|
|
567
|
+
return self["user_id"]
|
|
568
|
+
|
|
569
|
+
@property
|
|
570
|
+
def user_id_b36(self):
|
|
571
|
+
"""The user ID in base 36"""
|
|
572
|
+
return self.user_id
|
|
573
|
+
|
|
574
|
+
@property
|
|
575
|
+
def user_id_b10(self):
|
|
576
|
+
"""The user ID in base 10"""
|
|
577
|
+
return utils.base_36_to_10(self.user_id)
|
|
578
|
+
|
|
579
|
+
@property
|
|
580
|
+
def username(self):
|
|
581
|
+
"""The username"""
|
|
582
|
+
return self["username"]
|
|
583
|
+
|
|
584
|
+
@property
|
|
585
|
+
def channel_id(self):
|
|
586
|
+
"""The channel ID, if we are a channel"""
|
|
587
|
+
return self["channel_id"]
|
|
588
|
+
|
|
589
|
+
@property
|
|
590
|
+
def channel_name(self):
|
|
591
|
+
"""The channel name, if we are a channel"""
|
|
592
|
+
return self["channel_name"]
|
|
593
|
+
|
|
594
|
+
@property
|
|
595
|
+
def num_followers(self):
|
|
596
|
+
"""The number of followers of this user or channel"""
|
|
597
|
+
return self["followers"]["num_followers"]
|
|
598
|
+
|
|
599
|
+
@property
|
|
600
|
+
def num_followers_total(self):
|
|
601
|
+
"""The total number of followers of this account across all channels"""
|
|
602
|
+
return self["followers"]["num_followers_total"]
|
|
603
|
+
|
|
604
|
+
@property
|
|
605
|
+
def latest_follower(self):
|
|
606
|
+
"""The latest follower of this user or channel"""
|
|
607
|
+
if not self["followers"]["latest_follower"]:
|
|
608
|
+
return None #No-one has followed this user or channel yet
|
|
609
|
+
return Follower(self["followers"]["latest_follower"])
|
|
610
|
+
|
|
611
|
+
@property
|
|
612
|
+
def recent_followers(self):
|
|
613
|
+
"""A list of recent followers"""
|
|
614
|
+
data = self["followers"]["recent_followers"].copy()
|
|
615
|
+
return [Follower(jsondata_block) for jsondata_block in data]
|
|
616
|
+
|
|
617
|
+
@property
|
|
618
|
+
def new_followers(self):
|
|
619
|
+
"""Followers that are newer than the last time this was checked (or newer than RumbleAPI object creation)"""
|
|
620
|
+
recent_followers = self.recent_followers
|
|
621
|
+
|
|
622
|
+
nf = [follower for follower in recent_followers if follower.followed_on > self.last_newfollower_time]
|
|
623
|
+
nf.sort(key = lambda x: x.followed_on)
|
|
624
|
+
|
|
625
|
+
self.last_newfollower_time = time.time()
|
|
626
|
+
|
|
627
|
+
return nf
|
|
628
|
+
|
|
629
|
+
@property
|
|
630
|
+
def num_subscribers(self):
|
|
631
|
+
"""The number of subscribers of this user or channel"""
|
|
632
|
+
return self["subscribers"]["num_subscribers"]
|
|
633
|
+
|
|
634
|
+
@property
|
|
635
|
+
def num_subscribers_total(self):
|
|
636
|
+
"""The total number of subscribers of this account across all channels"""
|
|
637
|
+
return self["subscribers"]["num_subscribers_total"]
|
|
638
|
+
|
|
639
|
+
@property
|
|
640
|
+
def latest_subscriber(self):
|
|
641
|
+
"""The latest subscriber of this user or channel"""
|
|
642
|
+
if not self["subscribers"]["latest_subscriber"]:
|
|
643
|
+
return None #No-one has subscribed to this user or channel yet
|
|
644
|
+
return Subscriber(self["subscribers"]["latest_subscriber"])
|
|
645
|
+
|
|
646
|
+
@property
|
|
647
|
+
def recent_subscribers(self):
|
|
648
|
+
"""A list of recent subscribers (shallow)"""
|
|
649
|
+
data = self["subscribers"]["recent_subscribers"].copy()
|
|
650
|
+
return [Subscriber(jsondata_block) for jsondata_block in data]
|
|
651
|
+
|
|
652
|
+
@property
|
|
653
|
+
def new_subscribers(self):
|
|
654
|
+
"""Subscribers that are newer than the last time this was checked (or newer than RumbleAPI object creation)"""
|
|
655
|
+
recent_subscribers = self.recent_subscribers
|
|
656
|
+
|
|
657
|
+
ns = [subscriber for subscriber in recent_subscribers if subscriber.subscribed_on > self.last_newsubscriber_time]
|
|
658
|
+
ns.sort(key = lambda x: x.subscribed_on)
|
|
659
|
+
|
|
660
|
+
self.last_newsubscriber_time = time.time()
|
|
661
|
+
|
|
662
|
+
return ns
|
|
663
|
+
|
|
664
|
+
@property
|
|
665
|
+
def livestreams(self):
|
|
666
|
+
"""A dictionairy of our livestreams"""
|
|
667
|
+
self.check_refresh()
|
|
668
|
+
return self.__livestreams
|
|
669
|
+
|
|
670
|
+
@property
|
|
671
|
+
def latest_livestream(self):
|
|
672
|
+
"""Return latest livestream to be created. Use this to get a single running livestream"""
|
|
673
|
+
if not self.livestreams:
|
|
674
|
+
return None #No livestreams are running
|
|
675
|
+
return max(self.livestreams.values(), key = lambda x: x.created_on)
|