Compare commits
No commits in common. "master" and "documentation" have entirely different histories.
master
...
documentat
30 changed files with 6089 additions and 1469 deletions
38
.github/workflows/build_documentation.yml
vendored
38
.github/workflows/build_documentation.yml
vendored
|
@ -1,38 +0,0 @@
|
||||||
name: Build documentation
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
python-version: ['3.10']
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install pdoc
|
|
||||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
|
||||||
|
|
||||||
- name: Build docs
|
|
||||||
run: |
|
|
||||||
chmod +x ./scripts/build_documentation.sh
|
|
||||||
bash ./scripts/build_documentation.sh
|
|
||||||
|
|
||||||
- name: Deploy
|
|
||||||
uses: JamesIves/github-pages-deploy-action@4.1.4
|
|
||||||
with:
|
|
||||||
branch: documentation
|
|
||||||
folder: ./documentation
|
|
37
.github/workflows/publish_to_pypi.yml
vendored
37
.github/workflows/publish_to_pypi.yml
vendored
|
@ -1,37 +0,0 @@
|
||||||
name: Upload Python Package to PyPI
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
python-version: ['3.10']
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
python -m pip install pytest
|
|
||||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
|
||||||
|
|
||||||
- name: Build package
|
|
||||||
run: |
|
|
||||||
chmod +x ./scripts/compile_for_pypi.sh
|
|
||||||
./scripts/compile_for_pypi.sh
|
|
||||||
|
|
||||||
- name: Publish package
|
|
||||||
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
|
||||||
with:
|
|
||||||
user: __token__
|
|
||||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
28
.github/workflows/pytest.yml
vendored
28
.github/workflows/pytest.yml
vendored
|
@ -1,28 +0,0 @@
|
||||||
name: Test with pyTest
|
|
||||||
|
|
||||||
on: [workflow_dispatch, push, pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
python-version: ['3.10']
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
python -m pip install pytest
|
|
||||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
|
||||||
|
|
||||||
- name: Test with pyTest
|
|
||||||
run: |
|
|
||||||
python -m pytest -v
|
|
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -1,9 +0,0 @@
|
||||||
.vscode/
|
|
||||||
__pycache__/
|
|
||||||
env/
|
|
||||||
|
|
||||||
.pytest-cache/
|
|
||||||
documentation/
|
|
||||||
|
|
||||||
dist/*
|
|
||||||
*.egg-info
|
|
21
LICENSE
21
LICENSE
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2021 SKevo
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
47
README.md
47
README.md
|
@ -1,47 +0,0 @@
|
||||||
# Piped API client (Python)
|
|
||||||
|
|
||||||
A Python API wrapper for [Piped](https://piped-docs.kavin.rocks/). This can essentially be used as an alternative way to access YouTube's API, without needing to use an API key. Forked to allow for search queries to be made.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install git+https://gitdab.com/cere/python-piped-api-client
|
|
||||||
```
|
|
||||||
|
|
||||||
## Quickstart
|
|
||||||
|
|
||||||
Getting started is very easy:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from piped_api import PipedClient
|
|
||||||
|
|
||||||
CLIENT = PipedClient()
|
|
||||||
|
|
||||||
|
|
||||||
# Print out the first audio stream URL for a video:
|
|
||||||
video = CLIENT.get_video(video_id)
|
|
||||||
audio_stream = video.get_streams('audio')[0]
|
|
||||||
|
|
||||||
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?
|
|
||||||
|
|
||||||
<!-- 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!
|
|
||||||
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 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.
|
|
||||||
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.
|
|
||||||
|
|
||||||
It is not meant to be a replacement for the official YouTube API, but it can help you to cut the strings that Google attaches to you when using their API.
|
|
||||||
|
|
||||||
## Useful links
|
|
||||||
|
|
||||||
- [Piped's official API documentation](https://piped-docs.kavin.rocks/docs/api-documentation/)
|
|
7
index.html
Normal file
7
index.html
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="refresh" content="0; url=./piped_api.html"/>
|
||||||
|
</head>
|
||||||
|
</html>
|
379
piped_api.html
Normal file
379
piped_api.html
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,25 +0,0 @@
|
||||||
import typing as t
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from .client import PipedClient
|
|
||||||
from .models.comments import Comments
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# For pdoc:
|
|
||||||
README_PATH = Path(__file__).parent.parent.absolute() / Path('README.md')
|
|
||||||
try:
|
|
||||||
with open(README_PATH, 'r', encoding="UTF-8") as readme:
|
|
||||||
__readme__ = readme.read()
|
|
||||||
|
|
||||||
except:
|
|
||||||
__readme__ = "Failed to read README.md!"
|
|
||||||
|
|
||||||
__doc__ = __readme__
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Supress unused-import warnings:
|
|
||||||
if t.TYPE_CHECKING:
|
|
||||||
_ = [PipedClient, Comments]
|
|
938
piped_api/client.html
Normal file
938
piped_api/client.html
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,170 +0,0 @@
|
||||||
import typing as t
|
|
||||||
|
|
||||||
from requests import Session
|
|
||||||
|
|
||||||
from .models import BasePipedModel
|
|
||||||
from .models.comments import Comments
|
|
||||||
from .models.videos import Video
|
|
||||||
from .models.channels import NextPageChannel, Channel
|
|
||||||
|
|
||||||
|
|
||||||
_MDL = t.TypeVar('_MDL', bound=t.Type[BasePipedModel])
|
|
||||||
|
|
||||||
|
|
||||||
class APIError(Exception): """Raised when an API call fails"""
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class PipedClient:
|
|
||||||
"""
|
|
||||||
An API client for [Piped](https://piped.kavin.rocks).
|
|
||||||
|
|
||||||
See also [Piped API docs](https://piped-docs.kavin.rocks/docs)
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, base_api_url: str='https://pipedapi.kavin.rocks', session: t.Type[Session]=Session()) -> None:
|
|
||||||
"""
|
|
||||||
### Parameters:
|
|
||||||
- `base_api_url` - The base URL to the instance's API. Trailing slashes will be stripped.
|
|
||||||
- `session` - A class/subclass of `requests.Session` to use for all requests.
|
|
||||||
For example, you could use [requests-cache](https://pypi.org/project/requests-cache/) to make all requests cacheable.
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.base_api_url = base_api_url.strip("/")
|
|
||||||
self.session = session
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
### Parameters:
|
|
||||||
- `uri` - The URI to get JSON data from
|
|
||||||
- `as_model` - The `BasePipedModel` to load the JSON data into. If this is `None`, the JSON data is returned as a `dict`.
|
|
||||||
- `**kwargs` - Additional keyword arguments to pass to `requests.Session.get`
|
|
||||||
"""
|
|
||||||
|
|
||||||
json: t.Union[dict, list] = self.session.get(f"{self.base_api_url}{uri}", **kwargs).json()
|
|
||||||
|
|
||||||
if isinstance(json, dict) and json.get('error', None) is not None:
|
|
||||||
raise APIError(f"Error: {json['error']}")
|
|
||||||
|
|
||||||
if as_model is not None:
|
|
||||||
return as_model(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:
|
|
||||||
"""
|
|
||||||
Gets a list of comments for a specific video.
|
|
||||||
|
|
||||||
### Parameters:
|
|
||||||
- `video_id` - The ID of the video to get comments for
|
|
||||||
- `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.
|
|
||||||
- `**kwargs` - Additional keyword arguments to pass to `requests.Session.get`
|
|
||||||
|
|
||||||
[Piped Documentation](https://piped-docs.kavin.rocks/docs/api-documentation/#commentsvideoid)
|
|
||||||
"""
|
|
||||||
|
|
||||||
if nextpage is not None:
|
|
||||||
kwargs.update({'params': {'nextpage': nextpage}})
|
|
||||||
return self._get_json(f"/nextpage/comments/{video_id}", Comments, **kwargs)
|
|
||||||
|
|
||||||
return self._get_json(f"/comments/{video_id}", Comments, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def get_trending(self, country_code: str='US', **kwargs) -> t.List[Video.RelatedStream]:
|
|
||||||
"""
|
|
||||||
Obtains trending videos for a specific country. If there are no trending videos (or `country_code` is invalid),
|
|
||||||
an empty list is returned.
|
|
||||||
|
|
||||||
### Parameters:
|
|
||||||
- `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.
|
|
||||||
- `**kwargs` - Additional keyword arguments to pass to `requests.Session.get`
|
|
||||||
|
|
||||||
[Piped Documentation](https://piped-docs.kavin.rocks/docs/api-documentation/#trending)
|
|
||||||
"""
|
|
||||||
|
|
||||||
kwargs.update({'params': {'region': country_code.upper()}})
|
|
||||||
|
|
||||||
return [Video.RelatedStream(trending_video) for trending_video in self._get_json(f"/trending", **kwargs)]
|
|
||||||
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
### Parameters:
|
|
||||||
- `channel_id` - The ID of the channel to get information for
|
|
||||||
- `**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)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_channel_by_name(self, channel_name: str, **kwargs) -> Channel:
|
|
||||||
"""
|
|
||||||
Gets information about a specific channel by its name.
|
|
||||||
|
|
||||||
### Parameters:
|
|
||||||
- `channel_name` - The name of the channel to get information for
|
|
||||||
- `**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)
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
def get_search_results(self, search_query: str, filter: str="all", **kwargs) -> t.List[str]:
|
|
||||||
"""
|
|
||||||
Obtains search results for a query.
|
|
||||||
|
|
||||||
### Parameters:
|
|
||||||
- `search_query` - The query to get search results for
|
|
||||||
- `**kwargs` - Additional keyword arguments to pass to `requests.Session.get`
|
|
||||||
"""
|
|
||||||
|
|
||||||
kwargs.update({'params': {'q': search_query, 'filter': filter}})
|
|
||||||
|
|
||||||
return self._get_json(f"/search", **kwargs)
|
|
327
piped_api/models.html
Normal file
327
piped_api/models.html
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,15 +0,0 @@
|
||||||
import typing as t
|
|
||||||
|
|
||||||
|
|
||||||
class BasePipedModel:
|
|
||||||
"""
|
|
||||||
Base class for all Piped models.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, data: t.Dict[str, t.Any]) -> None:
|
|
||||||
"""
|
|
||||||
### Parameters:
|
|
||||||
- `data` - The JSON (`dict`) data to initialize the model with.
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.data = data
|
|
651
piped_api/models/channels.html
Normal file
651
piped_api/models/channels.html
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,104 +0,0 @@
|
||||||
import typing as t
|
|
||||||
|
|
||||||
from . import BasePipedModel
|
|
||||||
from .videos import Video
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class NextPageChannel(BasePipedModel):
|
|
||||||
"""
|
|
||||||
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
|
|
||||||
def id(self) -> str:
|
|
||||||
"""
|
|
||||||
The channel's ID
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['id']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self) -> str:
|
|
||||||
"""
|
|
||||||
The channel's name
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['name']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def avatar_url(self) -> str:
|
|
||||||
"""
|
|
||||||
The channel's avatar URL
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['avatarUrl']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def banner_url(self) -> str:
|
|
||||||
"""
|
|
||||||
The channel's banner URL
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['bannerUrl']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def description(self) -> str:
|
|
||||||
"""
|
|
||||||
The channel's description
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['description']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def subscriber_count(self) -> int:
|
|
||||||
"""
|
|
||||||
The number of subscribers the channel has
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['subscriberCount']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def verified(self) -> bool:
|
|
||||||
"""
|
|
||||||
Whether or not the channel is verified by YouTube (has a badge)
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['verified']
|
|
1002
piped_api/models/comments.html
Normal file
1002
piped_api/models/comments.html
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,163 +0,0 @@
|
||||||
import typing as t
|
|
||||||
|
|
||||||
from . import BasePipedModel
|
|
||||||
|
|
||||||
|
|
||||||
class Comments(BasePipedModel):
|
|
||||||
class Comment(BasePipedModel):
|
|
||||||
@property
|
|
||||||
def author(self) -> str:
|
|
||||||
"""
|
|
||||||
The name of the author of the comment
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['author']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def comment_id(self) -> str:
|
|
||||||
"""
|
|
||||||
The comment ID
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['commentId']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def comment_text(self) -> str:
|
|
||||||
"""
|
|
||||||
The text of the comment
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['commentText']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def commented_time(self) -> str:
|
|
||||||
"""
|
|
||||||
The time the comment was made (format: `'x y ago'`).
|
|
||||||
|
|
||||||
### Note:
|
|
||||||
The raw time from API also includes the `'(edited)'` suffix to mark comment as edited (if it was).
|
|
||||||
By accessing this property, the suffix is automatically removed.
|
|
||||||
If you for whatever reason want to keep the suffix, access this property directly via `Comment.data['commentedTime']`
|
|
||||||
"""
|
|
||||||
|
|
||||||
time: str = self.data['commentedTime']
|
|
||||||
|
|
||||||
return time.removesuffix(' (edited)')
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_edited(self) -> bool:
|
|
||||||
"""
|
|
||||||
Whether or not the comment is edited.
|
|
||||||
|
|
||||||
### Note:
|
|
||||||
This property checks whether there is `'(edited)'` in the `commentedTime` property, because that's where you get that from.
|
|
||||||
See `Comments.Comment.commented_time`
|
|
||||||
"""
|
|
||||||
|
|
||||||
time: str = self.data['commentedTime']
|
|
||||||
|
|
||||||
return time.endswith('(edited)')
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def commentor_url(self) -> str:
|
|
||||||
"""
|
|
||||||
The URL of the channel that made the comment
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['commentorUrl']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def replies_page(self) -> t.Optional[str]:
|
|
||||||
"""
|
|
||||||
Same as `Comments.nextpage`, but to load replies.
|
|
||||||
|
|
||||||
`None` means that there are no replies.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['repliesPage']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def hearted(self) -> bool:
|
|
||||||
"""
|
|
||||||
Whether or not the comment has been hearted
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['hearted']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def like_count(self) -> int:
|
|
||||||
"""
|
|
||||||
The number of likes the comment has
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['likeCount']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pinned(self) -> bool:
|
|
||||||
"""
|
|
||||||
Whether or not the comment is pinned
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['pinned']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def thumbnail(self) -> str:
|
|
||||||
"""
|
|
||||||
The thumbnail of the commentor's channel
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['thumbnail']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def verified(self) -> bool:
|
|
||||||
"""
|
|
||||||
Whether or not the author of the comment is verified
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['verified']
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_comments(self) -> t.List[Comment]:
|
|
||||||
"""
|
|
||||||
Obtain a list of comments
|
|
||||||
"""
|
|
||||||
|
|
||||||
return [self.Comment(comment_json) for comment_json in self.data['comments']]
|
|
||||||
|
|
||||||
|
|
||||||
def __iter__(self) -> t.Iterator[Comment]:
|
|
||||||
iter(self.get_comments())
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def disabled(self) -> bool:
|
|
||||||
"""
|
|
||||||
Whether or not the comments are disabled
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['disabled']
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def nextpage(self) -> t.Optional[str]:
|
|
||||||
"""
|
|
||||||
A JSON encoded page, which is used for the nextpage endpoint.
|
|
||||||
|
|
||||||
If there is no nextpage data, this returns `None`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['nextpage']
|
|
2739
piped_api/models/videos.html
Normal file
2739
piped_api/models/videos.html
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,559 +0,0 @@
|
||||||
import typing as t
|
|
||||||
|
|
||||||
from datetime import datetime, date, timedelta
|
|
||||||
|
|
||||||
from . import BasePipedModel
|
|
||||||
|
|
||||||
|
|
||||||
class Video(BasePipedModel):
|
|
||||||
@property
|
|
||||||
def title(self) -> str:
|
|
||||||
"""
|
|
||||||
The title/name of the video
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['title']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def description(self) -> str:
|
|
||||||
"""
|
|
||||||
The description of the video
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['description']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def upload_date(self) -> date:
|
|
||||||
"""
|
|
||||||
The date the video was uploaded at.
|
|
||||||
|
|
||||||
### Note:
|
|
||||||
Use `Video.data['uploadDate']` to get the raw string that was obtained from the API - this package
|
|
||||||
automatically converts the raw string to a `datetime.date` object.
|
|
||||||
"""
|
|
||||||
|
|
||||||
raw = self.data['uploadDate']
|
|
||||||
|
|
||||||
return datetime.strptime(raw, r"%Y-%m-%d").date()
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def uploader(self) -> str:
|
|
||||||
"""
|
|
||||||
The channel name of the author of the video
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['uploader']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def uploader_url(self) -> str:
|
|
||||||
"""
|
|
||||||
The URI to the author's channel
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['uploaderUrl']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def uploader_avatar(self) -> str:
|
|
||||||
"""
|
|
||||||
The URL to the video author's avatar image
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['uploaderAvatar']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def thumbnail_url(self) -> str:
|
|
||||||
"""
|
|
||||||
The URL to the video's thumbnail image
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['thumbnail']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def hls(self) -> t.Optional[str]:
|
|
||||||
"""
|
|
||||||
The hls manifest URL, to be used for Livestreams
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['hls']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def dash(self) -> t.Optional[str]:
|
|
||||||
"""
|
|
||||||
The dash manifest URL for OTF streams
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['dash']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def lbry_id(self) -> str:
|
|
||||||
"""
|
|
||||||
The lbry id of the video, if available. I assume this has something to do with https://lbry.com/
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['lbryId']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def uploader_verified(self) -> str:
|
|
||||||
"""
|
|
||||||
Whether or not the channel that uploaded the video is verified by YouTube (badge)
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['uploaderVerified']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def duration(self) -> timedelta:
|
|
||||||
"""
|
|
||||||
The duration of the video.
|
|
||||||
|
|
||||||
### Note:
|
|
||||||
The original value from the API was in seconds (`Video.data['duration']`), but this package
|
|
||||||
converts it to a `datetime.timedelta` object.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return timedelta(seconds=self.data['duration'])
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def views(self) -> int:
|
|
||||||
"""
|
|
||||||
The number of views the video has received
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['views']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def likes(self) -> int:
|
|
||||||
"""
|
|
||||||
The amount of likes the video has received. `-1` if hidden
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['likes']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def dislikes(self) -> int:
|
|
||||||
"""
|
|
||||||
The amount of dislikes the video has received. `-1` if hidden
|
|
||||||
|
|
||||||
### Note:
|
|
||||||
This is obsolete since YouTube did a tiny gigantical little big whoopsie with their like system and screwed it all up
|
|
||||||
You can use awesome user-made projects such as https://returnyoutubedislike.com to obtain the dislike count
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['dislikes']
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Stream(BasePipedModel):
|
|
||||||
"""
|
|
||||||
Either an audio or video stream of a video
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def url(self) -> str:
|
|
||||||
"""
|
|
||||||
The URL of the stream
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['url']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def format(self) -> str:
|
|
||||||
"""
|
|
||||||
The format of the stream (`'M4A' or 'WEBMA_OPUS' or 'MPEG_4' or 'WEBM' or 'v3GPP'`
|
|
||||||
|
|
||||||
No, I don't know how many are there or what does each mean
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['format']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def quality(self) -> str:
|
|
||||||
"""
|
|
||||||
The standard quality we all know and love (e. g.: `'240p'` for video or `'128k'` for audio)
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['quality']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def mime_type(self) -> str:
|
|
||||||
"""
|
|
||||||
If you come from web development (or other invidious area that works with these French mimes),
|
|
||||||
then you already know what this is. If not, consider [checking the Mozilla documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types)
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['mimeType']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def codec(self) -> str:
|
|
||||||
"""
|
|
||||||
What is this? I don't know. A codec?
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['codec']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def video_only(self) -> bool:
|
|
||||||
"""
|
|
||||||
Whether or not the stream is video only (AKA. muted video)
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['videoOnly']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bitrate(self) -> int:
|
|
||||||
"""
|
|
||||||
The bitrate of the stream
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['bitrate']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def init_start(self) -> int:
|
|
||||||
"""
|
|
||||||
Not sure what this does, but it seems to be useful for creating dash streams
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['initStart']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def init_end(self) -> int:
|
|
||||||
"""
|
|
||||||
Not sure what this does, but it seems to be useful for creating dash streams
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['initEnd']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def index_start(self) -> int:
|
|
||||||
"""
|
|
||||||
Not sure what this does, but it seems to be useful for creating dash streams
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['indexStart']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def index_end(self) -> int:
|
|
||||||
"""
|
|
||||||
Not sure what this does, but it seems to be useful for creating dash streams
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['indexEnd']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def width(self) -> int:
|
|
||||||
"""
|
|
||||||
The width of the stream. `'0'` for audio streams (makes sense)
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['width']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def height(self) -> int:
|
|
||||||
"""
|
|
||||||
The height of the stream. `'0'` for audio streams (makes sense)
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['width']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def fps(self) -> int:
|
|
||||||
"""
|
|
||||||
Frames Per Second. This is usually `'0'` for audio and `'30'` or `'60'` for video
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['fps']
|
|
||||||
|
|
||||||
|
|
||||||
def get_streams(self, type: t.Literal['video', 'audio']='video') -> t.List[Stream]:
|
|
||||||
"""
|
|
||||||
Get the streams of a video.
|
|
||||||
|
|
||||||
### Parameters:
|
|
||||||
- `type` - The type of stream to get. Either `'video'` or `'audio'`
|
|
||||||
"""
|
|
||||||
|
|
||||||
if type == 'video' or type == 'audio':
|
|
||||||
return [self.Stream(stream_data) for stream_data in self.data[f"{type}Streams"]]
|
|
||||||
|
|
||||||
raise ValueError('Invalid stream type. Must be either `video` or `audio`')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class RelatedStream(BasePipedModel):
|
|
||||||
"""
|
|
||||||
A related stream (e. g.: related video to the current one from the right sidebar, video related to/uploaded by a channel and trending video).
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def url(self) -> str:
|
|
||||||
"""
|
|
||||||
The URL to the related video
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['url']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def title(self) -> str:
|
|
||||||
"""
|
|
||||||
The title of the related video
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['title']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def thumbnail(self) -> str:
|
|
||||||
"""
|
|
||||||
The thumbnail URL of the related video
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['thumbnail']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def uploader_name(self) -> str:
|
|
||||||
"""
|
|
||||||
The name of the channel that uploaded the related video
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['uploaderName']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def uploader_url(self) -> str:
|
|
||||||
"""
|
|
||||||
The URL of the channel that uploaded the related video
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['uploaderUrl']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def uploader_avatar(self) -> str:
|
|
||||||
"""
|
|
||||||
The URL of the channel's avatar
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['uploaderAvatar']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def uploaded_date(self) -> str:
|
|
||||||
"""
|
|
||||||
The date the related video was uploaded (format: `'x y ago'`)
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['uploadedDate']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def short_description(self) -> t.Optional[str]:
|
|
||||||
"""
|
|
||||||
The short description of the related video. As far as I know, this is always `None` - perhaps some unused YouTube feature?
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['shortDescription']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def duration(self) -> timedelta:
|
|
||||||
"""
|
|
||||||
The duration of the related video.
|
|
||||||
|
|
||||||
### Note:
|
|
||||||
The original value from the API was in seconds (`Video.data['duration']`), but this package
|
|
||||||
converts it to a `datetime.timedelta` object.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return timedelta(seconds=self.data['duration'])
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def views(self) -> str:
|
|
||||||
"""
|
|
||||||
The amount of views the related video has received
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['views']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def uploaded(self) -> datetime:
|
|
||||||
"""
|
|
||||||
The date the related video was uploaded (as a `datetime.datetime` object).
|
|
||||||
|
|
||||||
### Note:
|
|
||||||
The original value was in milliseconds since epoch (`Video.data['uploaded']`), but this package converts it to a `datetime.datetime` object.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return datetime.fromtimestamp(self.data['uploaded'] / 1000)
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def uploader_verified(self) -> bool:
|
|
||||||
"""
|
|
||||||
Whether or not the channel that uploaded the related video is verified by YouTube (e. g.: has badge)
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['uploaderVerified']
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def related_videos(self) -> t.List[RelatedStream]:
|
|
||||||
"""
|
|
||||||
List of related streams
|
|
||||||
"""
|
|
||||||
|
|
||||||
return [self.RelatedStream(video_data) for video_data in self.data['relatedStreams']]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Subtitle(BasePipedModel):
|
|
||||||
@property
|
|
||||||
def url(self) -> str:
|
|
||||||
"""
|
|
||||||
The URL to the subtitle
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['url']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def mime_type(self) -> str:
|
|
||||||
"""
|
|
||||||
If you come from web development (or other invidious area that works with these French mimes),
|
|
||||||
then you already know what this is. If not, consider [checking the Mozilla documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types)
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['mimeType']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self) -> str:
|
|
||||||
"""
|
|
||||||
The name of the language the captions are in
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['name']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def code(self) -> str:
|
|
||||||
"""
|
|
||||||
The country code for the captions
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['code']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def auto_generated(self) -> bool:
|
|
||||||
"""
|
|
||||||
Whether or not the captions are auto-generated by YouTube
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['autoGenerated']
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def subtitles(self) -> t.List[Subtitle]:
|
|
||||||
"""
|
|
||||||
A list of captions for the video
|
|
||||||
"""
|
|
||||||
|
|
||||||
return [self.Subtitle(subtitle_data) for subtitle_data in self.data['subtitles']]
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def livestream(self) -> bool:
|
|
||||||
"""
|
|
||||||
Whether or not the video is a livestream
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['livestream']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def proxy_url(self) -> str:
|
|
||||||
"""
|
|
||||||
The base URL for Piped proxy
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['proxyUrl']
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Chapter(BasePipedModel):
|
|
||||||
"""
|
|
||||||
A video chapter (or "section").
|
|
||||||
|
|
||||||
YouTube displays a list of chapters, if there are timestamps in the description.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def title(self) -> str:
|
|
||||||
"""
|
|
||||||
The title of the chapter
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['title']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def image(self) -> str:
|
|
||||||
"""
|
|
||||||
The image URL for the chapter
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.data['image']
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def start(self) -> timedelta:
|
|
||||||
"""
|
|
||||||
The start time of the chapter
|
|
||||||
|
|
||||||
### Note:
|
|
||||||
The original value from the API was in seconds, this package automatically converts it to `datetime.timedelta`
|
|
||||||
"""
|
|
||||||
|
|
||||||
return timedelta(seconds=self.data['start'])
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def chapters(self) -> t.List[Chapter]:
|
|
||||||
"""
|
|
||||||
A list of chapters for the video
|
|
||||||
"""
|
|
||||||
|
|
||||||
return [self.Chapter(chapter_data) for chapter_data in self.data['chapters']]
|
|
|
@ -1,3 +0,0 @@
|
||||||
pytest
|
|
||||||
|
|
||||||
requests
|
|
|
@ -1,4 +0,0 @@
|
||||||
cd "$(dirname "$0")"
|
|
||||||
cd ../
|
|
||||||
|
|
||||||
pdoc -o documentation piped_api
|
|
|
@ -1,4 +0,0 @@
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
bash ./build_documentation.sh
|
|
||||||
bash ./compile_for_pypi.sh
|
|
|
@ -1,4 +0,0 @@
|
||||||
cd "$(dirname "$0")"
|
|
||||||
cd ../
|
|
||||||
|
|
||||||
python setup.py sdist
|
|
46
search.js
Normal file
46
search.js
Normal file
File diff suppressed because one or more lines are too long
62
setup.py
62
setup.py
|
@ -1,62 +0,0 @@
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import sys
|
|
||||||
sys.path.append('.')
|
|
||||||
|
|
||||||
import setuptools
|
|
||||||
|
|
||||||
|
|
||||||
__description__ = "Piped API client"
|
|
||||||
__author__ = "SKevo"
|
|
||||||
__copyright__ = "Copyright (c) 2021, SKevo"
|
|
||||||
__credits__ = ["SKevo"]
|
|
||||||
__license__ = "MIT"
|
|
||||||
__version__ = "v1.0.1-beta"
|
|
||||||
__maintainer__ = "SKevo"
|
|
||||||
__email__ = "me@kevo.link"
|
|
||||||
__status__ = "4 - Beta"
|
|
||||||
|
|
||||||
|
|
||||||
README_PATH = Path(__file__).parent.absolute() / Path('README.md')
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(README_PATH, 'r', encoding="UTF-8") as readme:
|
|
||||||
__readme__ = readme.read()
|
|
||||||
|
|
||||||
except:
|
|
||||||
__readme__ = "Failed to read README.md!"
|
|
||||||
|
|
||||||
__doc__ = __readme__
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
setuptools.setup(
|
|
||||||
name = 'piped-api',
|
|
||||||
packages = setuptools.find_packages(exclude=('tests',)),
|
|
||||||
|
|
||||||
long_description=__readme__,
|
|
||||||
long_description_content_type='text/markdown',
|
|
||||||
|
|
||||||
version = __version__,
|
|
||||||
license = __license__,
|
|
||||||
description = __description__,
|
|
||||||
keywords = ["piped", "api", "client"],
|
|
||||||
|
|
||||||
author = __author__,
|
|
||||||
author_email = __email__,
|
|
||||||
|
|
||||||
url = 'https://github.com/CWKevo/python-piped-api-client',
|
|
||||||
|
|
||||||
install_requires=['requests'],
|
|
||||||
|
|
||||||
classifiers=[
|
|
||||||
f'Development Status :: {__status__}',
|
|
||||||
'Intended Audience :: Developers',
|
|
||||||
'License :: OSI Approved :: MIT License',
|
|
||||||
'Programming Language :: Python :: 3.6',
|
|
||||||
'Programming Language :: Python :: 3.7',
|
|
||||||
'Programming Language :: Python :: 3.8',
|
|
||||||
'Programming Language :: Python :: 3.9',
|
|
||||||
'Programming Language :: Python :: 3.10',
|
|
||||||
],
|
|
||||||
)
|
|
|
@ -1,4 +0,0 @@
|
||||||
from piped_api import PipedClient
|
|
||||||
|
|
||||||
|
|
||||||
CLIENT = PipedClient()
|
|
|
@ -1,58 +0,0 @@
|
||||||
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()
|
|
|
@ -1,34 +0,0 @@
|
||||||
from tests import CLIENT
|
|
||||||
|
|
||||||
|
|
||||||
def test_comments(video_id: str='dQw4w9WgXcQ') -> None:
|
|
||||||
"""
|
|
||||||
Prints out first 20 pages of comments from a video.
|
|
||||||
"""
|
|
||||||
|
|
||||||
at_page = 0
|
|
||||||
max_pages = 5
|
|
||||||
total_comments = 0
|
|
||||||
np = None
|
|
||||||
|
|
||||||
while at_page < max_pages:
|
|
||||||
comments = CLIENT.get_comments(video_id, nextpage=np, params={'hl': 'us'})
|
|
||||||
at_page += 1
|
|
||||||
|
|
||||||
print('=' * 35, f'Page: {at_page}', '=' * 35)
|
|
||||||
for comment in comments.get_comments():
|
|
||||||
total_comments += 1
|
|
||||||
print(f'Comment {comment.comment_id} by "{comment.author}" ({comment.commented_time}), {comment.like_count} likes: "{comment.comment_text}"')
|
|
||||||
|
|
||||||
if comments.nextpage == None:
|
|
||||||
print(f"No more comments! Total: {total_comments}, expected: {max_pages * 20}")
|
|
||||||
break
|
|
||||||
|
|
||||||
np = comments.nextpage
|
|
||||||
|
|
||||||
print(f"Okay, that's enough comments... Total: {total_comments}, expected: {max_pages * 20}")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
test_comments()
|
|
|
@ -1,17 +0,0 @@
|
||||||
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()
|
|
|
@ -1,63 +0,0 @@
|
||||||
import typing as t
|
|
||||||
|
|
||||||
from tests import CLIENT
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
|
|
||||||
def test_video(video_id: str='dQw4w9WgXcQ') -> None:
|
|
||||||
"""
|
|
||||||
Prints out information about a video.
|
|
||||||
"""
|
|
||||||
|
|
||||||
video = CLIENT.get_video(video_id)
|
|
||||||
short_description = video.description[:100].replace('\n', '')
|
|
||||||
|
|
||||||
print(f"""
|
|
||||||
Video ID: {video_id}
|
|
||||||
Title: {video.title}
|
|
||||||
Description: {short_description}...
|
|
||||||
Views: {video.views}
|
|
||||||
|
|
||||||
Uploaded by: {video.uploader}
|
|
||||||
Uploaded on: {video.upload_date} ({datetime.now().year - video.upload_date.year} years ago)
|
|
||||||
|
|
||||||
Duration: {video.duration}
|
|
||||||
FPS: {video.get_streams('video')[0].fps}
|
|
||||||
""")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_trending(country_codes: t.List[str]=['US', 'SK', 'CN']) -> None:
|
|
||||||
"""
|
|
||||||
Prints out trending videos for a specific country.
|
|
||||||
"""
|
|
||||||
|
|
||||||
for country_code in country_codes:
|
|
||||||
videos = CLIENT.get_trending(country_code)
|
|
||||||
|
|
||||||
# Nothing ever trends in China's YouTube:
|
|
||||||
if country_code == 'CN':
|
|
||||||
assert len(videos) == 0
|
|
||||||
print("\nYes, empty list works.")
|
|
||||||
|
|
||||||
for video in videos:
|
|
||||||
print(f"{video.uploader_name} >> {video.title} ({video.views} views)")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_audio(video_id: str='dQw4w9WgXcQ') -> None:
|
|
||||||
"""
|
|
||||||
Prints out the first audio stream URL for a video.
|
|
||||||
"""
|
|
||||||
|
|
||||||
video = CLIENT.get_video(video_id)
|
|
||||||
audio_stream = video.get_streams('audio')[0]
|
|
||||||
|
|
||||||
print(f"Audio stream URL: {audio_stream.url} ({audio_stream.mime_type})")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
test_video()
|
|
||||||
test_trending()
|
|
||||||
test_get_audio()
|
|
Loading…
Reference in a new issue