cocorum 0.3.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 +543 -0
- cocorum/localvars.py +51 -0
- cocorum/ssechat.py +383 -0
- cocorum/utils.py +26 -0
- cocorum-0.3.0.dist-info/METADATA +76 -0
- cocorum-0.3.0.dist-info/RECORD +8 -0
- cocorum-0.3.0.dist-info/WHEEL +4 -0
- cocorum-0.3.0.dist-info/licenses/LICENSE.txt +674 -0
cocorum/__init__.py
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Cocorum: a Python wrapper for the Rumble.com API
|
|
3
|
+
|
|
4
|
+
S.D.G."""
|
|
5
|
+
|
|
6
|
+
import time
|
|
7
|
+
import calendar
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from .localvars import *
|
|
11
|
+
from .utils import *
|
|
12
|
+
|
|
13
|
+
class RumbleAPISubObj():
|
|
14
|
+
"""Abstract class for a Rumble API object"""
|
|
15
|
+
def __init__(self, json):
|
|
16
|
+
"""Pass the JSON block for a single Rumble API subobject"""
|
|
17
|
+
self._json = json
|
|
18
|
+
|
|
19
|
+
def __getitem__(self, key):
|
|
20
|
+
"""Get a key from the JSON"""
|
|
21
|
+
return self._json[key]
|
|
22
|
+
|
|
23
|
+
class RumbleUserAction(RumbleAPISubObj):
|
|
24
|
+
"""Abstract class for Rumble user actions"""
|
|
25
|
+
def __init__(self, json):
|
|
26
|
+
"""Pass the JSON block for a single Rumble user action"""
|
|
27
|
+
super().__init__(json)
|
|
28
|
+
self.__profile_pic = None
|
|
29
|
+
|
|
30
|
+
def __eq__(self, other):
|
|
31
|
+
"""Is this follower equal to another"""
|
|
32
|
+
#Check if the compared string is our username
|
|
33
|
+
if isinstance(other, str):
|
|
34
|
+
return self.username == other
|
|
35
|
+
|
|
36
|
+
#Check if the compared object has a username and if it matches our own
|
|
37
|
+
if hasattr(other, "username"):
|
|
38
|
+
return self.username == other.username
|
|
39
|
+
|
|
40
|
+
def __str__(self):
|
|
41
|
+
"""Follower as a string"""
|
|
42
|
+
return self.username
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def username(self):
|
|
46
|
+
"""The follower's username"""
|
|
47
|
+
return self["username"]
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def profile_pic_url(self):
|
|
51
|
+
"""The user's profile picture URL"""
|
|
52
|
+
return self["profile_pic_url"]
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def profile_pic(self):
|
|
56
|
+
"""The user's profile picture as a bytes string"""
|
|
57
|
+
if not self.profile_pic_url: #The profile picture is blank
|
|
58
|
+
return b''
|
|
59
|
+
|
|
60
|
+
if not self.__profile_pic: #We never queried the profile pic before
|
|
61
|
+
response = requests.get(self.profile_pic_url)
|
|
62
|
+
if response.status_code != 200:
|
|
63
|
+
#TODO make this timeout assignable
|
|
64
|
+
raise Exception("Status code " + str(response.status_code), DEFAULT_TIMEOUT)
|
|
65
|
+
|
|
66
|
+
self.__profile_pic = response.content
|
|
67
|
+
|
|
68
|
+
return self.__profile_pic
|
|
69
|
+
|
|
70
|
+
class RumbleFollower(RumbleUserAction):
|
|
71
|
+
"""Rumble follower"""
|
|
72
|
+
@property
|
|
73
|
+
def followed_on(self):
|
|
74
|
+
"""When the follower followed, in seconds since Epoch UTC"""
|
|
75
|
+
return parse_timestamp(self["followed_on"])
|
|
76
|
+
|
|
77
|
+
class RumbleSubscriber(RumbleUserAction):
|
|
78
|
+
"""Rumble subscriber"""
|
|
79
|
+
def __eq__(self, other):
|
|
80
|
+
"""Is this subscriber equal to another"""
|
|
81
|
+
#Check if the compared string is our username
|
|
82
|
+
if isinstance(other, str):
|
|
83
|
+
return self.username == other
|
|
84
|
+
|
|
85
|
+
#check if the compared number is our amount in cents
|
|
86
|
+
if isinstance(other, (int, float)):
|
|
87
|
+
return self.amount_cents == other
|
|
88
|
+
|
|
89
|
+
#Check if the compared object's username matches our own, if it has one
|
|
90
|
+
if hasattr(other, "username"):
|
|
91
|
+
#Check if the compared object's cost amout matches our own, if it has one
|
|
92
|
+
if hasattr(other, "amount_cents"):
|
|
93
|
+
self.amount_cents == other.amount_cents
|
|
94
|
+
|
|
95
|
+
#Other object has no amount_cents attribute
|
|
96
|
+
return self.username == other.username
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def user(self):
|
|
100
|
+
"""AFAIK this is being deprecated, use username instead"""
|
|
101
|
+
return self["user"]
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def amount_cents(self):
|
|
105
|
+
"""The total subscription amount in cents"""
|
|
106
|
+
return self["amount_cents"]
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def amount_dollars(self):
|
|
110
|
+
"""The subscription amount in dollars"""
|
|
111
|
+
return self["amount_dollars"]
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def subscribed_on(self):
|
|
115
|
+
"""When the subscriber subscribed, in seconds since Epoch UTC"""
|
|
116
|
+
return parse_timestamp(self["subscribed_on"])
|
|
117
|
+
|
|
118
|
+
class RumbleStreamCategory(RumbleAPISubObj):
|
|
119
|
+
"""Category of a Rumble stream"""
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def slug(self):
|
|
123
|
+
"""Return the category's slug, AKA it's ID"""
|
|
124
|
+
return self["slug"]
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def title(self):
|
|
128
|
+
"""Return the category's title"""
|
|
129
|
+
return self["title"]
|
|
130
|
+
|
|
131
|
+
def __eq__(self, other):
|
|
132
|
+
"""Is this category equal to another"""
|
|
133
|
+
#Check if the compared string is our slug or title
|
|
134
|
+
if isinstance(other, str):
|
|
135
|
+
return other in (self.slug, self.title)
|
|
136
|
+
|
|
137
|
+
#Check if the compared object has the same slug, if it has one
|
|
138
|
+
if hasattr(other, "slug"):
|
|
139
|
+
return self.slug == other.slug
|
|
140
|
+
|
|
141
|
+
def __str__(self):
|
|
142
|
+
"""The category in string form"""
|
|
143
|
+
return self.title
|
|
144
|
+
|
|
145
|
+
class RumbleLivestream():
|
|
146
|
+
"""Rumble livestream"""
|
|
147
|
+
def __init__(self, json, api):
|
|
148
|
+
"""Pass the JSON block of a single Rumble livestream"""
|
|
149
|
+
self._json = json
|
|
150
|
+
self.api = api
|
|
151
|
+
self.is_disappeared = False #The livestream is in the API listing
|
|
152
|
+
self.__chat = RumbleLiveChat(self)
|
|
153
|
+
|
|
154
|
+
def __eq__(self, other):
|
|
155
|
+
"""Is this stream equal to another"""
|
|
156
|
+
#Check if the compared string is our stream ID
|
|
157
|
+
if isinstance(other, str):
|
|
158
|
+
return self.stream_id == other #or self.title == other
|
|
159
|
+
|
|
160
|
+
#check if the compared number is our chat ID (linked to stream ID)
|
|
161
|
+
if isinstance(other, (int, float)):
|
|
162
|
+
return self.chat_id == other
|
|
163
|
+
|
|
164
|
+
#Check if the compared object has the same stream ID
|
|
165
|
+
if hasattr(other, "stream_id"):
|
|
166
|
+
return self.stream_id == other.stream_id
|
|
167
|
+
|
|
168
|
+
#Check if the compared object has the same chat ID
|
|
169
|
+
if hasattr(other, "chat_id"):
|
|
170
|
+
return self.stream_id == other.chat_id
|
|
171
|
+
|
|
172
|
+
def __str__(self):
|
|
173
|
+
"""The livestream in string form"""
|
|
174
|
+
return self.stream_id
|
|
175
|
+
|
|
176
|
+
def __getitem__(self, key):
|
|
177
|
+
"""Return a key from the JSON, refreshing if necessary"""
|
|
178
|
+
#The livestream has not disappeared from the API listing,
|
|
179
|
+
#the key requested is not a value that doesn't change,
|
|
180
|
+
#and it has been api.refresh rate since the last time we refreshed
|
|
181
|
+
if (not self.is_disappeared) and (key not in STATIC_KEYS_STREAM) and (time.time() - self.api.last_refresh_time > self.api.refresh_rate):
|
|
182
|
+
self.api.refresh()
|
|
183
|
+
|
|
184
|
+
return self._json[key]
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def stream_id(self):
|
|
188
|
+
"""The livestream ID"""
|
|
189
|
+
return self["id"]
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def chat_id(self):
|
|
193
|
+
"""The livestream chat ID"""
|
|
194
|
+
return id_stream_to_chat(self.stream_id)
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def title(self):
|
|
198
|
+
"""The title of the livestream"""
|
|
199
|
+
return self["title"]
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def created_on(self):
|
|
203
|
+
"""When the livestream was created, in seconds since the Epock UTC"""
|
|
204
|
+
return parse_timestamp(self["created_on"])
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def is_live(self):
|
|
208
|
+
"""Is the stream live?"""
|
|
209
|
+
return self["is_live"] and not self.is_disappeared
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def visibility(self):
|
|
213
|
+
"""TODO"""
|
|
214
|
+
return self["visibility"]
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def categories(self):
|
|
218
|
+
"""A list of our categories"""
|
|
219
|
+
data = self["categories"].copy().values()
|
|
220
|
+
return [RumbleStreamCategory(json_block) for json_block in data]
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def likes(self):
|
|
224
|
+
"""Number of likes on the stream"""
|
|
225
|
+
return self["likes"]
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def dislikes(self):
|
|
229
|
+
"""Number of dislikes on the stream"""
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def like_ratio(self):
|
|
233
|
+
"""Ratio of people who liked the stream to people who reacted total"""
|
|
234
|
+
try:
|
|
235
|
+
return self.likes / (self.likes + self.dislikes)
|
|
236
|
+
|
|
237
|
+
except ZeroDivisionError:
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def watching_now(self):
|
|
242
|
+
"""The number of people watching now"""
|
|
243
|
+
return self["watching_now"]
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def chat(self):
|
|
247
|
+
"""The livestream chat"""
|
|
248
|
+
return self.__chat
|
|
249
|
+
|
|
250
|
+
class RumbleChatMessage(RumbleUserAction):
|
|
251
|
+
"""A single message in a Rumble livestream chat"""
|
|
252
|
+
def __eq__(self, other):
|
|
253
|
+
"""Is this message equal to another"""
|
|
254
|
+
#Check if the compared string is our message
|
|
255
|
+
if isinstance(other, str):
|
|
256
|
+
return self.text == other
|
|
257
|
+
|
|
258
|
+
# #Check if the compared message has the same username and text (not needed)
|
|
259
|
+
# if type(other) == type(self):
|
|
260
|
+
# return (self.username, self.text) == (other.username, other.text)
|
|
261
|
+
|
|
262
|
+
#Check if the compared object has the same text
|
|
263
|
+
if hasattr(other, "text"):
|
|
264
|
+
#Check if the compared object has the same username, if it has one
|
|
265
|
+
if hasattr(other, "username"):
|
|
266
|
+
return (self.username, self.text) == (other.username, other.text)
|
|
267
|
+
|
|
268
|
+
return self.text == other.text #the other object had no username attribute
|
|
269
|
+
|
|
270
|
+
def __str__(self):
|
|
271
|
+
"""Follower as a string"""
|
|
272
|
+
return self.text
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def text(self):
|
|
276
|
+
"""The message text"""
|
|
277
|
+
return self["text"]
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def created_on(self):
|
|
281
|
+
"""When the message was created, in seconds since Epoch UTC"""
|
|
282
|
+
return parse_timestamp(self["created_on"])
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def badges(self):
|
|
286
|
+
"""The user's badges"""
|
|
287
|
+
return tuple(self["badges"].values())
|
|
288
|
+
|
|
289
|
+
class RumbleRant(RumbleChatMessage):
|
|
290
|
+
"""A single rant in a Rumble livestream chat"""
|
|
291
|
+
def __eq__(self, other):
|
|
292
|
+
"""Is this category equal to another"""
|
|
293
|
+
#Check if the compared string is our message
|
|
294
|
+
if isinstance(other, str):
|
|
295
|
+
return self.text == other
|
|
296
|
+
|
|
297
|
+
# #Check if the compared rant has the same username, amount, and text (unneccesary?)
|
|
298
|
+
# if type(other) == type(self):
|
|
299
|
+
# return (self.username, self.amount_cents, self.text) == (other.username, other.amount_cents, other.text)
|
|
300
|
+
|
|
301
|
+
#Check if the compared object has the same text
|
|
302
|
+
if hasattr(other, "text"):
|
|
303
|
+
#Check if the compared object has the same username, if it has one
|
|
304
|
+
if hasattr(other, "username"):
|
|
305
|
+
#Check if the compared object has the same cost amount, if it has one
|
|
306
|
+
if hasattr(other, "amount_cents"):
|
|
307
|
+
return (self.username, self.amount_cents, self.text) == (other.username, other.amount_cents, other.text)
|
|
308
|
+
|
|
309
|
+
#Other object has no amount_cents attribute
|
|
310
|
+
return (self.username, self.text) == (other.username, other.text)
|
|
311
|
+
|
|
312
|
+
#Other object had no username attribute
|
|
313
|
+
return self.text == other.text
|
|
314
|
+
|
|
315
|
+
@property
|
|
316
|
+
def expires_on(self):
|
|
317
|
+
"""When the rant will expire, in seconds since the Epoch UTC"""
|
|
318
|
+
return self["expires_on"]
|
|
319
|
+
|
|
320
|
+
@property
|
|
321
|
+
def amount_cents(self):
|
|
322
|
+
"""The total rant amount in cents"""
|
|
323
|
+
return self["amount_cents"]
|
|
324
|
+
|
|
325
|
+
@property
|
|
326
|
+
def amount_dollars(self):
|
|
327
|
+
"""The rant amount in dollars"""
|
|
328
|
+
return self["amount_dollars"]
|
|
329
|
+
|
|
330
|
+
class RumbleLiveChat():
|
|
331
|
+
"""Reference for chat of a Rumble livestream"""
|
|
332
|
+
def __init__(self, stream):
|
|
333
|
+
"""Pass the JSON block of a single Rumble livestream"""
|
|
334
|
+
self.stream = stream
|
|
335
|
+
self.api = stream.api
|
|
336
|
+
self.last_newmessage_time = 0 #Last time we were checked for "new" messages
|
|
337
|
+
self.last_newrant_time = 0 #Last time we were checked for "new" rants
|
|
338
|
+
|
|
339
|
+
def __getitem__(self, key):
|
|
340
|
+
"""Return a key from the stream's chat JSON"""
|
|
341
|
+
return self.stream._json["chat"][key]
|
|
342
|
+
|
|
343
|
+
@property
|
|
344
|
+
def latest_message(self):
|
|
345
|
+
"""The latest chat message"""
|
|
346
|
+
if not self["latest_message"]:
|
|
347
|
+
return None #No-one has chatted on this stream yet
|
|
348
|
+
return RumbleChatMessage(self["latest_message"])
|
|
349
|
+
|
|
350
|
+
@property
|
|
351
|
+
def recent_messages(self):
|
|
352
|
+
"""Recent chat messages"""
|
|
353
|
+
data = self["recent_messages"].copy()
|
|
354
|
+
return [RumbleChatMessage(json_block) for json_block in data]
|
|
355
|
+
|
|
356
|
+
@property
|
|
357
|
+
def new_messages(self):
|
|
358
|
+
"""Chat messages that are newer than the last time this was referenced"""
|
|
359
|
+
rem = self.recent_messages.copy()
|
|
360
|
+
rem.sort(key = lambda x: x.created_on) #Sort the messages so the newest ones are last
|
|
361
|
+
|
|
362
|
+
if rem[-1].created_on < self.last_newmessage_time: #All messages are older than the last time we checked
|
|
363
|
+
return []
|
|
364
|
+
|
|
365
|
+
i = 0
|
|
366
|
+
for i, m in enumerate(rem):
|
|
367
|
+
if m.created_on > self.last_newmessage_time:
|
|
368
|
+
break #i is now the index of the oldest new message
|
|
369
|
+
|
|
370
|
+
self.last_newmessage_time = time.time()
|
|
371
|
+
return rem[i:]
|
|
372
|
+
|
|
373
|
+
@property
|
|
374
|
+
def latest_rant(self):
|
|
375
|
+
"""The latest chat rant"""
|
|
376
|
+
if not self["latest_rant"]:
|
|
377
|
+
return None #No-one has ranted on this stream yet
|
|
378
|
+
return RumbleRant(self["latest_rant"])
|
|
379
|
+
|
|
380
|
+
@property
|
|
381
|
+
def recent_rants(self):
|
|
382
|
+
"""Recent chat rants"""
|
|
383
|
+
data = self["recent_rants"].copy()
|
|
384
|
+
return [RumbleRant(json_block) for json_block in data]
|
|
385
|
+
|
|
386
|
+
@property
|
|
387
|
+
def new_rants(self):
|
|
388
|
+
"""Chat rants that are newer than the last time this was referenced"""
|
|
389
|
+
rera = self.recent_rants.copy()
|
|
390
|
+
rera.sort(key = lambda x: x.created_on) #Sort the rants so the newest ones are last
|
|
391
|
+
|
|
392
|
+
if rera[-1].created_on < self.last_newrant_time: #All rants are older than the last time we checked
|
|
393
|
+
return []
|
|
394
|
+
|
|
395
|
+
i = 0
|
|
396
|
+
for i, r in enumerate(rera):
|
|
397
|
+
if r.created_on > self.last_newrant_time:
|
|
398
|
+
break #i is now the index of the oldest new rant
|
|
399
|
+
|
|
400
|
+
self.last_newrant_time = time.time()
|
|
401
|
+
return rera[i:]
|
|
402
|
+
|
|
403
|
+
class RumbleAPI():
|
|
404
|
+
"""Rumble API wrapper"""
|
|
405
|
+
def __init__(self, api_url, refresh_rate = DEFAULT_REFRESH_RATE, request_timeout = DEFAULT_TIMEOUT):
|
|
406
|
+
"""Pass the Rumble API URL, and how long to wait before refreshing on new queries"""
|
|
407
|
+
self.refresh_rate = refresh_rate
|
|
408
|
+
self.request_timeout = request_timeout
|
|
409
|
+
self.last_refresh_time = 0
|
|
410
|
+
self.__livestreams = {}
|
|
411
|
+
self._json = {}
|
|
412
|
+
self.api_url = api_url
|
|
413
|
+
|
|
414
|
+
@property
|
|
415
|
+
def api_url(self):
|
|
416
|
+
"""Our API URL"""
|
|
417
|
+
return self.__api_url
|
|
418
|
+
|
|
419
|
+
@api_url.setter
|
|
420
|
+
def api_url(self, url):
|
|
421
|
+
"""Set our API URL"""
|
|
422
|
+
self.__api_url = url
|
|
423
|
+
self.refresh()
|
|
424
|
+
|
|
425
|
+
def __getitem__(self, key):
|
|
426
|
+
"""Return a key from the JSON, refreshing if necessary"""
|
|
427
|
+
if key not in STATIC_KEYS and time.time() - self.last_refresh_time > self.refresh_rate:
|
|
428
|
+
self.refresh()
|
|
429
|
+
return self._json[key]
|
|
430
|
+
|
|
431
|
+
def check_refresh(self):
|
|
432
|
+
"""Refresh only if we are past the refresh rate"""
|
|
433
|
+
if time.time() - self.last_refresh_time > self.refresh_rate:
|
|
434
|
+
self.refresh()
|
|
435
|
+
|
|
436
|
+
def refresh(self):
|
|
437
|
+
"""Reload data from the API"""
|
|
438
|
+
self.last_refresh_time = time.time()
|
|
439
|
+
response = requests.get(self.api_url, headers = HEADERS, timeout = self.request_timeout)
|
|
440
|
+
if response.status_code != 200:
|
|
441
|
+
raise Exception("Status code " + str(response.status_code))
|
|
442
|
+
|
|
443
|
+
self._json = response.json()
|
|
444
|
+
|
|
445
|
+
#Remove livestream references that are no longer listed
|
|
446
|
+
listed_ids = [json["id"] for json in self._json["livestreams"]]
|
|
447
|
+
for stream_id in self.__livestreams.copy():
|
|
448
|
+
if stream_id not in listed_ids:
|
|
449
|
+
self.__livestreams[stream_id].is_disappeared = True
|
|
450
|
+
del self.__livestreams[stream_id]
|
|
451
|
+
|
|
452
|
+
#Update livestream references' JSONs in-place
|
|
453
|
+
for json in self._json["livestreams"]:
|
|
454
|
+
try:
|
|
455
|
+
#Update the JSON of the stored livestream
|
|
456
|
+
self.__livestreams[json["id"]]._json = json
|
|
457
|
+
|
|
458
|
+
except KeyError: #The livestream has not been stored yet
|
|
459
|
+
self.__livestreams[json["id"]] = RumbleLivestream(json, self)
|
|
460
|
+
|
|
461
|
+
@property
|
|
462
|
+
def api_type(self):
|
|
463
|
+
"""Type of API URL in use, user or channel"""
|
|
464
|
+
return self["type"]
|
|
465
|
+
|
|
466
|
+
@property
|
|
467
|
+
def user_id(self):
|
|
468
|
+
"""The user ID"""
|
|
469
|
+
return self["user_id"]
|
|
470
|
+
|
|
471
|
+
@property
|
|
472
|
+
def username(self):
|
|
473
|
+
"""The username"""
|
|
474
|
+
return self["username"]
|
|
475
|
+
|
|
476
|
+
@property
|
|
477
|
+
def channel_id(self):
|
|
478
|
+
"""The channel ID, if we are a channel"""
|
|
479
|
+
return self["channel_id"]
|
|
480
|
+
|
|
481
|
+
@property
|
|
482
|
+
def channel_name(self):
|
|
483
|
+
"""The channel name, if we are a channel"""
|
|
484
|
+
return self["channel_name"]
|
|
485
|
+
|
|
486
|
+
@property
|
|
487
|
+
def num_followers(self):
|
|
488
|
+
"""The number of followers of this user or channel"""
|
|
489
|
+
return self["followers"]["num_followers"]
|
|
490
|
+
|
|
491
|
+
@property
|
|
492
|
+
def num_followers_total(self):
|
|
493
|
+
"""The total number of followers of this account across all channels"""
|
|
494
|
+
return self["followers"]["num_followers_total"]
|
|
495
|
+
|
|
496
|
+
@property
|
|
497
|
+
def latest_follower(self):
|
|
498
|
+
"""The latest follower of this user or channel"""
|
|
499
|
+
if not self["followers"]["latest_follower"]:
|
|
500
|
+
return None #No-one has followed this user or channel yet
|
|
501
|
+
return RumbleFollower(self["followers"]["latest_follower"])
|
|
502
|
+
|
|
503
|
+
@property
|
|
504
|
+
def recent_followers(self):
|
|
505
|
+
"""A list (technically a shallow generator object) of recent followers"""
|
|
506
|
+
data = self["followers"]["recent_followers"].copy()
|
|
507
|
+
return [RumbleFollower(json_block) for json_block in data]
|
|
508
|
+
|
|
509
|
+
@property
|
|
510
|
+
def num_subscribers(self):
|
|
511
|
+
"""The number of subscribers of this user or channel"""
|
|
512
|
+
return self["subscribers"]["num_subscribers"]
|
|
513
|
+
|
|
514
|
+
@property
|
|
515
|
+
def num_subscribers_total(self):
|
|
516
|
+
"""The total number of subscribers of this account across all channels"""
|
|
517
|
+
return self["subscribers"]["num_subscribers_total"]
|
|
518
|
+
|
|
519
|
+
@property
|
|
520
|
+
def latest_subscriber(self):
|
|
521
|
+
"""The latest subscriber of this user or channel"""
|
|
522
|
+
if not self["subscribers"]["latest_subscriber"]:
|
|
523
|
+
return None #No-one has subscribed to this user or channel yet
|
|
524
|
+
return RumbleSubscriber(self["subscribers"]["latest_subscriber"])
|
|
525
|
+
|
|
526
|
+
@property
|
|
527
|
+
def recent_subscribers(self):
|
|
528
|
+
"""A list (technically a shallow generator object) of recent subscribers"""
|
|
529
|
+
data = self["subscribers"]["recent_subscribers"].copy()
|
|
530
|
+
return [RumbleSubscriber(json_block) for json_block in data]
|
|
531
|
+
|
|
532
|
+
@property
|
|
533
|
+
def livestreams(self):
|
|
534
|
+
"""A dictionairy of our livestreams"""
|
|
535
|
+
self.check_refresh()
|
|
536
|
+
return self.__livestreams
|
|
537
|
+
|
|
538
|
+
@property
|
|
539
|
+
def latest_livestream(self):
|
|
540
|
+
"""Return latest livestream to be created. Use this to get a single running livestream"""
|
|
541
|
+
if not self.livestreams:
|
|
542
|
+
return None #No livestreams are running
|
|
543
|
+
return max(self.livestreams.values(), key = lambda x: x.created_on)
|
cocorum/localvars.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Cocorum local variable definitions
|
|
3
|
+
|
|
4
|
+
S.D.G."""
|
|
5
|
+
|
|
6
|
+
#Rumble timestamp format, not including the 6 TODO characters at the end
|
|
7
|
+
TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S"
|
|
8
|
+
|
|
9
|
+
#Headers for the Rumble Live Stream API request (currently must fake a User-Agent string)
|
|
10
|
+
HEADERS = {"User-Agent" : "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36"}
|
|
11
|
+
|
|
12
|
+
#Keys of the API JSON that should not change unless the API URL changes, and so do not trigger a refresh
|
|
13
|
+
STATIC_KEYS = [
|
|
14
|
+
"user_id",
|
|
15
|
+
"username",
|
|
16
|
+
"channel_id",
|
|
17
|
+
"channel_name",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
#Keys of the API JSON stream object that should not change unless the API URL changes, and so do not trigger a refresh
|
|
21
|
+
STATIC_KEYS_STREAM = [
|
|
22
|
+
"id",
|
|
23
|
+
"created_on"
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
#API types, under JSON["type"]
|
|
27
|
+
API_TYPE_USER = "user"
|
|
28
|
+
API_TYPE_CHANNEL = "channel"
|
|
29
|
+
|
|
30
|
+
#Stream visibility possibilities, under JSON["livestreams"][0]["visibility"]
|
|
31
|
+
STREAM_VIS_PUBLIC = "public"
|
|
32
|
+
STREAM_VIS_UNLISTED = "unlisted"
|
|
33
|
+
STREAM_VIS_PRIVATE = "private"
|
|
34
|
+
|
|
35
|
+
#Base URL to Rumble's website, for URLs that are relative to it
|
|
36
|
+
RUMBLE_BASE_URL = "https://rumble.com"
|
|
37
|
+
|
|
38
|
+
#Numerical base that the stream ID is in
|
|
39
|
+
STREAM_ID_BASE = "0123456789abcdefghijklmnopqrstuvwxyz"
|
|
40
|
+
|
|
41
|
+
#Rumble's SSE chat display URL for a stream, format this string with a chat_id
|
|
42
|
+
SSE_CHAT_URL = "https://web7.rumble.com/chat/api/chat/{chat_id}/stream"
|
|
43
|
+
|
|
44
|
+
#Size of chat badge icons to retrieve, only valid one has long been the string 48
|
|
45
|
+
BADGE_ICON_SIZE = "48"
|
|
46
|
+
|
|
47
|
+
#How long to wait before giving up on a network request, in seconds
|
|
48
|
+
DEFAULT_TIMEOUT = 20
|
|
49
|
+
|
|
50
|
+
#How long to reuse old data from the API, in seconds
|
|
51
|
+
DEFAULT_REFRESH_RATE = 10
|