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…
	
	Add table
		Add a link
		
	
		Reference in a new issue