Skip to content

cocorum.scraping

The primary use from this module is the Scraper class, used for getting data from Rumble web pages, or iun 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 2025 Wilbur Jaywright.

This file is part of Cocorum.

Cocorum is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

Cocorum is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License along with Cocorum. If not, see https://www.gnu.org/licenses/.

S.D.G.

BaseComment

A comment on a Rumble video

Source code in cocorum/basehandles.py
 60
 61
 62
 63
 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
 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
class BaseComment:
    """A comment on a Rumble video"""
    def __int__(self):
        """The comment in integer form (its ID)"""
        return self.comment_id

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

    @property
    def comment_id_b10(self):
        """The base 10 ID of the comment"""
        return self.comment_id

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

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

    Args:
        other (int, str, HTMLComment, APIComment): Object to compare to.

    Returns:
        Comparison (bool, None): 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):
        """Un-delete this comment"""

        return self.servicephp.comment_restore(self)

    def set_rumbles(self, vote: int):
        """Post a like or dislike on this comment.

    Args:
        vote (int): -1, 0, or 1 (0 means clear vote).
        """

        return self.servicephp.rumbles(vote, self, item_type = 2)

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 (int, str, HTMLComment, APIComment)

Object to compare to.

required

Returns:

Name Type Description
Comparison (bool, None)

Did it fit the criteria?

Source code in cocorum/basehandles.py
 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
def __eq__(self, other):
    """Determine if this comment is equal to another.

Args:
    other (int, str, HTMLComment, APIComment): Object to compare to.

Returns:
    Comparison (bool, None): 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
62
63
64
def __int__(self):
    """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
66
67
68
def __str__(self):
    """The comment as a string (its text)"""
    return self.text

delete()

Delete this comment

Source code in cocorum/basehandles.py
115
116
117
118
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
106
107
108
109
110
111
112
113
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
120
121
122
123
def restore(self):
    """Un-delete this comment"""

    return self.servicephp.comment_restore(self)

set_rumbles(vote)

Post a like or dislike on this comment.

Parameters:

Name Type Description Default
vote int

-1, 0, or 1 (0 means clear vote).

required
Source code in cocorum/basehandles.py
125
126
127
128
129
130
131
132
def set_rumbles(self, vote: int):
    """Post a like or dislike on this comment.

Args:
    vote (int): -1, 0, or 1 (0 means clear vote).
    """

    return self.servicephp.rumbles(vote, self, item_type = 2)

BaseContentVotes

Likes and dislikes on a video or comment

Source code in cocorum/basehandles.py
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
class BaseContentVotes:
    """Likes and dislikes on a video or comment"""

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

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

    Args:
        other (int, str, HTMLContentVotes): Object to compare to.

    Returns:
        Comparison (bool, None): 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 (int, str, HTMLContentVotes)

Object to compare to.

required

Returns:

Name Type Description
Comparison (bool, None)

Did it fit the criteria?

Source code in cocorum/basehandles.py
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
def __eq__(self, other):
    """Determine if this content votes is equal to another.

Args:
    other (int, str, HTMLContentVotes): Object to compare to.

Returns:
    Comparison (bool, None): 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
137
138
139
def __int__(self):
    """The integer form of the content votes"""
    return self.score

BasePlaylist

A playlist of Rumble videos

Source code in cocorum/basehandles.py
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
318
319
320
321
322
323
class BasePlaylist:
    """A playlist of Rumble videos"""

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

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

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

    Args:
        other (int, str, HTMLPlaylist): Object to compare to.

    Returns:
        Comparison (bool, None): 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):
        """The numeric ID of the playlist in base 64"""
        return self.playlist_id

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

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

    Args:
        video_id (int, 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):
        """Remove a video from this playlist

    Args:
        video_id (int, 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 = None):
        """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 (int | str | None): 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 (int, str, HTMLPlaylist)

Object to compare to.

required

Returns:

Name Type Description
Comparison (bool, None)

Did it fit the criteria?

Source code in cocorum/basehandles.py
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
def __eq__(self, other):
    """Determine if this playlist is equal to another.

Args:
    other (int, str, HTMLPlaylist): Object to compare to.

Returns:
    Comparison (bool, None): 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
229
230
231
def __int__(self):
    """The playlist as an integer (it's ID in base 10)"""
    return self.playlist_id_b10

__str__()

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

Source code in cocorum/basehandles.py
233
234
235
def __str__(self):
    """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 (int, str)

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

required
Source code in cocorum/basehandles.py
274
275
276
277
278
279
280
281
def add_video(self, video_id):
    """Add a video to this playlist

Args:
    video_id (int, 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
320
321
322
323
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 (int, str)

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

required
Source code in cocorum/basehandles.py
283
284
285
286
287
288
289
290
def delete_video(self, video_id):
    """Remove a video from this playlist

Args:
    video_id (int, 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 int | str | None

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
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
def edit(self, title: str = None, description: str = None, visibility: str = None, channel_id = None):
    """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 (int | str | None): 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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
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
class BaseUser:
    """A Rumble user"""

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

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

    Args:
        other (int, str, APIUser): Object to compare to.

    Returns:
        Comparison (bool, None): 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):
        """The numeric ID of the user in base 10"""
        return self.user_id

    @property
    def user_id_b36(self):
        """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 (int, str, APIUser)

Object to compare to.

required

Returns:

Name Type Description
Comparison (bool, None)

Did it fit the criteria?

Source code in cocorum/basehandles.py
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
def __eq__(self, other):
    """Determine if this user is equal to another.

Args:
    other (int, str, APIUser): Object to compare to.

Returns:
    Comparison (bool, None): 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
170
171
172
def __int__(self):
    """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
210
211
212
213
214
215
216
217
218
219
220
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
222
223
224
def unmute(self):
    """Unmute this user."""
    self.servicephp.unmute(self.username)

BaseUserBadge

A badge on a username

Source code in cocorum/basehandles.py
22
23
24
25
26
27
28
29
30
31
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
class BaseUserBadge:
    """A badge on a username"""
    def __eq__(self, other):
        """Check if this badge is equal to another.

    Args:
        other (str, HTMLUserBadge): Object to compare to.

    Returns:
        Comparison (bool, None): 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):
        """The chat user badge in string form"""
        return self.slug

    @property
    def icon(self):
        """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 (str, HTMLUserBadge)

Object to compare to.

required

Returns:

Name Type Description
Comparison (bool, None)

Did it fit the criteria?

Source code in cocorum/basehandles.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def __eq__(self, other):
    """Check if this badge is equal to another.

Args:
    other (str, HTMLUserBadge): Object to compare to.

Returns:
    Comparison (bool, None): 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

__str__()

The chat user badge in string form

Source code in cocorum/basehandles.py
44
45
46
def __str__(self):
    """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
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
class HTMLChannel(HTMLObj):
    """Channel under a user as extracted from their channels page"""

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

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

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

    Args:
        other (int, str, HTMLChannel): Object to compare to.

    Returns:
        Comparison (bool, None): 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)

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

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

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

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

    @property
    def title(self):
        """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 (int, str, HTMLChannel)

Object to compare to.

required

Returns:

Name Type Description
Comparison (bool, None)

Did it fit the criteria?

Source code in cocorum/scraping.py
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
def __eq__(self, other):
    """Determine if this channel is equal to another.

Args:
    other (int, str, HTMLChannel): Object to compare to.

Returns:
    Comparison (bool, None): 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)

__int__()

The channel as an integer (its numeric ID)

Source code in cocorum/scraping.py
276
277
278
def __int__(self):
    """The channel as an integer (its numeric ID)"""
    return self.channel_id_b10

__str__()

The channel as a string (its slug)

Source code in cocorum/scraping.py
272
273
274
def __str__(self):
    """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
 76
 77
 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
class HTMLComment(HTMLObj, BaseComment):
    """A comment on a video as returned by service.php comment.list"""

    def __init__(self, elem, sphp):
        """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 = {badge.slug: badge for badge in badges_unkeyed}

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

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

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

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

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

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

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

    @property
    def video_id_b36(self):
        """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):
        """Allowed actions on this comment based on the login used to retrieve
        it"""
        return self["data-actions"].split(",")

    @property
    def get_rumbles(self):
        """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

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
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    def __init__(self, elem, sphp):
        """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 = {badge.slug: badge for badge in badges_unkeyed}
    

    HTMLContentVotes

    Bases: HTMLObj, BaseContentVotes

    Votes made on content

    Source code in cocorum/scraping.py
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    class HTMLContentVotes(HTMLObj, BaseContentVotes):
        """Votes made on content"""
    
        def __str__(self):
            """The string form of the content votes"""
            # return self.score_formatted
            return str(self.score)
    
        @property
        def score(self):
            """Summed score of the content"""
            return int(self._elem.find("span", attrs={"class": "rumbles-count"}).string)
    
        @property
        def content_type(self):
            """The type of content being voted on"""
            return int(self["data-type"])
    
        @property
        def content_id(self):
            """The numerical ID of the content being voted on"""
            return int(self["data-id"])
    

    content_id property

    The numerical ID of the content being voted on

    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
    148
    149
    150
    151
    def __str__(self):
        """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
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    class HTMLObj:
        """Abstract object scraped from bs4 HTML"""
    
        def __init__(self, elem, sphp = 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 = elem
            self.servicephp = sphp
    
        def __getitem__(self, key):
            """Get a key from the element attributes
    
        Args:
            key (str): A valid attribute name.
            """
    
            return self._elem.attrs[key]
    

    __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
    41
    42
    43
    44
    45
    46
    47
    48
    def __getitem__(self, key):
        """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
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    def __init__(self, elem, sphp = 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 = elem
        self.servicephp = sphp
    

    HTMLPlaylist

    Bases: HTMLObj, BasePlaylist

    A playlist as obtained from HTML data

    Source code in cocorum/scraping.py
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    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
    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
    class HTMLPlaylist(HTMLObj, BasePlaylist):
        """A playlist as obtained from HTML data"""
    
        def __init__(self, elem, 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)
    
            # The Scraper object that created this one
            self.scraper = scraper
    
            # Convenience access to ServicePHP
            self.servicephp = self.scraper.servicephp
    
            # The binary data of our thumbnail
            self.__thumbnail = None
    
            # The loaded page of the playlist
            self.__pagesoup = None
    
        @property
        def _pagesoup(self):
            """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):
            """The url of the playlist's thumbnail image"""
            return self._elem.find("img", attrs={"class": "thumbnail__image"}).get("src")
    
        @property
        def thumbnail(self):
            """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):
            """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):
            """The URL of the playlist page """
            return static.URI.rumble_base + self._url_raw
    
        @property
        def playlist_id(self):
            """The numeric ID of the playlist in base 64"""
            return self._url_raw.split("/")[-1]
    
        @property
        def _channel_url_raw(self):
            """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):
            """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):
            """Is this playlist under a channel?"""
            return self._channel_url_raw.startswith("/c/")
    
        @property
        def title(self):
            """The title of the playlist"""
            return self._pagesoup.find("h1", attrs={"class": "playlist-control-panel__playlist-name"}).string.strip()
    
        @property
        def description(self):
            """The description of the playlist"""
            return self._pagesoup.find("div", attrs={"class": "playlist-control-panel__description"}).string.strip()
    
        @property
        def visibility(self):
            """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):
            """The number of items in the playlist"""
            # TODO: This is doable but I just don't care right now.
            raise NotImplementedError("This is doable but I just don't care right now.")
    

    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

    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
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    def __init__(self, elem, 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)
    
        # The Scraper object that created this one
        self.scraper = scraper
    
        # Convenience access to ServicePHP
        self.servicephp = self.scraper.servicephp
    
        # The binary data of our thumbnail
        self.__thumbnail = None
    
        # The loaded page of the playlist
        self.__pagesoup = None
    

    HTMLUserBadge

    Bases: HTMLObj, BaseUserBadge

    A user badge as extracted from a bs4 HTML element

    Source code in cocorum/scraping.py
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    class HTMLUserBadge(HTMLObj, BaseUserBadge):
        """A user badge as extracted from a bs4 HTML element"""
    
        def __init__(self, elem, sphp):
            """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 = elem.attrs["src"].split("/")[-1:elem.attrs["src"].rfind("_")]
            self.__icon = None
    
        @property
        def label(self):
            """The string label of the badge in whatever language the Service.PHP
            agent used"""
            return self["title"]
    
        @property
        def icon_url(self):
            """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

    __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
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    def __init__(self, elem, sphp):
        """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 = elem.attrs["src"].split("/")[-1:elem.attrs["src"].rfind("_")]
        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
    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
    389
    390
    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
    class HTMLVideo(HTMLObj):
        """Video on a user or channel page as extracted from the page's HTML"""
    
        def __init__(self, elem):
            """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):
            """The video as an integer (it's numeric ID)"""
            return self.video_id_b10
    
        def __str__(self):
            """The video as a string (it's ID in base 36)"""
            return self.video_id_b36
    
        def __eq__(self, other):
            """Determine if this video is equal to another.
    
        Args:
            other (int, str, HTMLVideo): Object to compare to.
    
        Returns:
            Comparison (bool, None): 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)
    
        @property
        def video_id(self):
            """The numeric ID of the video in base 10"""
            return int(self._elem.get("data-video-id"))
    
        @property
        def video_id_b10(self):
            """The numeric ID of the video in base 10"""
            return self.video_id
    
        @property
        def video_id_b36(self):
            """The numeric ID of the video in base 36"""
            return utils.base_10_to_36(self.video_id)
    
        @property
        def thumbnail_url(self):
            """The URL of the video's thumbnail image"""
            return self._elem.find("img", attrs={"class": "thumbnail__image"}).get("src")
    
        @property
        def thumbnail(self):
            """The video 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 video_url(self):
            """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):
            """The title of the video"""
            return self._elem.find("h3", attrs={"class": "thumbnail__title"}).get("title")
    
        @property
        def upload_date(self):
            """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 binary string

    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 (int, str, HTMLVideo)

    Object to compare to.

    required

    Returns:

    Name Type Description
    Comparison (bool, None)

    Did it fit the criteria?

    Source code in cocorum/scraping.py
    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
    def __eq__(self, other):
        """Determine if this video is equal to another.
    
    Args:
        other (int, str, HTMLVideo): Object to compare to.
    
    Returns:
        Comparison (bool, None): 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)
    

    __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
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    def __init__(self, elem):
        """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
    347
    348
    349
    def __int__(self):
        """The video as an integer (it's numeric ID)"""
        return self.video_id_b10
    

    __str__()

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

    Source code in cocorum/scraping.py
    351
    352
    353
    def __str__(self):
        """The video as a string (it's ID in base 36)"""
        return self.video_id_b36
    

    Scraper

    Scraper for general information

    Source code in cocorum/scraping.py
    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
    502
    503
    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
    class Scraper:
        """Scraper for general information"""
    
        def __init__(self, servicephp):
            """Scraper for general information.
    
        Args:
            servicephp (ServicePHP): A ServicePHP instance, for authentication.
            """
    
            self.servicephp = servicephp
    
        @property
        def session_cookie(self):
            """The session cookie we are logged in with"""
            return self.servicephp.session_cookie
    
        @property
        def username(self):
            """Our username"""
            return self.servicephp.username
    
        def soup_request(self, url: str, allow_soft_404: bool = False):
            """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: str = None):
            """Get the record IDs for mutes.
    
        Args:
            username (str): Username to find record ID for.
                Defaults to None.
    
        Returns:
            Record (int, dict): Either the single user's mute record ID, 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):
            """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):
            """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.
                Defaults to None, return all videos.
                Note, rounded up to the nearest page.
    
        Returns:
            Videos (list): List of HTMLVideo objects.
            """
    
            # 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):
            """Get the playlists under the logged in user"""
            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):
            """Load the primary and secondary upload categories from Rumble
    
            Returns:
                categories1 (dict): The primary categories, name : numeric ID
                categories2 (dict): 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
    

    The session cookie we are logged in with

    username property

    Our username

    __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
    432
    433
    434
    435
    436
    437
    438
    439
    def __init__(self, servicephp):
        """Scraper for general information.
    
    Args:
        servicephp (ServicePHP): A ServicePHP instance, for authentication.
        """
    
        self.servicephp = servicephp
    

    get_categories()

    Load the primary and secondary upload categories from Rumble

    Returns:

    Name Type Description
    categories1 dict

    The primary categories, name : numeric ID

    categories2 dict

    The secondary categories, name : numeric ID

    Source code in cocorum/scraping.py
    596
    597
    598
    599
    600
    601
    602
    603
    604
    605
    606
    607
    608
    609
    610
    611
    612
    613
    614
    615
    616
    617
    def get_categories(self):
        """Load the primary and secondary upload categories from Rumble
    
        Returns:
            categories1 (dict): The primary categories, name : numeric ID
            categories2 (dict): 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
    522
    523
    524
    525
    526
    527
    528
    529
    530
    531
    532
    533
    534
    535
    536
    537
    538
    539
    def get_channels(self, username: str = None):
        """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.

    None

    Returns:

    Name Type Description
    Record (int, dict)

    Either the single user's mute record ID, or a dict of all username:mute record ID pairs.

    Source code in cocorum/scraping.py
    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
    502
    503
    504
    505
    506
    507
    508
    509
    510
    511
    512
    513
    514
    515
    516
    517
    518
    519
    520
    def get_muted_user_record(self, username: str = None):
        """Get the record IDs for mutes.
    
    Args:
        username (str): Username to find record ID for.
            Defaults to None.
    
    Returns:
        Record (int, dict): Either the single user's mute record ID, 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

    Source code in cocorum/scraping.py
    591
    592
    593
    594
    def get_playlists(self):
        """Get the playlists under the logged in user"""
        soup = self.soup_request(static.URI.playlists_page)
        return [HTMLPlaylist(elem, self) for elem in soup.find_all("div", attrs={"class": "playlist"})]
    

    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. Defaults to None, return all videos. Note, rounded up to the nearest page.

    None

    Returns:

    Name Type Description
    Videos list

    List of HTMLVideo objects.

    Source code in cocorum/scraping.py
    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
    def get_videos(self, username=None, is_channel=False, max_num=None):
        """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.
            Defaults to None, return all videos.
            Note, rounded up to the nearest page.
    
    Returns:
        Videos (list): List of HTMLVideo objects.
        """
    
        # 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
    451
    452
    453
    454
    455
    456
    457
    458
    459
    460
    461
    462
    463
    464
    465
    466
    467
    468
    469
    470
    471
    472
    473
    474
    def soup_request(self, url: str, allow_soft_404: bool = False):
        """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")
    

    S.D.G.