Taylor Fox Dahlin
commited on
Improvement/channel docs (#1011)
Browse files* Docstring improvements
* Documentation for the channel object
* Additional metadata available from the channel object, with unit tests
- docs/api.rst +7 -0
- docs/index.rst +1 -0
- docs/user/channel.rst +46 -0
- docs/user/playlist.rst +4 -4
- pytube/__main__.py +3 -2
- pytube/contrib/channel.py +64 -0
- pytube/contrib/playlist.py +28 -0
- tests/contrib/test_channel.py +28 -1
docs/api.rst
CHANGED
@@ -20,6 +20,13 @@ Playlist Object
|
|
20 |
:members:
|
21 |
:inherited-members:
|
22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
Stream Object
|
24 |
-------------
|
25 |
|
|
|
20 |
:members:
|
21 |
:inherited-members:
|
22 |
|
23 |
+
Channel Object
|
24 |
+
--------------
|
25 |
+
|
26 |
+
.. autoclass:: pytube.contrib.channel.Channel
|
27 |
+
:members:
|
28 |
+
:inherited-members:
|
29 |
+
|
30 |
Stream Object
|
31 |
-------------
|
32 |
|
docs/index.rst
CHANGED
@@ -58,6 +58,7 @@ of pytube.
|
|
58 |
user/streams
|
59 |
user/captions
|
60 |
user/playlist
|
|
|
61 |
user/cli
|
62 |
user/exceptions
|
63 |
|
|
|
58 |
user/streams
|
59 |
user/captions
|
60 |
user/playlist
|
61 |
+
user/channel
|
62 |
user/cli
|
63 |
user/exceptions
|
64 |
|
docs/user/channel.rst
ADDED
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.. _channel:
|
2 |
+
|
3 |
+
Using Channels
|
4 |
+
==============
|
5 |
+
|
6 |
+
This guide will walk you through the basics of working with pytube Channels.
|
7 |
+
|
8 |
+
Creating a Channel
|
9 |
+
------------------
|
10 |
+
|
11 |
+
Using pytube to interact with channels is similar to interacting with playlists.
|
12 |
+
Begin by importing the Channel class::
|
13 |
+
|
14 |
+
>>> from pytube import Channel
|
15 |
+
|
16 |
+
Now let's create a channel object. You can do this by initializing the object with a channel URL::
|
17 |
+
|
18 |
+
>>> c = Channel('https://www.youtube.com/c/ProgrammingKnowledge')
|
19 |
+
|
20 |
+
Or you can create one from a link to the channel's video page::
|
21 |
+
|
22 |
+
>>> c = Channel('https://www.youtube.com/c/ProgrammingKnowledge/videos')
|
23 |
+
|
24 |
+
Now, we have a :class:`Channel <pytube.Channel>` object called ``c`` that we can do some work with.
|
25 |
+
|
26 |
+
Interacting with a channel
|
27 |
+
--------------------------
|
28 |
+
|
29 |
+
Fundamentally, a Channel object is just a container for YouTube objects.
|
30 |
+
|
31 |
+
If, for example, we wanted to download all of the videos created by a channel, we would do the following::
|
32 |
+
|
33 |
+
>>> print(f'Downloading videos by: {c.channel_name}')
|
34 |
+
Downloading videos by: ProgrammingKnowledge
|
35 |
+
>>> for video in c.videos:
|
36 |
+
>>> video.streams.first().download()
|
37 |
+
|
38 |
+
Or, if we're only interested in the URLs for the videos, we can look at those as well::
|
39 |
+
|
40 |
+
>>> for url in c.video_urls[:3]:
|
41 |
+
>>> print(url)
|
42 |
+
['https://www.youtube.com/watch?v=tMqMU1U2MCU',
|
43 |
+
'https://www.youtube.com/watch?v=YBfInrtWq8Y',
|
44 |
+
'https://www.youtube.com/watch?v=EP9WrMw6Gzg']
|
45 |
+
|
46 |
+
And that's basically all there is to it!
|
docs/user/playlist.rst
CHANGED
@@ -39,8 +39,8 @@ Or, if we're only interested in the URLs for the videos, we can look at those as
|
|
39 |
|
40 |
>>> for url in p.video_urls[:3]:
|
41 |
>>> print(url)
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
|
46 |
-
And that's basically all there is to it!
|
|
|
39 |
|
40 |
>>> for url in p.video_urls[:3]:
|
41 |
>>> print(url)
|
42 |
+
['https://www.youtube.com/watch?v=41qgdwd3zAg',
|
43 |
+
'https://www.youtube.com/watch?v=Lbs7vmx3YwU',
|
44 |
+
'https://www.youtube.com/watch?v=YtX-Rmoea0M']
|
45 |
|
46 |
+
And that's basically all there is to it!
|
pytube/__main__.py
CHANGED
@@ -36,8 +36,6 @@ class YouTube:
|
|
36 |
|
37 |
:param str url:
|
38 |
A valid YouTube watch URL.
|
39 |
-
:param bool defer_prefetch_init:
|
40 |
-
Defers executing any network requests.
|
41 |
:param func on_progress_callback:
|
42 |
(Optional) User defined callback function for stream download
|
43 |
progress events.
|
@@ -420,6 +418,7 @@ class YouTube:
|
|
420 |
@property
|
421 |
def keywords(self) -> List[str]:
|
422 |
"""Get the video keywords.
|
|
|
423 |
:rtype: List[str]
|
424 |
"""
|
425 |
return self.player_response.get('videoDetails', {}).get('keywords', [])
|
@@ -427,6 +426,7 @@ class YouTube:
|
|
427 |
@property
|
428 |
def channel_id(self) -> str:
|
429 |
"""Get the video poster's channel id.
|
|
|
430 |
:rtype: str
|
431 |
"""
|
432 |
return self.player_response.get('videoDetails', {}).get('channelId', None)
|
@@ -434,6 +434,7 @@ class YouTube:
|
|
434 |
@property
|
435 |
def channel_url(self) -> str:
|
436 |
"""Construct the channel url for the video's poster from the channel id.
|
|
|
437 |
:rtype: str
|
438 |
"""
|
439 |
return f'https://www.youtube.com/channel/{self.channel_id}'
|
|
|
36 |
|
37 |
:param str url:
|
38 |
A valid YouTube watch URL.
|
|
|
|
|
39 |
:param func on_progress_callback:
|
40 |
(Optional) User defined callback function for stream download
|
41 |
progress events.
|
|
|
418 |
@property
|
419 |
def keywords(self) -> List[str]:
|
420 |
"""Get the video keywords.
|
421 |
+
|
422 |
:rtype: List[str]
|
423 |
"""
|
424 |
return self.player_response.get('videoDetails', {}).get('keywords', [])
|
|
|
426 |
@property
|
427 |
def channel_id(self) -> str:
|
428 |
"""Get the video poster's channel id.
|
429 |
+
|
430 |
:rtype: str
|
431 |
"""
|
432 |
return self.player_response.get('videoDetails', {}).get('channelId', None)
|
|
|
434 |
@property
|
435 |
def channel_url(self) -> str:
|
436 |
"""Construct the channel url for the video's poster from the channel id.
|
437 |
+
|
438 |
:rtype: str
|
439 |
"""
|
440 |
return f'https://www.youtube.com/channel/{self.channel_id}'
|
pytube/contrib/channel.py
CHANGED
@@ -12,6 +12,13 @@ logger = logging.getLogger(__name__)
|
|
12 |
|
13 |
class Channel(Playlist):
|
14 |
def __init__(self, url: str, proxies: Optional[Dict[str, str]] = None):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
super().__init__(url, proxies)
|
16 |
|
17 |
self.channel_uri = extract.channel_name(url)
|
@@ -19,6 +26,7 @@ class Channel(Playlist):
|
|
19 |
self.channel_url = (
|
20 |
f"https://www.youtube.com{self.channel_uri}"
|
21 |
)
|
|
|
22 |
self.videos_url = self.channel_url + '/videos'
|
23 |
self.playlists_url = self.channel_url + '/playlists'
|
24 |
self.community_url = self.channel_url + '/community'
|
@@ -31,8 +39,40 @@ class Channel(Playlist):
|
|
31 |
self._featured_channels_html = None
|
32 |
self._about_html = None
|
33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
@property
|
35 |
def html(self):
|
|
|
|
|
|
|
|
|
36 |
if self._html:
|
37 |
return self._html
|
38 |
self._html = request.get(self.videos_url)
|
@@ -40,6 +80,12 @@ class Channel(Playlist):
|
|
40 |
|
41 |
@property
|
42 |
def playlists_html(self):
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
if self._playlists_html:
|
44 |
return self._playlists_html
|
45 |
else:
|
@@ -48,6 +94,12 @@ class Channel(Playlist):
|
|
48 |
|
49 |
@property
|
50 |
def community_html(self):
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
if self._community_html:
|
52 |
return self._community_html
|
53 |
else:
|
@@ -56,6 +108,12 @@ class Channel(Playlist):
|
|
56 |
|
57 |
@property
|
58 |
def featured_channels_html(self):
|
|
|
|
|
|
|
|
|
|
|
|
|
59 |
if self._featured_channels_html:
|
60 |
return self._featured_channels_html
|
61 |
else:
|
@@ -64,6 +122,12 @@ class Channel(Playlist):
|
|
64 |
|
65 |
@property
|
66 |
def about_html(self):
|
|
|
|
|
|
|
|
|
|
|
|
|
67 |
if self._about_html:
|
68 |
return self._about_html
|
69 |
else:
|
|
|
12 |
|
13 |
class Channel(Playlist):
|
14 |
def __init__(self, url: str, proxies: Optional[Dict[str, str]] = None):
|
15 |
+
"""Construct a :class:`Channel <Channel>`.
|
16 |
+
|
17 |
+
:param str url:
|
18 |
+
A valid YouTube channel URL.
|
19 |
+
:param proxies:
|
20 |
+
(Optional) A dictionary of proxies to use for web requests.
|
21 |
+
"""
|
22 |
super().__init__(url, proxies)
|
23 |
|
24 |
self.channel_uri = extract.channel_name(url)
|
|
|
26 |
self.channel_url = (
|
27 |
f"https://www.youtube.com{self.channel_uri}"
|
28 |
)
|
29 |
+
|
30 |
self.videos_url = self.channel_url + '/videos'
|
31 |
self.playlists_url = self.channel_url + '/playlists'
|
32 |
self.community_url = self.channel_url + '/community'
|
|
|
39 |
self._featured_channels_html = None
|
40 |
self._about_html = None
|
41 |
|
42 |
+
@property
|
43 |
+
def channel_name(self):
|
44 |
+
"""Get the name of the YouTube channel.
|
45 |
+
|
46 |
+
:rtype: str
|
47 |
+
"""
|
48 |
+
return self.initial_data['metadata']['channelMetadataRenderer']['title']
|
49 |
+
|
50 |
+
@property
|
51 |
+
def channel_id(self):
|
52 |
+
"""Get the ID of the YouTube channel.
|
53 |
+
|
54 |
+
This will return the underlying ID, not the vanity URL.
|
55 |
+
|
56 |
+
:rtype: str
|
57 |
+
"""
|
58 |
+
return self.initial_data['metadata']['channelMetadataRenderer']['externalId']
|
59 |
+
|
60 |
+
@property
|
61 |
+
def vanity_url(self):
|
62 |
+
"""Get the vanity URL of the YouTube channel.
|
63 |
+
|
64 |
+
Returns None if it doesn't exist.
|
65 |
+
|
66 |
+
:rtype: str
|
67 |
+
"""
|
68 |
+
return self.initial_data['metadata']['channelMetadataRenderer'].get('vanityChannelUrl', None) # noqa:E501
|
69 |
+
|
70 |
@property
|
71 |
def html(self):
|
72 |
+
"""Get the html for the /videos page.
|
73 |
+
|
74 |
+
:rtype: str
|
75 |
+
"""
|
76 |
if self._html:
|
77 |
return self._html
|
78 |
self._html = request.get(self.videos_url)
|
|
|
80 |
|
81 |
@property
|
82 |
def playlists_html(self):
|
83 |
+
"""Get the html for the /playlists page.
|
84 |
+
|
85 |
+
Currently unused for any functionality.
|
86 |
+
|
87 |
+
:rtype: str
|
88 |
+
"""
|
89 |
if self._playlists_html:
|
90 |
return self._playlists_html
|
91 |
else:
|
|
|
94 |
|
95 |
@property
|
96 |
def community_html(self):
|
97 |
+
"""Get the html for the /community page.
|
98 |
+
|
99 |
+
Currently unused for any functionality.
|
100 |
+
|
101 |
+
:rtype: str
|
102 |
+
"""
|
103 |
if self._community_html:
|
104 |
return self._community_html
|
105 |
else:
|
|
|
108 |
|
109 |
@property
|
110 |
def featured_channels_html(self):
|
111 |
+
"""Get the html for the /channels page.
|
112 |
+
|
113 |
+
Currently unused for any functionality.
|
114 |
+
|
115 |
+
:rtype: str
|
116 |
+
"""
|
117 |
if self._featured_channels_html:
|
118 |
return self._featured_channels_html
|
119 |
else:
|
|
|
122 |
|
123 |
@property
|
124 |
def about_html(self):
|
125 |
+
"""Get the html for the /about page.
|
126 |
+
|
127 |
+
Currently unused for any functionality.
|
128 |
+
|
129 |
+
:rtype: str
|
130 |
+
"""
|
131 |
if self._about_html:
|
132 |
return self._about_html
|
133 |
else:
|
pytube/contrib/playlist.py
CHANGED
@@ -30,6 +30,10 @@ class Playlist(Sequence):
|
|
30 |
|
31 |
@property
|
32 |
def playlist_id(self):
|
|
|
|
|
|
|
|
|
33 |
if self._playlist_id:
|
34 |
return self._playlist_id
|
35 |
self._playlist_id = extract.playlist_id(self._input_url)
|
@@ -37,10 +41,18 @@ class Playlist(Sequence):
|
|
37 |
|
38 |
@property
|
39 |
def playlist_url(self):
|
|
|
|
|
|
|
|
|
40 |
return f"https://www.youtube.com/playlist?list={self.playlist_id}"
|
41 |
|
42 |
@property
|
43 |
def html(self):
|
|
|
|
|
|
|
|
|
44 |
if self._html:
|
45 |
return self._html
|
46 |
self._html = request.get(self.playlist_url)
|
@@ -48,6 +60,10 @@ class Playlist(Sequence):
|
|
48 |
|
49 |
@property
|
50 |
def ytcfg(self):
|
|
|
|
|
|
|
|
|
51 |
if self._ytcfg:
|
52 |
return self._ytcfg
|
53 |
self._ytcfg = extract.get_ytcfg(self.html)
|
@@ -55,6 +71,10 @@ class Playlist(Sequence):
|
|
55 |
|
56 |
@property
|
57 |
def initial_data(self):
|
|
|
|
|
|
|
|
|
58 |
if self._initial_data:
|
59 |
return self._initial_data
|
60 |
else:
|
@@ -63,6 +83,10 @@ class Playlist(Sequence):
|
|
63 |
|
64 |
@property
|
65 |
def sidebar_info(self):
|
|
|
|
|
|
|
|
|
66 |
if self._sidebar_info:
|
67 |
return self._sidebar_info
|
68 |
else:
|
@@ -72,6 +96,10 @@ class Playlist(Sequence):
|
|
72 |
|
73 |
@property
|
74 |
def yt_api_key(self):
|
|
|
|
|
|
|
|
|
75 |
return self.ytcfg['INNERTUBE_API_KEY']
|
76 |
|
77 |
def _paginate(
|
|
|
30 |
|
31 |
@property
|
32 |
def playlist_id(self):
|
33 |
+
"""Get the playlist id.
|
34 |
+
|
35 |
+
:rtype: str
|
36 |
+
"""
|
37 |
if self._playlist_id:
|
38 |
return self._playlist_id
|
39 |
self._playlist_id = extract.playlist_id(self._input_url)
|
|
|
41 |
|
42 |
@property
|
43 |
def playlist_url(self):
|
44 |
+
"""Get the base playlist url.
|
45 |
+
|
46 |
+
:rtype: str
|
47 |
+
"""
|
48 |
return f"https://www.youtube.com/playlist?list={self.playlist_id}"
|
49 |
|
50 |
@property
|
51 |
def html(self):
|
52 |
+
"""Get the playlist page html.
|
53 |
+
|
54 |
+
:rtype: str
|
55 |
+
"""
|
56 |
if self._html:
|
57 |
return self._html
|
58 |
self._html = request.get(self.playlist_url)
|
|
|
60 |
|
61 |
@property
|
62 |
def ytcfg(self):
|
63 |
+
"""Extract the ytcfg from the playlist page html.
|
64 |
+
|
65 |
+
:rtype: dict
|
66 |
+
"""
|
67 |
if self._ytcfg:
|
68 |
return self._ytcfg
|
69 |
self._ytcfg = extract.get_ytcfg(self.html)
|
|
|
71 |
|
72 |
@property
|
73 |
def initial_data(self):
|
74 |
+
"""Extract the initial data from the playlist page html.
|
75 |
+
|
76 |
+
:rtype: dict
|
77 |
+
"""
|
78 |
if self._initial_data:
|
79 |
return self._initial_data
|
80 |
else:
|
|
|
83 |
|
84 |
@property
|
85 |
def sidebar_info(self):
|
86 |
+
"""Extract the sidebar info from the playlist page html.
|
87 |
+
|
88 |
+
:rtype: dict
|
89 |
+
"""
|
90 |
if self._sidebar_info:
|
91 |
return self._sidebar_info
|
92 |
else:
|
|
|
96 |
|
97 |
@property
|
98 |
def yt_api_key(self):
|
99 |
+
"""Extract the INNERTUBE_API_KEY from the playlist ytcfg.
|
100 |
+
|
101 |
+
:rtype: str
|
102 |
+
"""
|
103 |
return self.ytcfg['INNERTUBE_API_KEY']
|
104 |
|
105 |
def _paginate(
|
tests/contrib/test_channel.py
CHANGED
@@ -16,12 +16,39 @@ def test_init_with_url(request_get, channel_videos_html):
|
|
16 |
|
17 |
|
18 |
@mock.patch('pytube.request.get')
|
19 |
-
def
|
20 |
request_get.return_value = channel_videos_html
|
21 |
|
22 |
c = Channel('https://www.youtube.com/c/ProgrammingKnowledge/videos')
|
23 |
assert c.channel_uri == '/c/ProgrammingKnowledge'
|
24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
|
26 |
@mock.patch('pytube.request.get')
|
27 |
def test_channel_video_list(request_get, channel_videos_html):
|
|
|
16 |
|
17 |
|
18 |
@mock.patch('pytube.request.get')
|
19 |
+
def test_channel_uri(request_get, channel_videos_html):
|
20 |
request_get.return_value = channel_videos_html
|
21 |
|
22 |
c = Channel('https://www.youtube.com/c/ProgrammingKnowledge/videos')
|
23 |
assert c.channel_uri == '/c/ProgrammingKnowledge'
|
24 |
|
25 |
+
c = Channel('https://www.youtube.com/channel/UCs6nmQViDpUw0nuIx9c_WvA/videos')
|
26 |
+
assert c.channel_uri == '/channel/UCs6nmQViDpUw0nuIx9c_WvA'
|
27 |
+
|
28 |
+
|
29 |
+
@mock.patch('pytube.request.get')
|
30 |
+
def test_channel_name(request_get, channel_videos_html):
|
31 |
+
request_get.return_value = channel_videos_html
|
32 |
+
|
33 |
+
c = Channel('https://www.youtube.com/c/ProgrammingKnowledge/videos')
|
34 |
+
assert c.channel_name == 'ProgrammingKnowledge'
|
35 |
+
|
36 |
+
|
37 |
+
@mock.patch('pytube.request.get')
|
38 |
+
def test_channel_id(request_get, channel_videos_html):
|
39 |
+
request_get.return_value = channel_videos_html
|
40 |
+
|
41 |
+
c = Channel('https://www.youtube.com/c/ProgrammingKnowledge/videos')
|
42 |
+
assert c.channel_id == 'UCs6nmQViDpUw0nuIx9c_WvA'
|
43 |
+
|
44 |
+
|
45 |
+
@mock.patch('pytube.request.get')
|
46 |
+
def test_channel_vanity_url(request_get, channel_videos_html):
|
47 |
+
request_get.return_value = channel_videos_html
|
48 |
+
|
49 |
+
c = Channel('https://www.youtube.com/c/ProgrammingKnowledge/videos')
|
50 |
+
assert c.vanity_url == 'http://www.youtube.com/c/ProgrammingKnowledge'
|
51 |
+
|
52 |
|
53 |
@mock.patch('pytube.request.get')
|
54 |
def test_channel_video_list(request_get, channel_videos_html):
|