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 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)