Skip to content

cocorum.scraping

The primary use from this module is the Scraper class, used for getting data from Rumble web pages, or in the rare case where HTML is passed by one of the APIs' endpoints. You must first create an instance of cocorum.servicephp.ServicePHP(), and then pass it to this class upon initialization. All other classes are supporting sub-classes.

Scraping for Cocorum

Classes and utilities for extracting data from HTML, including that returned by the API.

Copyright 2026 Wilbur Jaywright d.b.a. Marswide BGL.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

S.D.G.

BaseComment

A comment on a Rumble video

Source code in cocorum/basehandles.py
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
class BaseComment:
    """A comment on a Rumble video"""

    def __int__(self) -> int:
        """The comment in integer form (its ID)"""
        return self.comment_id

    def __str__(self) -> str:
        """The comment as a string (its text)"""
        return self.text

    @property
    def comment_id_b10(self) -> int:
        """The base 10 ID of the comment"""
        assert isinstance(
            self.comment_id, int), "Contact the Cocorum developers, this is probably their mistake"
        return self.comment_id

    @property
    def comment_id_b36(self) -> str:
        """The base 36 ID of the comment"""
        return utils.base_10_to_36(self.comment_id)

    def __eq__(self, other: Any) -> bool:
        """Determine if this comment is equal to another.

        Args:
            other (Any): Object to compare to.

        Returns:
            Comparison (bool): Did it fit the criteria?
        """

        # Check for direct matches first
        if isinstance(other, int):
            return self.comment_id_b10 == other
        if isinstance(other, str):
            return str(self) == other

        # Check for object attributes to match to
        if hasattr(other, "comment_id_b10"):
            return self.comment_id_b10 == other.comment_id_b10

        # Check conversion to integer last
        if hasattr(other, "__int__"):
            return self.comment_id_b10 == int(other)

        return False

    def pin(self, unpin: bool = False):
        """Pin or unpin this comment.

    Args:
        unpin (bool): If true, unpins instead of pinning comment.
        """

        return self.servicephp.comment_pin(self, unpin)

    def delete(self):
        """Delete this comment"""

        return self.servicephp.comment_delete(self)

    def restore(self) -> APIComment:
        """Un-delete this comment"""

        return self.servicephp.comment_restore(self)

comment_id_b10 property

The base 10 ID of the comment

comment_id_b36 property

The base 36 ID of the comment

__eq__(other)

Determine if this comment is equal to another.

Parameters:

Name Type Description Default
other Any

Object to compare to.

required

Returns:

Name Type Description
Comparison bool

Did it fit the criteria?

Source code in cocorum/basehandles.py
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
def __eq__(self, other: Any) -> bool:
    """Determine if this comment is equal to another.

    Args:
        other (Any): Object to compare to.

    Returns:
        Comparison (bool): Did it fit the criteria?
    """

    # Check for direct matches first
    if isinstance(other, int):
        return self.comment_id_b10 == other
    if isinstance(other, str):
        return str(self) == other

    # Check for object attributes to match to
    if hasattr(other, "comment_id_b10"):
        return self.comment_id_b10 == other.comment_id_b10

    # Check conversion to integer last
    if hasattr(other, "__int__"):
        return self.comment_id_b10 == int(other)

    return False

__int__()

The comment in integer form (its ID)

Source code in cocorum/basehandles.py
81
82
83
def __int__(self) -> int:
    """The comment in integer form (its ID)"""
    return self.comment_id

__str__()

The comment as a string (its text)

Source code in cocorum/basehandles.py
85
86
87
def __str__(self) -> str:
    """The comment as a string (its text)"""
    return self.text

delete()

Delete this comment

Source code in cocorum/basehandles.py
136
137
138
139
def delete(self):
    """Delete this comment"""

    return self.servicephp.comment_delete(self)

pin(unpin=False)

Pin or unpin this comment.

Parameters:

Name Type Description Default
unpin bool

If true, unpins instead of pinning comment.

False
Source code in cocorum/basehandles.py
127
128
129
130
131
132
133
134
def pin(self, unpin: bool = False):
    """Pin or unpin this comment.

Args:
    unpin (bool): If true, unpins instead of pinning comment.
    """

    return self.servicephp.comment_pin(self, unpin)

restore()

Un-delete this comment

Source code in cocorum/basehandles.py
141
142
143
144
def restore(self) -> APIComment:
    """Un-delete this comment"""

    return self.servicephp.comment_restore(self)

BaseContentVotes

Likes and dislikes on a video or comment

Source code in cocorum/basehandles.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
class BaseContentVotes:
    """Likes and dislikes on a video or comment"""

    def __repr__(self) -> str:
        """String to represent this object"""
        return f"{type(self).__name__}(content_id={self.content_id}, score={self.score})"

    def __int__(self) -> int:
        """The integer form of the content votes"""
        return self.score

    def __eq__(self, other: Any) -> bool:
        """Determine if this content votes is equal to another.

        Args:
            other (Any): Object to compare to.

        Returns:
            Comparison (bool): Did it fit the criteria?
        """

        # Check for direct matches first
        if isinstance(other, int):
            return self.score == other
        if isinstance(other, str):
            return str(self) == other

        # Check for object attributes to match to
        if hasattr(other, "score"):
            return self.score == other.score

        # Check conversion to integer last
        if hasattr(other, "__int__"):
            return self.score == int(other)

        return False

__eq__(other)

Determine if this content votes is equal to another.

Parameters:

Name Type Description Default
other Any

Object to compare to.

required

Returns:

Name Type Description
Comparison bool

Did it fit the criteria?

Source code in cocorum/basehandles.py
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
def __eq__(self, other: Any) -> bool:
    """Determine if this content votes is equal to another.

    Args:
        other (Any): Object to compare to.

    Returns:
        Comparison (bool): Did it fit the criteria?
    """

    # Check for direct matches first
    if isinstance(other, int):
        return self.score == other
    if isinstance(other, str):
        return str(self) == other

    # Check for object attributes to match to
    if hasattr(other, "score"):
        return self.score == other.score

    # Check conversion to integer last
    if hasattr(other, "__int__"):
        return self.score == int(other)

    return False

__int__()

The integer form of the content votes

Source code in cocorum/basehandles.py
154
155
156
def __int__(self) -> int:
    """The integer form of the content votes"""
    return self.score

__repr__()

String to represent this object

Source code in cocorum/basehandles.py
150
151
152
def __repr__(self) -> str:
    """String to represent this object"""
    return f"{type(self).__name__}(content_id={self.content_id}, score={self.score})"

BasePlaylist

A playlist of Rumble videos

Source code in cocorum/basehandles.py
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
class BasePlaylist:
    """A playlist of Rumble videos"""

    def __int__(self) -> int:
        """The playlist as an integer (it's ID in base 10)"""
        return self.playlist_id_b10

    def __str__(self) -> str:
        """The playlist as a string (it's ID in base 64)"""
        return self.playlist_id_b64

    def __repr__(self) -> str:
        """String to represent this object"""
        return f"{type(self).__name__}(playlist_id={self.playlist_id}, title=\"{self.title}\")"

    def __eq__(self, other: Any) -> bool:
        """Determine if this playlist is equal to another.

        Args:
            other (Any): Object to compare to.

        Returns:
            Comparison (bool): Did it fit the criteria?
        """

        # Check for direct matches first
        if isinstance(other, int):
            return self.playlist_id_b64 == other
        if isinstance(other, str):
            return str(other) == self.playlist_id_b64

        # # Check for object attributes to match to
        # if hasattr(other, "playlist_id_b10"):
        #     return self.playlist_id_b10 == other.playlist_id_b10

        # # Check conversion to integer last, in case another ID or something happens to match
        # if hasattr(other, "__int__"):
        #     return self.playlist_id_b10 == int(other)

        return False

    @property
    def playlist_id_b64(self) -> str:
        """The numeric ID of the playlist in base 64"""
        return self.playlist_id

    @property
    def playlist_id_b10(self) -> int:
        """The numeric ID of the playlist in base 10"""
        raise NotImplementedError("See Cocorum issue #22")
        # return utils.base_64_to_10(self.playlist_id)

    def add_video(self, video_id: SupportsInt | str):
        """Add a video to this playlist

    Args:
        video_id (SupportsInt | str): The numeric ID of the video to add, in base 10 or 36.
        """

        return self.servicephp.playlist_add_video(self.playlist_id, video_id)

    def delete_video(self, video_id: SupportsInt | str):
        """Remove a video from this playlist

    Args:
        video_id (SupportsInt | str): The numeric ID of the video to remove, in base 10 or 36.
        """

        self.servicephp.playlist_delete_video(self.playlist_id, video_id)

    def edit(self, title: str = None, description: str = None, visibility: str = None, channel_id: Optional[SupportsInt | str] = None) -> APIPlaylist:
        """Edit the details of this playlist. WARNING: The original object will probably be stale after this operation.

        Args:
            title (str): The title of the playlist.
                Defaults to staying the same.
            description (str): Describe the playlist.
                Defaults to staying the same.
            visibility (str): Set to public, unlisted, or private via string.
                Defaults to staying the same.
            channel_id (SupportsInt | str): The ID of the channel to have the playlist under. TODO: Cannot be retrieved!
                Defaults to resetting to None.

        Returns:
            playlist (APIPlaylist): The edit result.
        """

        if title is None:
            title = self.title
        if description is None:
            description = self.description
        if visibility is None:
            visibility = self.visibility
        # if channel_id is False:
        #     channel_id = self.channel_id

        return self.servicephp.playlist_edit(self.playlist_id, title, description, visibility, channel_id)

    def delete(self):
        """Delete this playlist"""

        self.servicephp.playlist_delete(self)

playlist_id_b10 property

The numeric ID of the playlist in base 10

playlist_id_b64 property

The numeric ID of the playlist in base 64

__eq__(other)

Determine if this playlist is equal to another.

Parameters:

Name Type Description Default
other Any

Object to compare to.

required

Returns:

Name Type Description
Comparison bool

Did it fit the criteria?

Source code in cocorum/basehandles.py
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
def __eq__(self, other: Any) -> bool:
    """Determine if this playlist is equal to another.

    Args:
        other (Any): Object to compare to.

    Returns:
        Comparison (bool): Did it fit the criteria?
    """

    # Check for direct matches first
    if isinstance(other, int):
        return self.playlist_id_b64 == other
    if isinstance(other, str):
        return str(other) == self.playlist_id_b64

    # # Check for object attributes to match to
    # if hasattr(other, "playlist_id_b10"):
    #     return self.playlist_id_b10 == other.playlist_id_b10

    # # Check conversion to integer last, in case another ID or something happens to match
    # if hasattr(other, "__int__"):
    #     return self.playlist_id_b10 == int(other)

    return False

__int__()

The playlist as an integer (it's ID in base 10)

Source code in cocorum/basehandles.py
248
249
250
def __int__(self) -> int:
    """The playlist as an integer (it's ID in base 10)"""
    return self.playlist_id_b10

__repr__()

String to represent this object

Source code in cocorum/basehandles.py
256
257
258
def __repr__(self) -> str:
    """String to represent this object"""
    return f"{type(self).__name__}(playlist_id={self.playlist_id}, title=\"{self.title}\")"

__str__()

The playlist as a string (it's ID in base 64)

Source code in cocorum/basehandles.py
252
253
254
def __str__(self) -> str:
    """The playlist as a string (it's ID in base 64)"""
    return self.playlist_id_b64

add_video(video_id)

Add a video to this playlist

Parameters:

Name Type Description Default
video_id SupportsInt | str

The numeric ID of the video to add, in base 10 or 36.

required
Source code in cocorum/basehandles.py
297
298
299
300
301
302
303
304
def add_video(self, video_id: SupportsInt | str):
    """Add a video to this playlist

Args:
    video_id (SupportsInt | str): The numeric ID of the video to add, in base 10 or 36.
    """

    return self.servicephp.playlist_add_video(self.playlist_id, video_id)

delete()

Delete this playlist

Source code in cocorum/basehandles.py
343
344
345
346
def delete(self):
    """Delete this playlist"""

    self.servicephp.playlist_delete(self)

delete_video(video_id)

Remove a video from this playlist

Parameters:

Name Type Description Default
video_id SupportsInt | str

The numeric ID of the video to remove, in base 10 or 36.

required
Source code in cocorum/basehandles.py
306
307
308
309
310
311
312
313
def delete_video(self, video_id: SupportsInt | str):
    """Remove a video from this playlist

Args:
    video_id (SupportsInt | str): The numeric ID of the video to remove, in base 10 or 36.
    """

    self.servicephp.playlist_delete_video(self.playlist_id, video_id)

edit(title=None, description=None, visibility=None, channel_id=None)

Edit the details of this playlist. WARNING: The original object will probably be stale after this operation.

Parameters:

Name Type Description Default
title str

The title of the playlist. Defaults to staying the same.

None
description str

Describe the playlist. Defaults to staying the same.

None
visibility str

Set to public, unlisted, or private via string. Defaults to staying the same.

None
channel_id SupportsInt | str

The ID of the channel to have the playlist under. TODO: Cannot be retrieved! Defaults to resetting to None.

None

Returns:

Name Type Description
playlist APIPlaylist

The edit result.

Source code in cocorum/basehandles.py
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
def edit(self, title: str = None, description: str = None, visibility: str = None, channel_id: Optional[SupportsInt | str] = None) -> APIPlaylist:
    """Edit the details of this playlist. WARNING: The original object will probably be stale after this operation.

    Args:
        title (str): The title of the playlist.
            Defaults to staying the same.
        description (str): Describe the playlist.
            Defaults to staying the same.
        visibility (str): Set to public, unlisted, or private via string.
            Defaults to staying the same.
        channel_id (SupportsInt | str): The ID of the channel to have the playlist under. TODO: Cannot be retrieved!
            Defaults to resetting to None.

    Returns:
        playlist (APIPlaylist): The edit result.
    """

    if title is None:
        title = self.title
    if description is None:
        description = self.description
    if visibility is None:
        visibility = self.visibility
    # if channel_id is False:
    #     channel_id = self.channel_id

    return self.servicephp.playlist_edit(self.playlist_id, title, description, visibility, channel_id)

BaseUser

A Rumble user

Source code in cocorum/basehandles.py
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
class BaseUser:
    """A Rumble user"""

    def __int__(self) -> int:
        """The user as an integer (it's ID in base 10)"""
        return self.user_id_b10

    def __eq__(self, other: Any) -> bool:
        """Determine if this user is equal to another.

        Args:
            other (Any): Object to compare to.

        Returns:
            Comparison (bool): Did it fit the criteria?
        """

        # Check for direct matches first
        if isinstance(other, int):
            return self.user_id_b10 == other
        if isinstance(other, str):
            return str(other) in (self.user_id_b36, self.username)

        # Check for object attributes to match to
        if hasattr(other, "user_id"):
            return self.user_id_b10 == utils.ensure_b10(other.user_id)

        # Check conversion to integer last, in case another ID or something happens to match
        if hasattr(other, "__int__"):
            return self.user_id_b10 == int(other)

        return False

    @property
    def user_id_b10(self) -> int:
        """The numeric ID of the user in base 10"""
        return self.user_id

    @property
    def user_id_b36(self) -> str:
        """The numeric ID of the user in base 36"""
        return utils.base_10_to_36(self.user_id)

    def mute(self, duration: int = None, total: bool = False):
        """Mute this user.

    Args:
        duration (int): How long to mute the user in seconds.
            Defaults to infinite.
        total (bool): Wether or not they are muted across all videos.
            Defaults to False, just this video.
            """

        self.servicephp.mute(self, self.username, duration, total)

    def unmute(self):
        """Unmute this user."""
        self.servicephp.unmute(self.username)

user_id_b10 property

The numeric ID of the user in base 10

user_id_b36 property

The numeric ID of the user in base 36

__eq__(other)

Determine if this user is equal to another.

Parameters:

Name Type Description Default
other Any

Object to compare to.

required

Returns:

Name Type Description
Comparison bool

Did it fit the criteria?

Source code in cocorum/basehandles.py
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
def __eq__(self, other: Any) -> bool:
    """Determine if this user is equal to another.

    Args:
        other (Any): Object to compare to.

    Returns:
        Comparison (bool): Did it fit the criteria?
    """

    # Check for direct matches first
    if isinstance(other, int):
        return self.user_id_b10 == other
    if isinstance(other, str):
        return str(other) in (self.user_id_b36, self.username)

    # Check for object attributes to match to
    if hasattr(other, "user_id"):
        return self.user_id_b10 == utils.ensure_b10(other.user_id)

    # Check conversion to integer last, in case another ID or something happens to match
    if hasattr(other, "__int__"):
        return self.user_id_b10 == int(other)

    return False

__int__()

The user as an integer (it's ID in base 10)

Source code in cocorum/basehandles.py
188
189
190
def __int__(self) -> int:
    """The user as an integer (it's ID in base 10)"""
    return self.user_id_b10

mute(duration=None, total=False)

Mute this user.

Parameters:

Name Type Description Default
duration int

How long to mute the user in seconds. Defaults to infinite.

None
total bool

Wether or not they are muted across all videos. Defaults to False, just this video.

False
Source code in cocorum/basehandles.py
228
229
230
231
232
233
234
235
236
237
238
def mute(self, duration: int = None, total: bool = False):
    """Mute this user.

Args:
    duration (int): How long to mute the user in seconds.
        Defaults to infinite.
    total (bool): Wether or not they are muted across all videos.
        Defaults to False, just this video.
        """

    self.servicephp.mute(self, self.username, duration, total)

unmute()

Unmute this user.

Source code in cocorum/basehandles.py
240
241
242
def unmute(self):
    """Unmute this user."""
    self.servicephp.unmute(self.username)

BaseUserBadge

A badge on a username

Source code in cocorum/basehandles.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
class BaseUserBadge:
    """A badge on a username"""

    def __eq__(self, other: Any) -> bool:
        """Check if this badge is equal to another.

        Args:
            other (Any): Object to compare to.

        Returns:
            Comparison (bool): Did it fit the criteria?
        """

        # Check if the string is either our slug or our label in any language
        if isinstance(other, str):
            return other in (self.slug, self.label.values())

        # Check if the compared object has the same slug, if it has one
        if hasattr(other, "slug"):
            return self.slug == other.slug

        return False

    def __str__(self) -> str:
        """The chat user badge in string form"""
        return self.slug

    def __repr__(self) -> str:
        """String to represent this object"""
        return f"{type(self).__name__}(slug='{self.slug}')"

    @property
    def icon(self) -> bytes:
        """The badge's icon as a bytestring"""
        if not self.__icon:  # We never queried the icon before
            # TODO make the timeout configurable
            response = requests.get(
                self.icon_url, timeout=static.Delays.request_timeout)
            assert response.status_code == 200, "Status code " + \
                str(response.status_code)

            self.__icon = response.content

        return self.__icon

icon property

The badge's icon as a bytestring

__eq__(other)

Check if this badge is equal to another.

Parameters:

Name Type Description Default
other Any

Object to compare to.

required

Returns:

Name Type Description
Comparison bool

Did it fit the criteria?

Source code in cocorum/basehandles.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def __eq__(self, other: Any) -> bool:
    """Check if this badge is equal to another.

    Args:
        other (Any): Object to compare to.

    Returns:
        Comparison (bool): Did it fit the criteria?
    """

    # Check if the string is either our slug or our label in any language
    if isinstance(other, str):
        return other in (self.slug, self.label.values())

    # Check if the compared object has the same slug, if it has one
    if hasattr(other, "slug"):
        return self.slug == other.slug

    return False

__repr__()

String to represent this object

Source code in cocorum/basehandles.py
59
60
61
def __repr__(self) -> str:
    """String to represent this object"""
    return f"{type(self).__name__}(slug='{self.slug}')"

__str__()

The chat user badge in string form

Source code in cocorum/basehandles.py
55
56
57
def __str__(self) -> str:
    """The chat user badge in string form"""
    return self.slug

HTMLChannel

Bases: HTMLObj

Channel under a user as extracted from their channels page

Source code in cocorum/scraping.py
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
class HTMLChannel(HTMLObj):
    """Channel under a user as extracted from their channels page"""

    def __str__(self) -> str:
        """The channel as a string (its slug)"""
        return self.slug

    def __repr__(self) -> str:
        """String to represent this object"""
        return (
            f'{type(self).__name__}(title="{self.title}", channel_id={self.channel_id})'
        )

    def __int__(self) -> int:
        """The channel as an integer (its numeric ID)"""
        return self.channel_id_b10

    def __eq__(self, other: Any) -> bool:
        """Determine if this channel is equal to another.

        Args:
            other (Any): Object to compare to.

        Returns:
            Comparison (bool): Did it fit the criteria?
        """

        # Check for direct matches first
        if isinstance(other, int):
            return self.channel_id_b10 == other
        if isinstance(other, str):
            return str(other) in (self.slug, self.channel_id_b36)

        # Check for object attributes to match to
        if hasattr(other, "channel_id"):
            return self.channel_id_b10 == utils.ensure_b10(other.channel_id)
        if hasattr(other, "slug"):
            return self.slug == other.slug

        # Check conversion to integer last, in case an ID or something happens to match but the other is not actually a channel
        if hasattr(other, "__int__"):
            return self.channel_id_b10 == int(other)

        return False

    @property
    def slug(self) -> str:
        """The unique string ID of the channel"""
        return self["data-slug"]

    @property
    def channel_id(self) -> int:
        """The numeric ID of the channel in base 10"""
        return int(self["data-id"])

    @property
    def channel_id_b10(self) -> int:
        """The numeric ID of the channel in base 10"""
        return self.channel_id

    @property
    def channel_id_b36(self) -> str:
        """The numeric ID of the channel in base 36"""
        return utils.base_10_to_36(self.channel_id)

    @property
    def title(self) -> str:
        """The title of the channel"""
        return self["data-title"]

channel_id property

The numeric ID of the channel in base 10

channel_id_b10 property

The numeric ID of the channel in base 10

channel_id_b36 property

The numeric ID of the channel in base 36

slug property

The unique string ID of the channel

title property

The title of the channel

__eq__(other)

Determine if this channel is equal to another.

Parameters:

Name Type Description Default
other Any

Object to compare to.

required

Returns:

Name Type Description
Comparison bool

Did it fit the criteria?

Source code in cocorum/scraping.py
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
def __eq__(self, other: Any) -> bool:
    """Determine if this channel is equal to another.

    Args:
        other (Any): Object to compare to.

    Returns:
        Comparison (bool): Did it fit the criteria?
    """

    # Check for direct matches first
    if isinstance(other, int):
        return self.channel_id_b10 == other
    if isinstance(other, str):
        return str(other) in (self.slug, self.channel_id_b36)

    # Check for object attributes to match to
    if hasattr(other, "channel_id"):
        return self.channel_id_b10 == utils.ensure_b10(other.channel_id)
    if hasattr(other, "slug"):
        return self.slug == other.slug

    # Check conversion to integer last, in case an ID or something happens to match but the other is not actually a channel
    if hasattr(other, "__int__"):
        return self.channel_id_b10 == int(other)

    return False

__int__()

The channel as an integer (its numeric ID)

Source code in cocorum/scraping.py
333
334
335
def __int__(self) -> int:
    """The channel as an integer (its numeric ID)"""
    return self.channel_id_b10

__repr__()

String to represent this object

Source code in cocorum/scraping.py
327
328
329
330
331
def __repr__(self) -> str:
    """String to represent this object"""
    return (
        f'{type(self).__name__}(title="{self.title}", channel_id={self.channel_id})'
    )

__str__()

The channel as a string (its slug)

Source code in cocorum/scraping.py
323
324
325
def __str__(self) -> str:
    """The channel as a string (its slug)"""
    return self.slug

HTMLComment

Bases: HTMLObj, BaseComment

A comment on a video as returned by service.php comment.list

Source code in cocorum/scraping.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
class HTMLComment(HTMLObj, BaseComment):
    """A comment on a video as returned by service.php comment.list"""

    def __init__(self, elem: bs4.Tag, sphp: ServicePHP):
        """A comment on a video as returned by service.php comment.list

        Args:
            elem (bs4.Tag): The <li> element of the comment.
            sphp (ServicePHP): The parent ServicePHP, for convenience methods.
        """

        HTMLObj.__init__(self, elem, sphp)

        # Badges of the user who commented if we have them
        badges_unkeyed = (
            HTMLUserBadge(badge_elem, sphp)
            for badge_elem in self._elem.find_all(
                "li", attrs={"class": "comments-meta-user-badge"}
            )
        )

        self.user_badges: dict[str, HTMLUserBadge] = {
            badge.slug: badge for badge in badges_unkeyed
        }
        """The badges that this comment's user has, by slug"""

    def __repr__(self) -> str:
        """String to represent this object"""
        return (
            f"{type(self).__name__}(username='{self.username}', text=\"{self.text}\")"
        )

    @property
    def is_first(self) -> bool:
        """Is this comment the first one?"""
        return "comment-item-first" in self["class"]

    @property
    def comment_id(self) -> int:
        """The numeric ID of the comment in base 10"""
        return int(self["data-comment-id"])

    @property
    def text(self) -> str:
        """The text of the comment"""
        return self._elem.find("p", attrs={"class": "comment-text"}).string

    @property
    def username(self) -> str:
        """The name of the user who commented"""
        return self["data-username"]

    @property
    def entity_type(self) -> str:
        """Wether the comment was made by a user or a channel"""
        return self["data-entity-type"]

    @property
    def video_id(self) -> int:
        """The base 10 ID of the video the comment was posted on"""
        return self["data-video-fid"]

    @property
    def video_id_b10(self) -> int:
        """The base 10 ID of the video the comment was posted on"""
        return self.video_id

    @property
    def video_id_b36(self) -> str:
        """The base 36 ID of the video the comment was posted on"""
        return utils.base_10_to_36(self.video_id)

    @property
    def actions(self) -> str:
        """Allowed actions on this comment based on the login used to retrieve
        it"""
        return self["data-actions"].split(",")

    @property
    def get_rumbles(self) -> HTMLContentVotes:
        """The votes on this comment"""
        return HTMLContentVotes(self._elem.find("div", attrs={"class": "rumbles-vote"}))

actions property

Allowed actions on this comment based on the login used to retrieve it

comment_id property

The numeric ID of the comment in base 10

entity_type property

Wether the comment was made by a user or a channel

get_rumbles property

The votes on this comment

is_first property

Is this comment the first one?

text property

The text of the comment

user_badges = {(badge.slug): badge for badge in badges_unkeyed} instance-attribute

The badges that this comment's user has, by slug

username property

The name of the user who commented

video_id property

The base 10 ID of the video the comment was posted on

video_id_b10 property

The base 10 ID of the video the comment was posted on

video_id_b36 property

The base 36 ID of the video the comment was posted on

__init__(elem, sphp)

A comment on a video as returned by service.php comment.list

Parameters:

Name Type Description Default
elem Tag

The

  • element of the comment.

  • required
    sphp ServicePHP

    The parent ServicePHP, for convenience methods.

    required
    Source code in cocorum/scraping.py
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    def __init__(self, elem: bs4.Tag, sphp: ServicePHP):
        """A comment on a video as returned by service.php comment.list
    
        Args:
            elem (bs4.Tag): The <li> element of the comment.
            sphp (ServicePHP): The parent ServicePHP, for convenience methods.
        """
    
        HTMLObj.__init__(self, elem, sphp)
    
        # Badges of the user who commented if we have them
        badges_unkeyed = (
            HTMLUserBadge(badge_elem, sphp)
            for badge_elem in self._elem.find_all(
                "li", attrs={"class": "comments-meta-user-badge"}
            )
        )
    
        self.user_badges: dict[str, HTMLUserBadge] = {
            badge.slug: badge for badge in badges_unkeyed
        }
        """The badges that this comment's user has, by slug"""
    

    __repr__()

    String to represent this object

    Source code in cocorum/scraping.py
    120
    121
    122
    123
    124
    def __repr__(self) -> str:
        """String to represent this object"""
        return (
            f"{type(self).__name__}(username='{self.username}', text=\"{self.text}\")"
        )
    

    HTMLContentVotes

    Bases: HTMLObj, BaseContentVotes

    Votes made on content

    Source code in cocorum/scraping.py
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    class HTMLContentVotes(HTMLObj, BaseContentVotes):
        """Votes made on content"""
    
        def __str__(self) -> str:
            """The string form of the content votes"""
            # return self.score_formatted
            return str(self.score)
    
        @property
        def score(self) -> int:
            """Summed score of the content"""
            return int(self._elem.find("span", attrs={"class": "rumbles-count"}).string)
    
        @property
        def content_type(self) -> int:
            """The type of content being voted on"""
            return int(self["data-type"])
    
        @property
        def content_id(self) -> int:
            """The numerical ID of the content being voted on, in base 10"""
            return int(self["data-id"])
    

    content_id property

    The numerical ID of the content being voted on, in base 10

    content_type property

    The type of content being voted on

    score property

    Summed score of the content

    __str__()

    The string form of the content votes

    Source code in cocorum/scraping.py
    181
    182
    183
    184
    def __str__(self) -> str:
        """The string form of the content votes"""
        # return self.score_formatted
        return str(self.score)
    

    HTMLObj

    Abstract object scraped from bs4 HTML

    Source code in cocorum/scraping.py
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    class HTMLObj:
        """Abstract object scraped from bs4 HTML"""
    
        def __init__(self, elem: bs4.Tag, sphp: Optional[ServicePHP] = None):
            """Abstract object scraped from bs4 HTML
    
            Args:
                elem (bs4.Tag): The BeautifulSoup element to base our data on.
                sphp (ServicePHP): The parent ServicePHP, for convenience methods.
                    Defaults to None.
            """
    
            self._elem: bs4.Tag = elem
            """The BeautifulSoup element our data is based on"""
    
            self.servicephp: Optional[ServicePHP] = sphp
            """Our parent ServicePHP wrapper, if we were given one"""
    
        def __getitem__(self, key: str):
            """Get a key from the element attributes
    
            Args:
                key (str): A valid attribute name.
            """
    
            return self._elem.attrs[key]
    

    servicephp = sphp instance-attribute

    Our parent ServicePHP wrapper, if we were given one

    __getitem__(key)

    Get a key from the element attributes

    Parameters:

    Name Type Description Default
    key str

    A valid attribute name.

    required
    Source code in cocorum/scraping.py
    54
    55
    56
    57
    58
    59
    60
    61
    def __getitem__(self, key: str):
        """Get a key from the element attributes
    
        Args:
            key (str): A valid attribute name.
        """
    
        return self._elem.attrs[key]
    

    __init__(elem, sphp=None)

    Abstract object scraped from bs4 HTML

    Parameters:

    Name Type Description Default
    elem Tag

    The BeautifulSoup element to base our data on.

    required
    sphp ServicePHP

    The parent ServicePHP, for convenience methods. Defaults to None.

    None
    Source code in cocorum/scraping.py
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    def __init__(self, elem: bs4.Tag, sphp: Optional[ServicePHP] = None):
        """Abstract object scraped from bs4 HTML
    
        Args:
            elem (bs4.Tag): The BeautifulSoup element to base our data on.
            sphp (ServicePHP): The parent ServicePHP, for convenience methods.
                Defaults to None.
        """
    
        self._elem: bs4.Tag = elem
        """The BeautifulSoup element our data is based on"""
    
        self.servicephp: Optional[ServicePHP] = sphp
        """Our parent ServicePHP wrapper, if we were given one"""
    

    HTMLPlaylist

    Bases: HTMLObj, BasePlaylist

    A playlist as obtained from HTML data

    Source code in cocorum/scraping.py
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    class HTMLPlaylist(HTMLObj, BasePlaylist):
        """A playlist as obtained from HTML data"""
    
        def __init__(self, elem: bs4.Tag, scraper: Scraper):
            """A playlist as obtained from HTML data.
    
            Args:
                elem (bs4.Tag): The playlist class = "thumbnail__grid-item" element.
                scraper (Scraper): The HTML scraper object that spawned us.
            """
    
            HTMLObj.__init__(self, elem)
    
            self.scraper: Scraper = scraper
            """Our parent Scraper instance"""
    
            self.servicephp: ServicePHP = self.scraper.servicephp
            """Convenience access to our parent scraper's ServicePHP instance"""
    
            # The binary data of our thumbnail
            self.__thumbnail = None
    
            # The loaded page of the playlist
            self.__pagesoup = None
    
        @property
        def _pagesoup(self) -> bs4.BeautifulSoup:
            """The loaded page of the playlist"""
            if not self.__pagesoup:
                self.__pagesoup = self.scraper.soup_request(self.url)
    
            return self.__pagesoup
    
        @property
        def thumbnail_url(self) -> str:
            """The url of the playlist's thumbnail image"""
            return self._elem.find("img", attrs={"class": "thumbnail__image"}).get("src")
    
        @property
        def thumbnail(self) -> bytes:
            """The playlist thumbnail as a binary string"""
            if not self.__thumbnail:  # We never queried the thumbnail before
                response = requests.get(
                    self.thumbnail_url, timeout=static.Delays.request_timeout
                )
                assert response.status_code == 200, "Status code " + str(
                    response.status_code
                )
    
                self.__thumbnail = response.content
    
            return self.__thumbnail
    
        @property
        def _url_raw(self) -> str:
            """The URL of the playlist page (without Rumble base URL)"""
            return self._elem.find("a", attrs={"class": "playlist__name link"}).get("href")
    
        @property
        def url(self) -> str:
            """The URL of the playlist page"""
            return static.URI.rumble_base + self._url_raw
    
        @property
        def playlist_id(self) -> str:
            """The numeric ID of the playlist in base 64"""
            return self._url_raw.split("/")[-1]
    
        @property
        def _channel_url_raw(self) -> str:
            """The URL of the channel the playlist under (without base URL)"""
            return self._elem.find("a", attrs={"class": "channel__link link"}).get("href")
    
        @property
        def channel_url(self) -> str:
            """The URL of the base user or channel the playlist under"""
            return static.URI.rumble_base + self._channel_url_raw
    
        @property
        def is_under_channel(self) -> bool:
            """Is this playlist under a channel?"""
            return self._channel_url_raw.startswith("/c/")
    
        @property
        def title(self) -> str:
            """The title of the playlist"""
            return self._pagesoup.find(
                "h1", attrs={"class": "playlist-control-panel__playlist-name"}
            ).string.strip()
    
        @property
        def description(self) -> str:
            """The description of the playlist"""
            return self._pagesoup.find(
                "div", attrs={"class": "playlist-control-panel__description"}
            ).string.strip()
    
        @property
        def visibility(self) -> str:
            """The visibility of the playlist"""
            return (
                self._pagesoup.find(
                    "span", attrs={"class": "playlist-control-panel__visibility-state"}
                )
                .string.strip()
                .lower()
            )
    
        @property
        def num_items(self) -> int:
            """The number of items in the playlist"""
            return int(
                self._elem.find("span", attrs={"class": "playlist__videos"})
                .string.strip()
                .removesuffix(" videos")
            )
    

    channel_url property

    The URL of the base user or channel the playlist under

    description property

    The description of the playlist

    is_under_channel property

    Is this playlist under a channel?

    num_items property

    The number of items in the playlist

    playlist_id property

    The numeric ID of the playlist in base 64

    scraper = scraper instance-attribute

    Our parent Scraper instance

    servicephp = self.scraper.servicephp instance-attribute

    Convenience access to our parent scraper's ServicePHP instance

    thumbnail property

    The playlist thumbnail as a binary string

    thumbnail_url property

    The url of the playlist's thumbnail image

    title property

    The title of the playlist

    url property

    The URL of the playlist page

    visibility property

    The visibility of the playlist

    __init__(elem, scraper)

    A playlist as obtained from HTML data.

    Parameters:

    Name Type Description Default
    elem Tag

    The playlist class = "thumbnail__grid-item" element.

    required
    scraper Scraper

    The HTML scraper object that spawned us.

    required
    Source code in cocorum/scraping.py
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    def __init__(self, elem: bs4.Tag, scraper: Scraper):
        """A playlist as obtained from HTML data.
    
        Args:
            elem (bs4.Tag): The playlist class = "thumbnail__grid-item" element.
            scraper (Scraper): The HTML scraper object that spawned us.
        """
    
        HTMLObj.__init__(self, elem)
    
        self.scraper: Scraper = scraper
        """Our parent Scraper instance"""
    
        self.servicephp: ServicePHP = self.scraper.servicephp
        """Convenience access to our parent scraper's ServicePHP instance"""
    
        # The binary data of our thumbnail
        self.__thumbnail = None
    
        # The loaded page of the playlist
        self.__pagesoup = None
    

    HTMLRLSAPIKeyInfo

    Bases: HTMLObj

    Information on a user or channel's Live Stream API key

    Source code in cocorum/scraping.py
    629
    630
    631
    632
    633
    634
    635
    636
    637
    638
    639
    640
    641
    642
    643
    644
    645
    646
    647
    648
    649
    650
    651
    652
    653
    654
    655
    656
    657
    658
    659
    660
    661
    662
    663
    664
    665
    666
    667
    668
    669
    670
    671
    672
    673
    674
    675
    676
    677
    678
    679
    680
    681
    682
    683
    684
    685
    686
    687
    688
    689
    class HTMLRLSAPIKeyInfo(HTMLObj):
        """Information on a user or channel's Live Stream API key"""
    
        def __init__(self, button: bs4.Tag, servicephp: ServicePHP):
            """
            Information on a user or channel's Live Stream API key.
    
            Args:
                button (bs4.Tag): The reset button element for this key.
                servicephp (ServicePHP): The Service.PHP wrapper for convenience ops.
    
            """
    
            super().__init__(button, servicephp)
    
            self._passbox: bs4.Tag | None = self._elem.parent.find("input")
            """The password box where the API URL is, if we have one"""
    
        def __str__(self) -> str:
            """The API URL with key stored in this info object, if any"""
            return self.url_with_key if self.url_with_key else ""
    
        def __eq__(self, other) -> bool:
            """Compare this object to another"""
            if isinstance(other, str) or hasattr(other, "__str__"):
                return self.url_with_key == str(other)
    
            return False
    
        @property
        def channel_id(self) -> int | None:
            """The channel ID this key info is associated with, if any, in base 10"""
            return (
                int(self._elem["data-channel-id"])
                if self._elem["data-channel-id"]
                else None
            )
    
        @property
        def channel_id_b10(self) -> int | None:
            """The channel ID this key info is associated with, if any, in base 10"""
            return self.channel_id
    
        @property
        def channel_id_b36(self) -> str | None:
            """The channel ID this key info is associated with, if any, in base 36"""
            return utils.ensure_b36(self.channel_id) if self.channel_id else None
    
        @property
        def url_with_key(self) -> str | None:
            """The Live Stream API URL with key, if one has been generated"""
            return self._passbox["value"] if self._passbox else None
    
        def reset_key(self) -> bool:
            """
            Reset or generate this Live Stream API key. WARNING: Makes this object stale!
    
            Returns:
                success (bool): The success attribute of the response JSON.
            """
            return self.servicephp.reset_rls_api_key(self.channel_id)
    

    channel_id property

    The channel ID this key info is associated with, if any, in base 10

    channel_id_b10 property

    The channel ID this key info is associated with, if any, in base 10

    channel_id_b36 property

    The channel ID this key info is associated with, if any, in base 36

    url_with_key property

    The Live Stream API URL with key, if one has been generated

    __eq__(other)

    Compare this object to another

    Source code in cocorum/scraping.py
    651
    652
    653
    654
    655
    656
    def __eq__(self, other) -> bool:
        """Compare this object to another"""
        if isinstance(other, str) or hasattr(other, "__str__"):
            return self.url_with_key == str(other)
    
        return False
    

    __init__(button, servicephp)

    Information on a user or channel's Live Stream API key.

    Parameters:

    Name Type Description Default
    button Tag

    The reset button element for this key.

    required
    servicephp ServicePHP

    The Service.PHP wrapper for convenience ops.

    required
    Source code in cocorum/scraping.py
    632
    633
    634
    635
    636
    637
    638
    639
    640
    641
    642
    643
    644
    645
    def __init__(self, button: bs4.Tag, servicephp: ServicePHP):
        """
        Information on a user or channel's Live Stream API key.
    
        Args:
            button (bs4.Tag): The reset button element for this key.
            servicephp (ServicePHP): The Service.PHP wrapper for convenience ops.
    
        """
    
        super().__init__(button, servicephp)
    
        self._passbox: bs4.Tag | None = self._elem.parent.find("input")
        """The password box where the API URL is, if we have one"""
    

    __str__()

    The API URL with key stored in this info object, if any

    Source code in cocorum/scraping.py
    647
    648
    649
    def __str__(self) -> str:
        """The API URL with key stored in this info object, if any"""
        return self.url_with_key if self.url_with_key else ""
    

    reset_key()

    Reset or generate this Live Stream API key. WARNING: Makes this object stale!

    Returns:

    Name Type Description
    success bool

    The success attribute of the response JSON.

    Source code in cocorum/scraping.py
    682
    683
    684
    685
    686
    687
    688
    689
    def reset_key(self) -> bool:
        """
        Reset or generate this Live Stream API key. WARNING: Makes this object stale!
    
        Returns:
            success (bool): The success attribute of the response JSON.
        """
        return self.servicephp.reset_rls_api_key(self.channel_id)
    

    HTMLUserBadge

    Bases: HTMLObj, BaseUserBadge

    A user badge as extracted from a bs4 HTML element

    Source code in cocorum/scraping.py
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    class HTMLUserBadge(HTMLObj, BaseUserBadge):
        """A user badge as extracted from a bs4 HTML element"""
    
        def __init__(self, elem: bs4.Tag, sphp: ServicePHP):
            """A user badge as extracted from a bs4 HTML element.
    
            Args:
                elem (bs4.Tag): The badge <img> element.
            """
    
            HTMLObj.__init__(self, elem, sphp)
    
            self.slug: str = elem.attrs["src"].split(
                "/")[-1: elem.attrs["src"].rfind("_")]
            """The space-less string identifier for this badge type"""
    
            self.__icon = None
    
        @property
        def label(self) -> str:
            """The string label of the badge in whatever language the Service.PHP
            agent used"""
            return self["title"]
    
        @property
        def icon_url(self) -> str:
            """The URL of the badge's icon"""
            return static.URI.rumble_base + self["src"]
    

    icon_url property

    The URL of the badge's icon

    label property

    The string label of the badge in whatever language the Service.PHP agent used

    slug = elem.attrs['src'].split('/')[(-1):(elem.attrs['src'].rfind('_'))] instance-attribute

    The space-less string identifier for this badge type

    __init__(elem, sphp)

    A user badge as extracted from a bs4 HTML element.

    Parameters:

    Name Type Description Default
    elem Tag

    The badge element.

    required
    Source code in cocorum/scraping.py
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    def __init__(self, elem: bs4.Tag, sphp: ServicePHP):
        """A user badge as extracted from a bs4 HTML element.
    
        Args:
            elem (bs4.Tag): The badge <img> element.
        """
    
        HTMLObj.__init__(self, elem, sphp)
    
        self.slug: str = elem.attrs["src"].split(
            "/")[-1: elem.attrs["src"].rfind("_")]
        """The space-less string identifier for this badge type"""
    
        self.__icon = None
    

    HTMLVideo

    Bases: HTMLObj

    Video on a user or channel page as extracted from the page's HTML

    Source code in cocorum/scraping.py
    391
    392
    393
    394
    395
    396
    397
    398
    399
    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    417
    418
    419
    420
    421
    422
    423
    424
    425
    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    437
    438
    439
    440
    441
    442
    443
    444
    445
    446
    447
    448
    449
    450
    451
    452
    453
    454
    455
    456
    457
    458
    459
    460
    461
    462
    463
    464
    465
    466
    467
    468
    469
    470
    471
    472
    473
    474
    475
    476
    477
    478
    479
    480
    481
    482
    483
    484
    485
    486
    487
    488
    489
    490
    491
    492
    493
    494
    495
    496
    497
    498
    499
    500
    501
    class HTMLVideo(HTMLObj):
        """Video on a user or channel page as extracted from the page's HTML"""
    
        def __init__(self, elem: bs4.Tag):
            """Video on a user or channel page as extracted from the page's HTML.
    
            Args:
                elem (bs4.Tag): The class = "thumbnail__grid-item" video element.
            """
    
            super().__init__(elem)
    
            # The binary data of our thumbnail
            self.__thumbnail = None
    
        def __int__(self) -> int:
            """The video as an integer (it's numeric ID)"""
            return self.video_id_b10
    
        def __str__(self) -> str:
            """The video as a string (it's ID in base 36)"""
            return self.video_id_b36
    
        def __repr__(self) -> str:
            """String to represent this object"""
            return f'{type(self).__name__}(title="{self.title}", video_id={self.video_id})'
    
        def __eq__(self, other: Any) -> bool:
            """Determine if this video is equal to another.
    
            Args:
                other (Any): Object to compare to.
    
            Returns:
                Comparison (bool): Did it fit the criteria?
            """
    
            # Check for direct matches first
            if isinstance(other, int):
                return self.video_id_b10 == other
            if isinstance(other, str):
                return str(other) == self.video_id_b36
    
            # Check for object attributes to match to
            if hasattr(other, "video_id"):
                return self.video_id_b10 == utils.ensure_b10(other.video_id)
            if hasattr(other, "stream_id"):
                return self.video_id_b10 == utils.ensure_b10(other.stream_id)
    
            # Check conversion to integer last, in case another ID or something
            # happens to match
            if hasattr(other, "__int__"):
                return self.video_id_b10 == int(other)
    
            return False
    
        @property
        def video_id(self) -> int:
            """The numeric ID of the video in base 10"""
            return int(self._elem.get("data-video-id"))
    
        @property
        def video_id_b10(self) -> int:
            """The numeric ID of the video in base 10"""
            return self.video_id
    
        @property
        def video_id_b36(self) -> str:
            """The numeric ID of the video in base 36"""
            return utils.base_10_to_36(self.video_id)
    
        @property
        def thumbnail_url(self) -> str:
            """The URL of the video's thumbnail image"""
            return self._elem.find("img", attrs={"class": "thumbnail__image"}).get("src")
    
        @property
        def thumbnail(self) -> bytes:
            """The video thumbnail as a bytestring"""
            if not self.__thumbnail:  # We never queried the thumbnail before
                response = requests.get(
                    self.thumbnail_url, timeout=static.Delays.request_timeout
                )
                assert response.status_code == 200, "Status code " + str(
                    response.status_code
                )
    
                self.__thumbnail = response.content
    
            return self.__thumbnail
    
        @property
        def video_url(self) -> str:
            """The URL of the video's viewing page"""
            return static.URI.rumble_base + self._elem.find(
                "a", attrs={"class": "videostream__link link"}
            ).get("href")
    
        @property
        def title(self) -> str:
            """The title of the video"""
            return self._elem.find("h3", attrs={"class": "thumbnail__title"}).get("title")
    
        @property
        def upload_date(self) -> float:
            """The time that the video was uploaded, in seconds since epoch"""
            return utils.parse_timestamp(
                self._elem.find(
                    "time", attrs={"class": "videostream__data--subitem videostream__time"}
                ).get("datetime")
            )
    

    thumbnail property

    The video thumbnail as a bytestring

    thumbnail_url property

    The URL of the video's thumbnail image

    title property

    The title of the video

    upload_date property

    The time that the video was uploaded, in seconds since epoch

    video_id property

    The numeric ID of the video in base 10

    video_id_b10 property

    The numeric ID of the video in base 10

    video_id_b36 property

    The numeric ID of the video in base 36

    video_url property

    The URL of the video's viewing page

    __eq__(other)

    Determine if this video is equal to another.

    Parameters:

    Name Type Description Default
    other Any

    Object to compare to.

    required

    Returns:

    Name Type Description
    Comparison bool

    Did it fit the criteria?

    Source code in cocorum/scraping.py
    418
    419
    420
    421
    422
    423
    424
    425
    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    437
    438
    439
    440
    441
    442
    443
    444
    445
    def __eq__(self, other: Any) -> bool:
        """Determine if this video is equal to another.
    
        Args:
            other (Any): Object to compare to.
    
        Returns:
            Comparison (bool): Did it fit the criteria?
        """
    
        # Check for direct matches first
        if isinstance(other, int):
            return self.video_id_b10 == other
        if isinstance(other, str):
            return str(other) == self.video_id_b36
    
        # Check for object attributes to match to
        if hasattr(other, "video_id"):
            return self.video_id_b10 == utils.ensure_b10(other.video_id)
        if hasattr(other, "stream_id"):
            return self.video_id_b10 == utils.ensure_b10(other.stream_id)
    
        # Check conversion to integer last, in case another ID or something
        # happens to match
        if hasattr(other, "__int__"):
            return self.video_id_b10 == int(other)
    
        return False
    

    __init__(elem)

    Video on a user or channel page as extracted from the page's HTML.

    Parameters:

    Name Type Description Default
    elem Tag

    The class = "thumbnail__grid-item" video element.

    required
    Source code in cocorum/scraping.py
    394
    395
    396
    397
    398
    399
    400
    401
    402
    403
    404
    def __init__(self, elem: bs4.Tag):
        """Video on a user or channel page as extracted from the page's HTML.
    
        Args:
            elem (bs4.Tag): The class = "thumbnail__grid-item" video element.
        """
    
        super().__init__(elem)
    
        # The binary data of our thumbnail
        self.__thumbnail = None
    

    __int__()

    The video as an integer (it's numeric ID)

    Source code in cocorum/scraping.py
    406
    407
    408
    def __int__(self) -> int:
        """The video as an integer (it's numeric ID)"""
        return self.video_id_b10
    

    __repr__()

    String to represent this object

    Source code in cocorum/scraping.py
    414
    415
    416
    def __repr__(self) -> str:
        """String to represent this object"""
        return f'{type(self).__name__}(title="{self.title}", video_id={self.video_id})'
    

    __str__()

    The video as a string (it's ID in base 36)

    Source code in cocorum/scraping.py
    410
    411
    412
    def __str__(self) -> str:
        """The video as a string (it's ID in base 36)"""
        return self.video_id_b36
    

    HTMLVideoSettings

    Bases: HTMLObj

    Video settings from preparing to edit them

    Source code in cocorum/scraping.py
    504
    505
    506
    507
    508
    509
    510
    511
    512
    513
    514
    515
    516
    517
    518
    519
    520
    521
    522
    523
    524
    525
    526
    527
    528
    529
    530
    531
    532
    533
    534
    535
    536
    537
    538
    539
    540
    541
    542
    543
    544
    545
    546
    547
    548
    549
    550
    551
    552
    553
    554
    555
    556
    557
    558
    559
    560
    561
    562
    563
    564
    565
    566
    567
    568
    569
    570
    571
    572
    573
    574
    575
    576
    577
    578
    579
    580
    581
    582
    583
    584
    585
    586
    587
    588
    589
    590
    591
    592
    593
    594
    595
    596
    597
    598
    599
    600
    601
    602
    603
    604
    605
    606
    607
    608
    609
    610
    611
    612
    613
    614
    615
    616
    617
    618
    619
    620
    621
    622
    623
    624
    class HTMLVideoSettings(HTMLObj):
        """Video settings from preparing to edit them"""
    
        def __init__(self, elem: bs4.Tag, servicephp: ServicePHP):
            """Video on a user or channel page as extracted from the page's HTML.
    
            Args:
                elem (bs4.Tag): The returned HTML from the request.
                servicephp (ServicePHP): We may not need this. TODO.
            """
    
            super().__init__(elem, servicephp)
    
            # The binary data of our thumbnail
            self.__thumbnail = None
    
        def __repr__(self) -> str:
            """String to represent this object"""
            return f'{type(self).__name__}(title="{self.title}")'
    
        @property
        def thumbnail_url(self) -> str:
            """The URL to the thumbnail of the video"""
            label = self._elem.find(
                lambda tag: tag.name == "label" and "Thumbnail" in tag.text
            )
            for tag in label.next_siblings:
                if tag.name == "img":
                    break
            assert tag.name == "img", "Could not find thumbnail image tag"
    
            return tag["src"]
    
        @property
        def thumbnail(self) -> bytes:
            """The video thumbnail as a bytestring"""
            if not self.__thumbnail:  # We never queried the thumbnail before
                response = requests.get(
                    self.thumbnail_url, timeout=static.Delays.request_timeout
                )
                assert response.status_code == 200, "Status code " + str(
                    response.status_code
                )
    
                self.__thumbnail = response.content
    
            return self.__thumbnail
    
        @property
        def title(self) -> str:
            """The video title"""
            return self._elem.find(name="input", id="title")["value"]
    
        @property
        def description(self) -> str:
            """The video description"""
            return self._elem.find(name="textarea", id="description").text
    
        @property
        def tags(self) -> list[str]:
            """The video tags"""
            return self._elem.find("input", id="tags")["value"].split(static.Misc.tag_split)
    
        @property
        def youtube_url(self) -> str:
            """The URL of the video on YouTube"""
            return self._elem.find(name="input", id="youtube-url")["value"]
    
        @property
        def category_primary(self) -> (str, int):
            """The name and numeric ID of the video's primary category"""
            tag = self._elem.find(name="select", id="siteChannelId").find(
                "option", selected=True
            )
            return tag.text.strip(), int(tag["value"])
    
        @property
        def category_secondary(self) -> (str, int):
            """The name and numeric ID of the video's secondary category"""
            tag = self._elem.find(name="select", id="mediaChannelId").find(
                "option", selected=True
            )
            if tag:
                return tag.text.strip(), int(tag["value"])
            # No secondary channel was selected
            return None, 0
    
        @property
        def channel(self) -> (str | None, int):
            """The name and numeric ID of the channel the video was posted to"""
            tag = self._elem.find(name="select", id="channelId").find(
                "option", selected=True
            )
            if tag["value"]:
                return tag.text.strip(), int(tag["value"])
    
            # No channel was selected, video is posted under user account
            return None, 0
    
        @property
        def channel_featured(self) -> bool:
            """Wether this video is featured on the top of the channel"""
            return bool(
                self._elem.find("input", type="checkbox",
                                id="featured_for_channel").checked
            )
    
        @property
        def profile_featured(self) -> bool:
            """Wether this video is featured on the top of the profile"""
            return bool(
                self._elem.find("input", type="checkbox",
                                id="featured_for_user").checked
            )
    
        @property
        def visibility(self) -> str:
            """The video's visibility setting"""
            return self._elem.find("input", attrs={"name": "visibility"}, checked=True)[
                "value"
            ]
    

    category_primary property

    The name and numeric ID of the video's primary category

    category_secondary property

    The name and numeric ID of the video's secondary category

    channel property

    The name and numeric ID of the channel the video was posted to

    Wether this video is featured on the top of the channel

    description property

    The video description

    Wether this video is featured on the top of the profile

    tags property

    The video tags

    thumbnail property

    The video thumbnail as a bytestring

    thumbnail_url property

    The URL to the thumbnail of the video

    title property

    The video title

    visibility property

    The video's visibility setting

    youtube_url property

    The URL of the video on YouTube

    __init__(elem, servicephp)

    Video on a user or channel page as extracted from the page's HTML.

    Parameters:

    Name Type Description Default
    elem Tag

    The returned HTML from the request.

    required
    servicephp ServicePHP

    We may not need this. TODO.

    required
    Source code in cocorum/scraping.py
    507
    508
    509
    510
    511
    512
    513
    514
    515
    516
    517
    518
    def __init__(self, elem: bs4.Tag, servicephp: ServicePHP):
        """Video on a user or channel page as extracted from the page's HTML.
    
        Args:
            elem (bs4.Tag): The returned HTML from the request.
            servicephp (ServicePHP): We may not need this. TODO.
        """
    
        super().__init__(elem, servicephp)
    
        # The binary data of our thumbnail
        self.__thumbnail = None
    

    __repr__()

    String to represent this object

    Source code in cocorum/scraping.py
    520
    521
    522
    def __repr__(self) -> str:
        """String to represent this object"""
        return f'{type(self).__name__}(title="{self.title}")'
    

    Scraper

    Scraper for general information

    Source code in cocorum/scraping.py
    692
    693
    694
    695
    696
    697
    698
    699
    700
    701
    702
    703
    704
    705
    706
    707
    708
    709
    710
    711
    712
    713
    714
    715
    716
    717
    718
    719
    720
    721
    722
    723
    724
    725
    726
    727
    728
    729
    730
    731
    732
    733
    734
    735
    736
    737
    738
    739
    740
    741
    742
    743
    744
    745
    746
    747
    748
    749
    750
    751
    752
    753
    754
    755
    756
    757
    758
    759
    760
    761
    762
    763
    764
    765
    766
    767
    768
    769
    770
    771
    772
    773
    774
    775
    776
    777
    778
    779
    780
    781
    782
    783
    784
    785
    786
    787
    788
    789
    790
    791
    792
    793
    794
    795
    796
    797
    798
    799
    800
    801
    802
    803
    804
    805
    806
    807
    808
    809
    810
    811
    812
    813
    814
    815
    816
    817
    818
    819
    820
    821
    822
    823
    824
    825
    826
    827
    828
    829
    830
    831
    832
    833
    834
    835
    836
    837
    838
    839
    840
    841
    842
    843
    844
    845
    846
    847
    848
    849
    850
    851
    852
    853
    854
    855
    856
    857
    858
    859
    860
    861
    862
    863
    864
    865
    866
    867
    868
    869
    870
    871
    872
    873
    874
    875
    876
    877
    878
    879
    880
    881
    882
    883
    884
    885
    886
    887
    888
    889
    890
    891
    892
    893
    894
    895
    896
    897
    898
    899
    900
    901
    902
    903
    904
    905
    906
    907
    908
    909
    910
    911
    912
    913
    914
    915
    916
    917
    918
    919
    920
    921
    922
    923
    924
    925
    926
    927
    928
    929
    930
    931
    932
    933
    934
    935
    936
    937
    938
    939
    940
    941
    942
    943
    944
    945
    946
    947
    948
    class Scraper:
        """Scraper for general information"""
    
        def __init__(self, servicephp: ServicePHP):
            """Scraper for general information.
    
            Args:
                servicephp (ServicePHP): A ServicePHP instance, for authentication.
            """
    
            self.servicephp: ServicePHP = servicephp
            """Our ServicePHP instance, for authentication"""
    
        def __repr__(self) -> str:
            """String to represent this object"""
            return f"{type(self).__name__}(<{self.servicephp.username}>)"
    
        @property
        def session_cookie(self) -> dict[str, str]:
            """The session cookie we are logged in with"""
            return self.servicephp.session_cookie
    
        @property
        def username(self) -> str:
            """Our username (automatically sets the ServicePHP username if it was unknown)"""
            if self.servicephp.username:
                return self.servicephp.username
            return self.validate_username()
    
        def validate_username(self) -> str:
            """Find out the username that ServicePHP's cookie is associated with, setting it in ServicePHP if they do not match.
    
            Returns:
                username (str): The username as shown on the account settings page.
            """
            username = self.soup_request(static.URI.acc_settings_page).\
                find("input", attrs={"name": "username"})["value"]
            if not self.servicephp.username or self.servicephp.username != username:
                self.servicephp.username = username
            return username
    
        def soup_request(self, url: str, allow_soft_404: bool = False) -> bs4.BeautifulSoup:
            """Make a GET request to a URL, and return HTML beautiful soup for
                scraping.
    
            Args:
                url (str): The URL to query.
                allow_soft_404 (bool): Treat a 404 as a success if text is returned.
                    Defaults to False
    
            Returns:
                Soup (bs4.BeautifulSoup): The webpage at the URL, logged-in version.
            """
    
            r = requests.get(
                url,
                cookies=self.session_cookie,
                timeout=static.Delays.request_timeout,
                headers=static.RequestHeaders.user_agent,
            )
    
            assert r.status_code == 200 or (
                allow_soft_404 and r.status_code == 404 and r.text
            ), f"Fetching page {url} failed: {r}\n{r.text}"
    
            return bs4.BeautifulSoup(r.text, features="html.parser")
    
        def get_muted_user_record(
            self, username: Optional[str] = None
        ) -> int | None | dict[str, int]:
            """Get the record IDs for mutes.
    
            Args:
                username (str): Username to find record ID for.
                    Defaults to None, return all records as a dict.
    
            Returns:
                Record (int | dict[str, int] | None): Either the single user's mute record ID, or None if they are not found, or a dict of all username:mute record ID pairs.
            """
    
            # The page we are on
            pagenum = 1
    
            # username : record ID
            record_ids = {}
    
            # While there are more pages
            while True:
                # Get the next page of mutes and search for mute buttons
                soup = self.soup_request(
                    static.URI.mutes_page.format(page=pagenum))
                elems = soup.find_all(
                    "button", attrs={"class": "unmute_action button-small"}
                )
    
                # We reached the last page
                if not elems:
                    break
    
                # Get the record IDs per username from each button
                for e in elems:
                    # We were searching for a specific username and found it
                    if username and e.attrs["data-username"] == username:
                        return e.attrs["data-record-id"]
    
                    record_ids[e.attrs["data-username"]
                               ] = int(e.attrs["data-record-id"])
    
                # Turn the page
                pagenum += 1
    
            # Only return record IDs if we weren't searching for a particular one
            if not username:
                return record_ids
    
            # We were searching for a user and did not find them
            return None
    
        def get_channels(self, username: str = None) -> list[HTMLChannel]:
            """Get all channels under a username.
    
            Args:
                username (str): The username to get the channels under.
                    Defaults to None, use our own username.
    
            Returns:
                Channels (list): List of HTMLChannel objects.
            """
    
            if not username:
                username = self.username
    
            # Get the page of channels and parse for them
            soup = self.soup_request(
                static.URI.channels_page.format(username=username))
            elems = soup.find_all("div", attrs={"data-type": "channel"})
            return [HTMLChannel(e) for e in elems]
    
        def get_videos(
            self, username=None, is_channel=False, max_num=None
        ) -> list[HTMLVideo]:
            """Get the videos under a user or channel.
    
            Args:
                username (str): The name of the user or channel to search under.
                    Defaults to ourselves.
                is_channel (bool): Is this a channel instead of a userpage?
                    Defaults to False.
                max_num (int): The maximum number of videos to retrieve, starting from
                    the newest. WARNING: You will likely hit a 503 error if max_num is
                    None or too high.
                    Defaults to None, return all videos.
                    Note, rounded up to the nearest page.
    
            Returns:
                Videos (list[HTMLVideo]): List of scraped videos.
            """
    
            # default to the logged-in username
            if not username:
                username = self.username
    
            # If this is a channel username, we will need a slightly different URL
            uc = ("user", "c")[is_channel]
    
            # The base userpage URL currently has all their videos / livestreams on it
            url_start = f"{static.URI.rumble_base}/{uc}/{username}"
    
            # Start the loop with:
            # no videos found yet
            # the assumption that there will be new video elements
            # a current page number of 1
            videos = []
            new_video_elems = True
            pagenum = 1
            while new_video_elems and (not max_num or len(videos) < max_num):
                # Get the next page of videos
                soup = self.soup_request(
                    f"{url_start}?page={pagenum}", allow_soft_404=True)
    
                # Search for video listings
                new_video_elems = soup.find_all(
                    "div", attrs={"class": "videostream thumbnail__grid--item"}
                )
    
                # We found some video listings
                if new_video_elems:
                    videos += [HTMLVideo(e) for e in new_video_elems]
    
                # Turn the page
                pagenum += 1
    
            return videos
    
        def get_playlists(self) -> list[HTMLPlaylist]:
            """Get the playlists under the logged in user
    
            Returns:
                playlists (list[HTMLPlaylist]): List of scraped playlists
            """
    
            soup = self.soup_request(static.URI.playlists_page)
            return [
                HTMLPlaylist(elem, self)
                for elem in soup.find_all("div", attrs={"class": "playlist"})
            ]
    
        def get_categories(self) -> (dict[str, int], dict[str, int]):
            """Load the primary and secondary upload categories from Rumble
    
            Returns:
                categories1 (dict[str, int]): The primary categories, name : numeric ID
                categories2 (dict[str, int]): The secondary categories, name : numeric ID"""
    
            # TODO: We may be able to get this from an internal API at studio.rumble.com instead
            # See issue # 13
    
            print("Loading categories")
            soup = self.soup_request(static.URI.uploadphp)
    
            options_box1 = soup.find(
                "input", attrs={"id": "category_primary"}).parent
            options_elems1 = options_box1.find_all(
                "div", attrs={"class": "select-option"})
            categories1 = {
                e.string.strip(): int(e.attrs["data-value"]) for e in options_elems1
            }
    
            options_box2 = soup.find(
                "input", attrs={"id": "category_secondary"}).parent
            options_elems2 = options_box2.find_all(
                "div", attrs={"class": "select-option"})
            categories2 = {
                e.string.strip(): int(e.attrs["data-value"]) for e in options_elems2
            }
    
            return categories1, categories2
    
        def get_rls_api_keys(self) -> list[HTMLRLSAPIKeyInfo]:
            """
            Get information on the Live Stream API keys for the user and each channel.
    
            Returns:
                keys_info (list[HTMLRLSAPIKeyInfo]): An object wrapping the information for each user and channel key.
            """
            soup = self.soup_request(static.URI.rls_api_keys_page)
            buttons = soup.findAll("button", {"class": "reset-livestream-api-key"})
            return [HTMLRLSAPIKeyInfo(button, self.servicephp) for button in buttons]
    
        def get_acc_apikey(self) -> str:
            """Get the apiKey used for some account-related operations.
    
            Returns:
                apiKey (str): Said key, meant to be passed as a parameter in requests"""
    
            soup = self.soup_request(static.URI.account_page)
            return static.Misc.find_acc_apikey.findall(soup.prettify())[0]
    

    servicephp = servicephp instance-attribute

    Our ServicePHP instance, for authentication

    The session cookie we are logged in with

    username property

    Our username (automatically sets the ServicePHP username if it was unknown)

    __init__(servicephp)

    Scraper for general information.

    Parameters:

    Name Type Description Default
    servicephp ServicePHP

    A ServicePHP instance, for authentication.

    required
    Source code in cocorum/scraping.py
    695
    696
    697
    698
    699
    700
    701
    702
    703
    def __init__(self, servicephp: ServicePHP):
        """Scraper for general information.
    
        Args:
            servicephp (ServicePHP): A ServicePHP instance, for authentication.
        """
    
        self.servicephp: ServicePHP = servicephp
        """Our ServicePHP instance, for authentication"""
    

    __repr__()

    String to represent this object

    Source code in cocorum/scraping.py
    705
    706
    707
    def __repr__(self) -> str:
        """String to represent this object"""
        return f"{type(self).__name__}(<{self.servicephp.username}>)"
    

    get_acc_apikey()

    Get the apiKey used for some account-related operations.

    Returns:

    Name Type Description
    apiKey str

    Said key, meant to be passed as a parameter in requests

    Source code in cocorum/scraping.py
    941
    942
    943
    944
    945
    946
    947
    948
    def get_acc_apikey(self) -> str:
        """Get the apiKey used for some account-related operations.
    
        Returns:
            apiKey (str): Said key, meant to be passed as a parameter in requests"""
    
        soup = self.soup_request(static.URI.account_page)
        return static.Misc.find_acc_apikey.findall(soup.prettify())[0]
    

    get_categories()

    Load the primary and secondary upload categories from Rumble

    Returns:

    Name Type Description
    categories1 dict[str, int]

    The primary categories, name : numeric ID

    categories2 dict[str, int]

    The secondary categories, name : numeric ID

    Source code in cocorum/scraping.py
    899
    900
    901
    902
    903
    904
    905
    906
    907
    908
    909
    910
    911
    912
    913
    914
    915
    916
    917
    918
    919
    920
    921
    922
    923
    924
    925
    926
    927
    928
    def get_categories(self) -> (dict[str, int], dict[str, int]):
        """Load the primary and secondary upload categories from Rumble
    
        Returns:
            categories1 (dict[str, int]): The primary categories, name : numeric ID
            categories2 (dict[str, int]): The secondary categories, name : numeric ID"""
    
        # TODO: We may be able to get this from an internal API at studio.rumble.com instead
        # See issue # 13
    
        print("Loading categories")
        soup = self.soup_request(static.URI.uploadphp)
    
        options_box1 = soup.find(
            "input", attrs={"id": "category_primary"}).parent
        options_elems1 = options_box1.find_all(
            "div", attrs={"class": "select-option"})
        categories1 = {
            e.string.strip(): int(e.attrs["data-value"]) for e in options_elems1
        }
    
        options_box2 = soup.find(
            "input", attrs={"id": "category_secondary"}).parent
        options_elems2 = options_box2.find_all(
            "div", attrs={"class": "select-option"})
        categories2 = {
            e.string.strip(): int(e.attrs["data-value"]) for e in options_elems2
        }
    
        return categories1, categories2
    

    get_channels(username=None)

    Get all channels under a username.

    Parameters:

    Name Type Description Default
    username str

    The username to get the channels under. Defaults to None, use our own username.

    None

    Returns:

    Name Type Description
    Channels list

    List of HTMLChannel objects.

    Source code in cocorum/scraping.py
    810
    811
    812
    813
    814
    815
    816
    817
    818
    819
    820
    821
    822
    823
    824
    825
    826
    827
    828
    def get_channels(self, username: str = None) -> list[HTMLChannel]:
        """Get all channels under a username.
    
        Args:
            username (str): The username to get the channels under.
                Defaults to None, use our own username.
    
        Returns:
            Channels (list): List of HTMLChannel objects.
        """
    
        if not username:
            username = self.username
    
        # Get the page of channels and parse for them
        soup = self.soup_request(
            static.URI.channels_page.format(username=username))
        elems = soup.find_all("div", attrs={"data-type": "channel"})
        return [HTMLChannel(e) for e in elems]
    

    get_muted_user_record(username=None)

    Get the record IDs for mutes.

    Parameters:

    Name Type Description Default
    username str

    Username to find record ID for. Defaults to None, return all records as a dict.

    None

    Returns:

    Name Type Description
    Record int | dict[str, int] | None

    Either the single user's mute record ID, or None if they are not found, or a dict of all username:mute record ID pairs.

    Source code in cocorum/scraping.py
    759
    760
    761
    762
    763
    764
    765
    766
    767
    768
    769
    770
    771
    772
    773
    774
    775
    776
    777
    778
    779
    780
    781
    782
    783
    784
    785
    786
    787
    788
    789
    790
    791
    792
    793
    794
    795
    796
    797
    798
    799
    800
    801
    802
    803
    804
    805
    806
    807
    808
    def get_muted_user_record(
        self, username: Optional[str] = None
    ) -> int | None | dict[str, int]:
        """Get the record IDs for mutes.
    
        Args:
            username (str): Username to find record ID for.
                Defaults to None, return all records as a dict.
    
        Returns:
            Record (int | dict[str, int] | None): Either the single user's mute record ID, or None if they are not found, or a dict of all username:mute record ID pairs.
        """
    
        # The page we are on
        pagenum = 1
    
        # username : record ID
        record_ids = {}
    
        # While there are more pages
        while True:
            # Get the next page of mutes and search for mute buttons
            soup = self.soup_request(
                static.URI.mutes_page.format(page=pagenum))
            elems = soup.find_all(
                "button", attrs={"class": "unmute_action button-small"}
            )
    
            # We reached the last page
            if not elems:
                break
    
            # Get the record IDs per username from each button
            for e in elems:
                # We were searching for a specific username and found it
                if username and e.attrs["data-username"] == username:
                    return e.attrs["data-record-id"]
    
                record_ids[e.attrs["data-username"]
                           ] = int(e.attrs["data-record-id"])
    
            # Turn the page
            pagenum += 1
    
        # Only return record IDs if we weren't searching for a particular one
        if not username:
            return record_ids
    
        # We were searching for a user and did not find them
        return None
    

    get_playlists()

    Get the playlists under the logged in user

    Returns:

    Name Type Description
    playlists list[HTMLPlaylist]

    List of scraped playlists

    Source code in cocorum/scraping.py
    886
    887
    888
    889
    890
    891
    892
    893
    894
    895
    896
    897
    def get_playlists(self) -> list[HTMLPlaylist]:
        """Get the playlists under the logged in user
    
        Returns:
            playlists (list[HTMLPlaylist]): List of scraped playlists
        """
    
        soup = self.soup_request(static.URI.playlists_page)
        return [
            HTMLPlaylist(elem, self)
            for elem in soup.find_all("div", attrs={"class": "playlist"})
        ]
    

    get_rls_api_keys()

    Get information on the Live Stream API keys for the user and each channel.

    Returns:

    Name Type Description
    keys_info list[HTMLRLSAPIKeyInfo]

    An object wrapping the information for each user and channel key.

    Source code in cocorum/scraping.py
    930
    931
    932
    933
    934
    935
    936
    937
    938
    939
    def get_rls_api_keys(self) -> list[HTMLRLSAPIKeyInfo]:
        """
        Get information on the Live Stream API keys for the user and each channel.
    
        Returns:
            keys_info (list[HTMLRLSAPIKeyInfo]): An object wrapping the information for each user and channel key.
        """
        soup = self.soup_request(static.URI.rls_api_keys_page)
        buttons = soup.findAll("button", {"class": "reset-livestream-api-key"})
        return [HTMLRLSAPIKeyInfo(button, self.servicephp) for button in buttons]
    

    get_videos(username=None, is_channel=False, max_num=None)

    Get the videos under a user or channel.

    Parameters:

    Name Type Description Default
    username str

    The name of the user or channel to search under. Defaults to ourselves.

    None
    is_channel bool

    Is this a channel instead of a userpage? Defaults to False.

    False
    max_num int

    The maximum number of videos to retrieve, starting from the newest. WARNING: You will likely hit a 503 error if max_num is None or too high. Defaults to None, return all videos. Note, rounded up to the nearest page.

    None

    Returns:

    Name Type Description
    Videos list[HTMLVideo]

    List of scraped videos.

    Source code in cocorum/scraping.py
    830
    831
    832
    833
    834
    835
    836
    837
    838
    839
    840
    841
    842
    843
    844
    845
    846
    847
    848
    849
    850
    851
    852
    853
    854
    855
    856
    857
    858
    859
    860
    861
    862
    863
    864
    865
    866
    867
    868
    869
    870
    871
    872
    873
    874
    875
    876
    877
    878
    879
    880
    881
    882
    883
    884
    def get_videos(
        self, username=None, is_channel=False, max_num=None
    ) -> list[HTMLVideo]:
        """Get the videos under a user or channel.
    
        Args:
            username (str): The name of the user or channel to search under.
                Defaults to ourselves.
            is_channel (bool): Is this a channel instead of a userpage?
                Defaults to False.
            max_num (int): The maximum number of videos to retrieve, starting from
                the newest. WARNING: You will likely hit a 503 error if max_num is
                None or too high.
                Defaults to None, return all videos.
                Note, rounded up to the nearest page.
    
        Returns:
            Videos (list[HTMLVideo]): List of scraped videos.
        """
    
        # default to the logged-in username
        if not username:
            username = self.username
    
        # If this is a channel username, we will need a slightly different URL
        uc = ("user", "c")[is_channel]
    
        # The base userpage URL currently has all their videos / livestreams on it
        url_start = f"{static.URI.rumble_base}/{uc}/{username}"
    
        # Start the loop with:
        # no videos found yet
        # the assumption that there will be new video elements
        # a current page number of 1
        videos = []
        new_video_elems = True
        pagenum = 1
        while new_video_elems and (not max_num or len(videos) < max_num):
            # Get the next page of videos
            soup = self.soup_request(
                f"{url_start}?page={pagenum}", allow_soft_404=True)
    
            # Search for video listings
            new_video_elems = soup.find_all(
                "div", attrs={"class": "videostream thumbnail__grid--item"}
            )
    
            # We found some video listings
            if new_video_elems:
                videos += [HTMLVideo(e) for e in new_video_elems]
    
            # Turn the page
            pagenum += 1
    
        return videos
    

    soup_request(url, allow_soft_404=False)

    Make a GET request to a URL, and return HTML beautiful soup for scraping.

    Parameters:

    Name Type Description Default
    url str

    The URL to query.

    required
    allow_soft_404 bool

    Treat a 404 as a success if text is returned. Defaults to False

    False

    Returns:

    Name Type Description
    Soup BeautifulSoup

    The webpage at the URL, logged-in version.

    Source code in cocorum/scraping.py
    733
    734
    735
    736
    737
    738
    739
    740
    741
    742
    743
    744
    745
    746
    747
    748
    749
    750
    751
    752
    753
    754
    755
    756
    757
    def soup_request(self, url: str, allow_soft_404: bool = False) -> bs4.BeautifulSoup:
        """Make a GET request to a URL, and return HTML beautiful soup for
            scraping.
    
        Args:
            url (str): The URL to query.
            allow_soft_404 (bool): Treat a 404 as a success if text is returned.
                Defaults to False
    
        Returns:
            Soup (bs4.BeautifulSoup): The webpage at the URL, logged-in version.
        """
    
        r = requests.get(
            url,
            cookies=self.session_cookie,
            timeout=static.Delays.request_timeout,
            headers=static.RequestHeaders.user_agent,
        )
    
        assert r.status_code == 200 or (
            allow_soft_404 and r.status_code == 404 and r.text
        ), f"Fetching page {url} failed: {r}\n{r.text}"
    
        return bs4.BeautifulSoup(r.text, features="html.parser")
    

    validate_username()

    Find out the username that ServicePHP's cookie is associated with, setting it in ServicePHP if they do not match.

    Returns:

    Name Type Description
    username str

    The username as shown on the account settings page.

    Source code in cocorum/scraping.py
    721
    722
    723
    724
    725
    726
    727
    728
    729
    730
    731
    def validate_username(self) -> str:
        """Find out the username that ServicePHP's cookie is associated with, setting it in ServicePHP if they do not match.
    
        Returns:
            username (str): The username as shown on the account settings page.
        """
        username = self.soup_request(static.URI.acc_settings_page).\
            find("input", attrs={"name": "username"})["value"]
        if not self.servicephp.username or self.servicephp.username != username:
            self.servicephp.username = username
        return username
    

    S.D.G.