Some tweaks
This commit is contained in:
parent
d4ab82c07e
commit
8f8ffe3e7b
7 changed files with 164 additions and 87 deletions
|
@ -27,12 +27,17 @@ audio_stream = video.get_streams('audio')[0]
|
||||||
print(f"Audio stream URL: {audio_stream.url} ({audio_stream.mime_type})")
|
print(f"Audio stream URL: {audio_stream.url} ({audio_stream.mime_type})")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can find more examples in the [`tests`](https://github.com/CWKevo/python-piped-api-client/tree/master/tests) folder.
|
||||||
|
|
||||||
## Why?
|
## Why?
|
||||||
|
|
||||||
|
<!-- Soon... maybe.
|
||||||
|
|
||||||
This package has allowed me to start creating my open-source project, [ArchiveTube](https://github.com/CWKevo/ArchiveTube) - a scrapper and archive for YouTube content (videos and comments) - to preserve them and make them available to anyone, with ability to search for comments and videos. View hall of fame (most liked comments and videos), bring back dislikes via [ReturnYouTubeDislike.com](https://returnyoutubedislike.com), view deleted content and much more!
|
This package has allowed me to start creating my open-source project, [ArchiveTube](https://github.com/CWKevo/ArchiveTube) - a scrapper and archive for YouTube content (videos and comments) - to preserve them and make them available to anyone, with ability to search for comments and videos. View hall of fame (most liked comments and videos), bring back dislikes via [ReturnYouTubeDislike.com](https://returnyoutubedislike.com), view deleted content and much more!
|
||||||
Google has showed us that they make YouTube own us by harvesting our data. This is also followed by non-throught out decisions, which their users aren't happy with. Let's do it the other way around this time by reclaiming our content and entertainment back & make YouTube great again!
|
Google has showed us that they make YouTube own us by harvesting our data. This is also followed by non-throught out decisions, which their users aren't happy with. Let's do it the other way around this time by reclaiming our content and entertainment back & make YouTube great again!
|
||||||
|
-->
|
||||||
|
|
||||||
The creation of this package was also primarily fueled by the same type of motivation [Piped has](https://piped-docs.kavin.rocks/docs/why/).
|
The creation of this package was primarily fueled by the same type of motivation [Piped has](https://piped-docs.kavin.rocks/docs/why/).
|
||||||
|
|
||||||
Google's API is not very easy-to-use - you must obtain some JSON thingy to use it, and it is very low-level and not very user-friendly.
|
Google's API is not very easy-to-use - you must obtain some JSON thingy to use it, and it is very low-level and not very user-friendly.
|
||||||
On the other hand, this package accessed the [Piped API](https://piped.kavin.rocks/), which has a much more high-level API and doesn't need an account or API keys.
|
On the other hand, this package accessed the [Piped API](https://piped.kavin.rocks/), which has a much more high-level API and doesn't need an account or API keys.
|
||||||
|
|
|
@ -5,7 +5,7 @@ from requests import Session
|
||||||
from .models import BasePipedModel
|
from .models import BasePipedModel
|
||||||
from .models.comments import Comments
|
from .models.comments import Comments
|
||||||
from .models.videos import Video
|
from .models.videos import Video
|
||||||
from .models.channels import Channel
|
from .models.channels import NextPageChannel, Channel
|
||||||
|
|
||||||
|
|
||||||
_MDL = t.TypeVar('_MDL', bound=t.Type[BasePipedModel])
|
_MDL = t.TypeVar('_MDL', bound=t.Type[BasePipedModel])
|
||||||
|
@ -35,7 +35,7 @@ class PipedClient:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _get_json(self, uri: str, as_model: t.Optional[_MDL]=None, **kwargs) -> t.Union[_MDL, t.Dict[str, t.Any]]:
|
def _get_json(self, uri: str, as_model: t.Optional[_MDL]=None, **kwargs) -> t.Union[_MDL, t.Dict[str, t.Any], t.List[t.Any]]:
|
||||||
"""
|
"""
|
||||||
Obtains JSON data from specific URI of the Piped API.
|
Obtains JSON data from specific URI of the Piped API.
|
||||||
|
|
||||||
|
@ -56,6 +56,19 @@ class PipedClient:
|
||||||
return json
|
return json
|
||||||
|
|
||||||
|
|
||||||
|
def get_video(self, video_id: str, **kwargs) -> Video:
|
||||||
|
"""
|
||||||
|
Gets information about a specific video.
|
||||||
|
|
||||||
|
### Parameters:
|
||||||
|
- `video_id` - The ID of the video to get information for
|
||||||
|
- `**kwargs` - Additional keyword arguments to pass to `requests.Session.get`
|
||||||
|
|
||||||
|
[Piped Documentation](https://piped-docs.kavin.rocks/docs/api-documentation/#streamsvideoid)
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self._get_json(f"/streams/{video_id}", Video, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def get_comments(self, video_id: str, nextpage: t.Optional[t.Dict[str, t.Optional[str]]]=None, **kwargs) -> Comments:
|
def get_comments(self, video_id: str, nextpage: t.Optional[t.Dict[str, t.Optional[str]]]=None, **kwargs) -> Comments:
|
||||||
"""
|
"""
|
||||||
|
@ -66,29 +79,15 @@ class PipedClient:
|
||||||
- `nextpage` - Nextpage data, obtained from `.models.comments.Comments.nextpage` property. If this is `None`, the first page of comments is returned.
|
- `nextpage` - Nextpage data, obtained from `.models.comments.Comments.nextpage` property. If this is `None`, the first page of comments is returned.
|
||||||
There are often 20 comments per page.
|
There are often 20 comments per page.
|
||||||
- `**kwargs` - Additional keyword arguments to pass to `requests.Session.get`
|
- `**kwargs` - Additional keyword arguments to pass to `requests.Session.get`
|
||||||
"""
|
|
||||||
|
|
||||||
kw = kwargs.copy()
|
[Piped Documentation](https://piped-docs.kavin.rocks/docs/api-documentation/#commentsvideoid)
|
||||||
|
"""
|
||||||
|
|
||||||
if nextpage is not None:
|
if nextpage is not None:
|
||||||
kw.update({'params': {'nextpage': nextpage}})
|
kwargs.update({'params': {'nextpage': nextpage}})
|
||||||
return self._get_json(f"/nextpage/comments/{video_id}", Comments, **kw)
|
return self._get_json(f"/nextpage/comments/{video_id}", Comments, **kwargs)
|
||||||
|
|
||||||
else:
|
return self._get_json(f"/comments/{video_id}", Comments, **kwargs)
|
||||||
return self._get_json(f"/comments/{video_id}", Comments, **kw)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_video(self, video_id: str, **kwargs) -> Video:
|
|
||||||
"""
|
|
||||||
Gets information about a specific video.
|
|
||||||
|
|
||||||
### Parameters:
|
|
||||||
- `video_id` - The ID of the video to get information for
|
|
||||||
- `**kwargs` - Additional keyword arguments to pass to `requests.Session.get`
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self._get_json(f"/streams/{video_id}", Video, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def get_trending(self, country_code: str='US', **kwargs) -> t.List[Video.RelatedStream]:
|
def get_trending(self, country_code: str='US', **kwargs) -> t.List[Video.RelatedStream]:
|
||||||
|
@ -100,23 +99,30 @@ class PipedClient:
|
||||||
- `country_code` - The country code ([ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements)) to get trending videos for. This is automatically capitalized by this package,
|
- `country_code` - The country code ([ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements)) to get trending videos for. This is automatically capitalized by this package,
|
||||||
since Piped for some reason doesn't accept lowercase country codes. Note: countries such as China or North Korea don't have trending videos, so they will always return an empty list.
|
since Piped for some reason doesn't accept lowercase country codes. Note: countries such as China or North Korea don't have trending videos, so they will always return an empty list.
|
||||||
- `**kwargs` - Additional keyword arguments to pass to `requests.Session.get`
|
- `**kwargs` - Additional keyword arguments to pass to `requests.Session.get`
|
||||||
|
|
||||||
|
[Piped Documentation](https://piped-docs.kavin.rocks/docs/api-documentation/#trending)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
kw = kwargs.copy()
|
kwargs.update({'params': {'region': country_code.upper()}})
|
||||||
kw.update({'params': {'region': country_code.upper()}})
|
|
||||||
|
|
||||||
return [Video.RelatedStream(trending_video) for trending_video in self._get_json(f"/trending", **kw)]
|
return [Video.RelatedStream(trending_video) for trending_video in self._get_json(f"/trending", **kwargs)]
|
||||||
|
|
||||||
|
|
||||||
def get_channel_by_id(self, channel_id: str, **kwargs) -> Channel:
|
def get_channel_by_id(self, channel_id: str, nextpage: t.Optional[t.Dict[str, t.Optional[str]]]=None, **kwargs) -> t.Union[NextPageChannel, Channel]:
|
||||||
"""
|
"""
|
||||||
Gets information about a specific channel by its ID.
|
Gets information about a specific channel by its ID.
|
||||||
|
|
||||||
### Parameters:
|
### Parameters:
|
||||||
- `channel_id` - The ID of the channel to get information for
|
- `channel_id` - The ID of the channel to get information for
|
||||||
- `**kwargs` - Additional keyword arguments to pass to `requests.Session.get`
|
- `**kwargs` - Additional keyword arguments to pass to `requests.Session.get`
|
||||||
|
|
||||||
|
[Piped Documentation](https://piped-docs.kavin.rocks/docs/api-documentation/#channelchannelid)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if nextpage is not None:
|
||||||
|
kwargs.update({'params': {'nextpage': nextpage}})
|
||||||
|
return self._get_json(f"/nextpage/channel/{channel_id}", NextPageChannel, **kwargs)
|
||||||
|
|
||||||
return self._get_json(f"/channel/{channel_id}", Channel, **kwargs)
|
return self._get_json(f"/channel/{channel_id}", Channel, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -128,6 +134,24 @@ class PipedClient:
|
||||||
### Parameters:
|
### Parameters:
|
||||||
- `channel_name` - The name of the channel to get information for
|
- `channel_name` - The name of the channel to get information for
|
||||||
- `**kwargs` - Additional keyword arguments to pass to `requests.Session.get`
|
- `**kwargs` - Additional keyword arguments to pass to `requests.Session.get`
|
||||||
|
|
||||||
|
[Piped Documentation](https://piped-docs.kavin.rocks/docs/api-documentation/#cname)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self._get_json(f"/c/{channel_name}", Channel, **kwargs)
|
return self._get_json(f"/c/{channel_name}", Channel, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def get_search_suggestions(self, search_query: str, **kwargs) -> t.List[str]:
|
||||||
|
"""
|
||||||
|
Obtains search suggestions for a query.
|
||||||
|
|
||||||
|
### Parameters:
|
||||||
|
- `search_query` - The query to get search suggestions for
|
||||||
|
- `**kwargs` - Additional keyword arguments to pass to `requests.Session.get`
|
||||||
|
|
||||||
|
[Piped Documentation](https://piped-docs.kavin.rocks/docs/api-documentation/#suggestions)
|
||||||
|
"""
|
||||||
|
|
||||||
|
kwargs.update({'params': {'query': search_query}})
|
||||||
|
|
||||||
|
return self._get_json(f"/suggestions", **kwargs)
|
||||||
|
|
|
@ -5,9 +5,40 @@ from .videos import Video
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Channel(BasePipedModel):
|
class NextPageChannel(BasePipedModel):
|
||||||
"""
|
"""
|
||||||
Represents a YouTube channel
|
Represents a channel obtained via the `nextpage` endpoint.
|
||||||
|
|
||||||
|
This model contains only `nextpage` and `relatedStreams`. It's a parent for `Channel`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nextpage(self) -> str:
|
||||||
|
"""
|
||||||
|
A JSON encoded string to be passed to the `'nextpage'` endpoint(s) when
|
||||||
|
obtaining paginated data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.data['nextpage']
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uploaded_videos(self) -> t.List[Video.RelatedStream]:
|
||||||
|
"""
|
||||||
|
List of uploaded videos from the current fetched data
|
||||||
|
|
||||||
|
There are max. 30 videos per page
|
||||||
|
"""
|
||||||
|
|
||||||
|
return [Video.RelatedStream(video_data) for video_data in self.data['relatedStreams']]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Channel(NextPageChannel):
|
||||||
|
"""
|
||||||
|
Represents a YouTube channel.
|
||||||
|
|
||||||
|
Contains properties of `NextPageChannel`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -55,16 +86,6 @@ class Channel(BasePipedModel):
|
||||||
return self.data['description']
|
return self.data['description']
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def nextpage(self) -> str:
|
|
||||||
"""
|
|
||||||
A JSON encoded string to be passed to the `'nextpage'` endpoint(s) when
|
|
||||||
obtaining paginated data.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['nextpage']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def subscriber_count(self) -> int:
|
def subscriber_count(self) -> int:
|
||||||
"""
|
"""
|
||||||
|
@ -81,12 +102,3 @@ class Channel(BasePipedModel):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self.data['verified']
|
return self.data['verified']
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def uploaded_videos(self) -> t.List[Video.RelatedStream]:
|
|
||||||
"""
|
|
||||||
List of uploaded videos from the current fetched data
|
|
||||||
"""
|
|
||||||
|
|
||||||
return [Video.RelatedStream(video_data) for video_data in self.data['relatedVideos']]
|
|
||||||
|
|
|
@ -432,7 +432,7 @@ class Video(BasePipedModel):
|
||||||
List of related streams
|
List of related streams
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return [self.RelatedStream(video_data) for video_data in self.data['relatedVideos']]
|
return [self.RelatedStream(video_data) for video_data in self.data['relatedStreams']]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
from tests import CLIENT
|
|
||||||
|
|
||||||
|
|
||||||
def test_channel_by_id(channel_id: str='UCuAXFkgsw1L7xaCfnd5JJOw') -> None:
|
|
||||||
"""
|
|
||||||
Prints out information about a channel by its ID.
|
|
||||||
"""
|
|
||||||
|
|
||||||
channel = CLIENT.get_channel_by_id(channel_id)
|
|
||||||
assert channel.id == channel_id
|
|
||||||
|
|
||||||
print(f"""
|
|
||||||
Channel ID: {channel_id}
|
|
||||||
Name: {channel.name}
|
|
||||||
Description: {channel.description}
|
|
||||||
Subscriber count: {channel.subscriber_count}
|
|
||||||
""")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_channel_by_name(channel_name: str='SusanWojcicki') -> None:
|
|
||||||
"""
|
|
||||||
Prints out information about a channel by its ID.
|
|
||||||
"""
|
|
||||||
|
|
||||||
channel = CLIENT.get_channel_by_name(channel_name)
|
|
||||||
|
|
||||||
print(f"""
|
|
||||||
Channel ID: {channel.id}
|
|
||||||
Name: {channel.name}
|
|
||||||
Description: {channel.description}
|
|
||||||
Subscriber count: {channel.subscriber_count}
|
|
||||||
""")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
test_channel_by_id()
|
|
||||||
test_channel_by_name()
|
|
58
tests/test_channels.py
Normal file
58
tests/test_channels.py
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
from tests import CLIENT
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from random import choice
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_by_id(channel_id: str='UCuAXFkgsw1L7xaCfnd5JJOw') -> None:
|
||||||
|
"""
|
||||||
|
Prints out information about a channel by its ID.
|
||||||
|
"""
|
||||||
|
|
||||||
|
channel = CLIENT.get_channel_by_id(channel_id)
|
||||||
|
|
||||||
|
video_id = channel.uploaded_videos[0].url.removeprefix('/watch?v=')
|
||||||
|
likes = CLIENT.get_video(video_id).likes
|
||||||
|
|
||||||
|
print(f"Total likes for last video of {channel.name}: {likes}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_by_name(channel_name: str='SusanWojcicki') -> None:
|
||||||
|
"""
|
||||||
|
Prints out information about a channel by its ID.
|
||||||
|
"""
|
||||||
|
|
||||||
|
channel = CLIENT.get_channel_by_name(channel_name)
|
||||||
|
|
||||||
|
print(f"""
|
||||||
|
Channel ID: {channel.id}
|
||||||
|
Name: {channel.name}
|
||||||
|
Description: {channel.description}
|
||||||
|
Subscriber count: {channel.subscriber_count}
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_watchtime_trending() -> None:
|
||||||
|
"""
|
||||||
|
Prints out the total watchtime for recent videos of a random trending channel
|
||||||
|
"""
|
||||||
|
|
||||||
|
trending_channel_id = choice(CLIENT.get_trending('SK')).uploader_url.removeprefix('/channel/')
|
||||||
|
trending_channel = CLIENT.get_channel_by_id(trending_channel_id)
|
||||||
|
|
||||||
|
|
||||||
|
total_watchtime = timedelta(milliseconds=0)
|
||||||
|
|
||||||
|
for video in trending_channel.uploaded_videos:
|
||||||
|
total_watchtime += video.duration
|
||||||
|
|
||||||
|
print(f"Total watchtime for recent {len(trending_channel.uploaded_videos)} videos of {trending_channel.name} (https://youtube.com/channel/{trending_channel.id}): {total_watchtime}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
test_channel_by_name()
|
||||||
|
test_channel_by_id()
|
||||||
|
test_get_watchtime_trending()
|
17
tests/test_suggestions.py
Normal file
17
tests/test_suggestions.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
from tests import CLIENT
|
||||||
|
|
||||||
|
|
||||||
|
def test_suggestions(search_query: str='Susan') -> None:
|
||||||
|
"""
|
||||||
|
Obtains search suggestions for a query.
|
||||||
|
"""
|
||||||
|
|
||||||
|
suggestions = CLIENT.get_search_suggestions(search_query)
|
||||||
|
|
||||||
|
assert len(suggestions) > 0
|
||||||
|
print(suggestions)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
test_suggestions()
|
Loading…
Reference in a new issue