├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── Makefile ├── make.bat └── source │ ├── conf.py │ ├── contents.rst │ └── index.rst ├── setup.py ├── tests └── test_types.py └── ytm ├── __init__.py ├── __main__.py ├── apis ├── AbstractYouTubeMusic │ ├── AbstractYouTubeMusic.py │ ├── __init__.py │ ├── decorators │ │ ├── __init__.py │ │ ├── catch.py │ │ └── method.py │ └── methods │ │ ├── __init__.py │ │ ├── _search_filter.py │ │ ├── album.py │ │ ├── artist.py │ │ ├── artist_albums.py │ │ ├── artist_singles.py │ │ ├── guide.py │ │ ├── home.py │ │ ├── hotlist.py │ │ ├── playlist.py │ │ ├── queue.py │ │ ├── search.py │ │ ├── search_albums.py │ │ ├── search_artists.py │ │ ├── search_playlists.py │ │ ├── search_songs.py │ │ ├── search_suggestions.py │ │ ├── search_videos.py │ │ ├── song.py │ │ ├── watch.py │ │ ├── watch_radio.py │ │ └── watch_shuffle.py ├── BaseYouTubeMusic │ ├── BaseYouTubeMusic.py │ ├── __init__.py │ ├── constants │ │ └── __init__.py │ ├── decorators │ │ ├── __init__.py │ │ └── catch.py │ ├── methods │ │ ├── __init__.py │ │ ├── _get_page.py │ │ ├── _url.py │ │ ├── _url_api.py │ │ ├── _url_yt.py │ │ ├── browse.py │ │ ├── browse_album.py │ │ ├── browse_artist.py │ │ ├── browse_home.py │ │ ├── browse_hotlist.py │ │ ├── browse_playlist.py │ │ ├── guide.py │ │ ├── next.py │ │ ├── page_channel.py │ │ ├── page_home.py │ │ ├── page_hotlist.py │ │ ├── page_playlist.py │ │ ├── page_search.py │ │ ├── page_watch.py │ │ ├── queue.py │ │ ├── search.py │ │ ├── search_suggestions.py │ │ └── video_info.py │ └── utils │ │ ├── __init__.py │ │ ├── is_float.py │ │ ├── parse_fflags.py │ │ └── random_user_agent.py ├── YouTubeMusic │ ├── YouTubeMusic.py │ └── __init__.py ├── YouTubeMusicDL │ ├── YouTubeMusicDL.py │ └── __init__.py └── __init__.py ├── classes ├── BuiltinMeta.py └── __init__.py ├── constants └── __init__.py ├── decorators ├── __init__.py ├── _enforce.py ├── _set_attrs.py ├── enforce.py ├── enforce_parameters.py ├── enforce_return_value.py ├── parse.py ├── rename.py └── rename_module.py ├── exceptions ├── ArgumentError.py ├── ConnectionError.py ├── InvalidPageConfigurationError.py ├── MethodError.py ├── PageNotFoundError.py ├── ParserError.py ├── YouTubeApiError.py ├── YouTubeMusicApiError.py ├── __init__.py └── base │ ├── BaseException.py │ └── __init__.py ├── parsers ├── __init__.py ├── _search.py ├── _search_filter.py ├── album.py ├── artist.py ├── artist_albums.py ├── artist_singles.py ├── cleansers │ ├── __init__.py │ ├── ascii_time.py │ ├── iso_time.py │ ├── type.py │ └── views.py ├── constants │ └── __init__.py ├── decorators │ ├── __init__.py │ └── catch.py ├── formatters │ ├── __init__.py │ └── menu_items.py ├── guide.py ├── home.py ├── hotlist.py ├── playlist.py ├── queue.py ├── search.py ├── search_albums.py ├── search_artists.py ├── search_playlists.py ├── search_songs.py ├── search_suggestions.py ├── search_videos.py ├── song.py ├── watch.py ├── watch_radio.py └── watch_shuffle.py ├── types ├── __init__.py ├── base │ ├── Continuation.py │ ├── Id.py │ ├── Params.py │ ├── TypeB64.py │ ├── TypeStr.py │ ├── Union.py │ └── __init__.py ├── constants │ └── __init__.py ├── continuations │ ├── HomeContinuation.py │ ├── PlaylistContinuation.py │ ├── SearchContinuation.py │ ├── WatchContinuation.py │ └── __init__.py ├── ids │ ├── AlbumBrowseId.py │ ├── AlbumId.py │ ├── AlbumPlaylistBrowseId.py │ ├── AlbumPlaylistId.py │ ├── AlbumRadioId.py │ ├── AlbumShuffleId.py │ ├── ArtistBrowseId.py │ ├── ArtistId.py │ ├── ArtistRadioId.py │ ├── ArtistShuffleId.py │ ├── ArtistSongsPlaylistId.py │ ├── PlaylistBrowseId.py │ ├── PlaylistId.py │ ├── PlaylistRadioId.py │ ├── PlaylistShuffleId.py │ ├── SongId.py │ ├── SongRadioId.py │ └── __init__.py ├── params │ ├── ArtistAlbumsParams.py │ ├── ArtistSinglesParams.py │ └── __init__.py └── utils │ ├── __init__.py │ ├── is_base64.py │ ├── pad_base64.py │ └── truncate.py └── utils ├── __init__.py ├── _url.py ├── filter.py ├── first.py ├── get.py ├── include.py ├── isinstance.py ├── lstrip.py ├── rstrip.py ├── url_yt.py └── url_ytm.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # Environments 101 | .env 102 | .venv 103 | env/ 104 | venv/ 105 | ENV/ 106 | env.bak/ 107 | venv.bak/ 108 | 109 | # PyCharm 110 | .idea/ 111 | 112 | # VS Code 113 | .vscode/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | 133 | # Custom 134 | app.py 135 | app.*.py 136 | tst.py 137 | tst.*.py 138 | _.*.py 139 | .ignore/ -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('../../')) 16 | 17 | # Import libraries 18 | import sphinx_rtd_theme 19 | 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = 'python-youtube-music' 24 | copyright = '2020, Tom Bulled' 25 | author = 'Tom Bulled' 26 | 27 | # The full version, including alpha/beta/rc tags 28 | release = '0.0.1' 29 | 30 | 31 | # -- General configuration --------------------------------------------------- 32 | 33 | # Add any Sphinx extension module names here, as strings. They can be 34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 35 | # ones. 36 | extensions = [ 37 | 'sphinx.ext.autodoc', 38 | 'sphinx_rtd_theme', 39 | ] 40 | 41 | # Add any paths that contain templates here, relative to this directory. 42 | templates_path = ['_templates'] 43 | 44 | # List of patterns, relative to source directory, that match files and 45 | # directories to ignore when looking for source files. 46 | # This pattern also affects html_static_path and html_extra_path. 47 | exclude_patterns = [] 48 | 49 | 50 | # -- Options for HTML output ------------------------------------------------- 51 | 52 | # The theme to use for HTML and HTML Help pages. See the documentation for 53 | # a list of builtin themes. 54 | # 55 | # html_theme = 'alabaster' 56 | html_theme = 'sphinx_rtd_theme' 57 | 58 | # Add any paths that contain custom static files (such as style sheets) here, 59 | # relative to this directory. They are copied after the builtin static files, 60 | # so a file named "default.css" will overwrite the builtin "default.css". 61 | html_static_path = ['_static'] 62 | -------------------------------------------------------------------------------- /docs/source/contents.rst: -------------------------------------------------------------------------------- 1 | Contents 2 | ======== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | 9 | 10 | Indices and tables 11 | ================== 12 | 13 | * :ref:`genindex` 14 | * :ref:`modindex` 15 | * :ref:`search` 16 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to python-youtube-music's documentation! 2 | ================================================ 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | .. automodule:: ytm 9 | :members: 10 | 11 | .. autoclass:: ytm.AbstractYouTubeMusic 12 | :members: 13 | :inherited-members: 14 | :exclude-members: 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | NAME = 'ytm' 4 | VERSION = '0.1.0' 5 | DESCRIPTION = 'Python YouTube Music Web API Client' 6 | AUTHOR = 'Tom Bulled' 7 | URL = 'https://github.com/tombulled/python-youtube-music' 8 | PACKAGES = setuptools.find_packages() 9 | PYTHON_VERSION = '>=3.0.0' 10 | 11 | DEPENDENCIES_REQUIRED = \ 12 | ( 13 | 'requests', 14 | ) 15 | 16 | DEPENDENCIES_OPTIONAL = \ 17 | { 18 | 'dl': \ 19 | ( 20 | 'youtube-dl', 21 | 'mutagen', 22 | 'Pillow', 23 | ), 24 | 'dev': \ 25 | ( 26 | 'pytest', 27 | ), 28 | } 29 | 30 | config = dict \ 31 | ( 32 | name = NAME, 33 | version = VERSION, 34 | description = DESCRIPTION, 35 | author = AUTHOR, 36 | packages = PACKAGES, 37 | install_requires = DEPENDENCIES_REQUIRED, 38 | extras_require = DEPENDENCIES_OPTIONAL, 39 | python_requires = PYTHON_VERSION, 40 | url = URL, 41 | ) 42 | 43 | setuptools.setup(**config) 44 | -------------------------------------------------------------------------------- /ytm/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: Tom Bulled 3 | License: GNU General Public License v3.0 4 | Site: https://github.com/tombulled/python-youtube-music 5 | 6 | Python 3 YouTube Music Web API Client. 7 | 8 | The library facilitates *unauthenticated* requests to music.youtube.com. 9 | API classes are easily available and should provide all of the required functionality. 10 | Code documentation is available throughout. 11 | 12 | Getting Started: 13 | Import the library: 14 | >>> import ytm 15 | Create an API instance: 16 | >>> api = ytm.YouTubeMusic() 17 | >>> api 18 | 19 | See available methods: 20 | >>> from pprint import pprint 21 | >>> 22 | >>> pprint(list(api._methods)) 23 | ['_search_filter', 24 | 'album', 25 | 'artist', 26 | 'artist_albums', 27 | 'artist_singles', 28 | 'guide', 29 | 'home', 30 | 'hotlist', 31 | 'playlist', 32 | 'search', 33 | 'search_albums', 34 | 'search_artists', 35 | 'search_playlists', 36 | 'search_songs', 37 | 'search_suggestions', 38 | 'search_videos', 39 | 'song', 40 | 'watch', 41 | 'watch_radio', 42 | 'watch_shuffle'] 43 | >>> 44 | View documentation on a method: 45 | >>> help(api.search_suggestions) 46 | Help on method search_suggestions in module ytm.apis.AbstractYouTubeMusic.methods.search_suggestions: 47 | 48 | search_suggestions(query: str) -> list method of ytm.apis.YouTubeMusic.YouTubeMusic.YouTubeMusic instance 49 | Retrieve search suggestions. 50 | 51 | Args: 52 | self: Class Instance 53 | query: Search query 54 | Example: 'imagine' 55 | 56 | Returns: 57 | List of search suggestions 58 | 59 | Raises: 60 | MethodError: Method encountered an error 61 | 62 | Example: 63 | >>> api = ytm.AbstractYouTubeMusic() 64 | >>> 65 | >>> suggestions = api.search_suggestions('imagine') 66 | >>> 67 | >>> suggestions[0] 68 | 'imagine dragons' 69 | >>> 70 | 71 | >>> 72 | Call a method: 73 | >>> from pprint import pprint 74 | >>> 75 | >>> data = api.search_suggestions('foo') 76 | >>> 77 | >>> pprint(data) 78 | ['footloose', 79 | 'foo fighters', 80 | 'football songs', 81 | 'foogiano', 82 | 'foolish ashanti', 83 | 'foo fighters everlong', 84 | 'foolio'] 85 | >>> 86 | ''' 87 | 88 | from . import utils 89 | from . import apis 90 | 91 | __inherit = utils.__include(apis.__spec__) 92 | 93 | locals().update(__inherit) 94 | 95 | __all__ = \ 96 | ( 97 | *tuple(__inherit), 98 | *tuple(utils.__include(__spec__)), 99 | ) 100 | -------------------------------------------------------------------------------- /ytm/__main__.py: -------------------------------------------------------------------------------- 1 | # YouTubeMusicDL CLI code here. 2 | -------------------------------------------------------------------------------- /ytm/apis/AbstractYouTubeMusic/AbstractYouTubeMusic.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the Api class: AbstractYouTubeMusic 3 | ''' 4 | 5 | from ..BaseYouTubeMusic import BaseYouTubeMusic 6 | from . import methods 7 | 8 | class AbstractYouTubeMusic(object): 9 | ''' 10 | Abstract YouTube Music class. 11 | 12 | Base methods get abstracted and parsed. 13 | 14 | Attributes: 15 | _base: Base class reference 16 | _methods: API Methods 17 | ''' 18 | 19 | _base: BaseYouTubeMusic = None 20 | _methods: list = {} 21 | 22 | def __init__(self: object) -> None: 23 | ''' 24 | Initialise class. 25 | 26 | Args: 27 | self: Class instance 28 | 29 | Returns: 30 | None 31 | 32 | Example: 33 | >>> api = AbstractYouTubeMusic() 34 | ''' 35 | 36 | self._base = BaseYouTubeMusic() 37 | 38 | for method_name in methods.__all__: 39 | method = getattr(methods, method_name) 40 | 41 | setattr(self.__class__, method_name, method) 42 | 43 | if not method_name.startswith('_'): 44 | self._methods[method_name] = method 45 | 46 | def __repr__(self: object) -> str: 47 | ''' 48 | Return a string representation of the object. 49 | 50 | Returns a string in the format <{class_name}()> 51 | 52 | Args: 53 | self: Class instance 54 | 55 | Returns: 56 | String representation of the object 57 | 58 | Example: 59 | >>> api = AbstractYouTubeMusic() 60 | >>> 61 | >>> api 62 | 63 | >>> 64 | ''' 65 | 66 | return '<{class_name}()>'.format \ 67 | ( 68 | class_name = self.__class__.__name__, 69 | ) 70 | -------------------------------------------------------------------------------- /ytm/apis/AbstractYouTubeMusic/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Package containing the Api class: AbstractYouTubeMusic 3 | ''' 4 | 5 | from .AbstractYouTubeMusic import AbstractYouTubeMusic 6 | 7 | __all__ = (AbstractYouTubeMusic.__name__,) 8 | -------------------------------------------------------------------------------- /ytm/apis/AbstractYouTubeMusic/decorators/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Package containing decorators. 3 | 4 | These decorators range in purpose, but help the super package 5 | achieve general tasks 6 | ''' 7 | 8 | from .... import utils as __utils 9 | import types as __types 10 | 11 | __all__ = tuple \ 12 | ( 13 | __utils.include \ 14 | ( 15 | __spec__, 16 | lambda object: not isinstance(object, __types.ModuleType) \ 17 | and not object.__name__.startswith('_'), 18 | ), 19 | ) 20 | -------------------------------------------------------------------------------- /ytm/apis/AbstractYouTubeMusic/decorators/catch.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the decorator: catch 3 | ''' 4 | 5 | import functools 6 | from typing import Callable, Any 7 | from .... import exceptions 8 | 9 | def catch(func: Callable) -> Callable: 10 | ''' 11 | Catch method errors and re-raise them appropriately. 12 | 13 | Methods can raise exceptions when something goes wrong, 14 | this decorator re-raises them as MethodError exceptions 15 | 16 | Args: 17 | func: Function to decorate 18 | 19 | Returns: 20 | Decorated func 21 | 22 | Raises: 23 | MethodError: The method encountered an error 24 | 25 | Example: 26 | >>> @catch 27 | def my_method(x): 28 | assert isinstance(x, int), 'x was not an integer' 29 | 30 | 31 | >>> # Acceptable 32 | >>> my_method(1) 33 | >>> 34 | >>> # Unacceptable 35 | >>> my_method('a') 36 | MethodError: my_method() encountered an error: x was not an integer 37 | >>> 38 | ''' 39 | 40 | @functools.wraps(func) 41 | def wrapper(*args: Any, **kwargs: Any) -> Any: 42 | ''' 43 | Wrap func to re-raise exceptions as method errors. 44 | 45 | Args: 46 | *args: Function arguments 47 | **kwargs: Function keyword arguments 48 | 49 | Returns: 50 | The wrapped functions return value 51 | ''' 52 | 53 | error_message = 'Unknown' 54 | 55 | try: 56 | return func(*args, **kwargs) 57 | except Exception as error: 58 | error_message = str(error) 59 | 60 | raise exceptions.MethodError \ 61 | ( 62 | f'{func.__name__}() encountered an error: {error_message}' 63 | ) 64 | 65 | return wrapper 66 | -------------------------------------------------------------------------------- /ytm/apis/AbstractYouTubeMusic/decorators/method.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the decorator: method 3 | ''' 4 | 5 | import functools 6 | from typing import Callable, Any 7 | from .catch import catch 8 | from .... import decorators 9 | 10 | def method(parser: Callable = None) -> Callable: 11 | ''' 12 | Returns a decorator linking a method to a parser. 13 | 14 | Returns a decorator which will perform the following: 15 | * Enforce return value type 16 | * Enforce argument types 17 | * Catch and re-raise exceptions 18 | 19 | Args: 20 | parser: Method parser function 21 | 22 | Returns: 23 | Method decorator 24 | 25 | Example: 26 | >>> def my_parser(data): 27 | return data['key'] 28 | 29 | >>> 30 | >>> @method(my_parser) 31 | def my_method(x: int) -> str: 32 | return {'key': str(x)} 33 | 34 | >>> 35 | >>> # Acceptable 36 | >>> my_method(1) 37 | '1' 38 | >>> 39 | >>> # Unacceptable 40 | >>> my_method('a') 41 | MethodError: my_method() encountered an error: Expected argument 'x' to be of type 'int' not 'str' 42 | >>> 43 | ''' 44 | 45 | if not parser: 46 | parser = lambda data: data 47 | 48 | def decorator(func: Callable) -> Callable: 49 | ''' 50 | Decorate func as a method linked to a previously specified parser. 51 | 52 | Args: 53 | func: Function to decorate 54 | 55 | Returns: 56 | Decorated func 57 | ''' 58 | 59 | @decorators.enforce_return_value 60 | @decorators.parse(parser) 61 | @catch 62 | @decorators.enforce_parameters 63 | @functools.wraps(func) 64 | def wrapper(*args: Any, **kwargs: Any) -> Any: 65 | return func(*args, **kwargs) 66 | 67 | return wrapper 68 | 69 | return decorator 70 | -------------------------------------------------------------------------------- /ytm/apis/AbstractYouTubeMusic/methods/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Package containing methods. 3 | 4 | These methods interact wrap base Api functionality 5 | ''' 6 | 7 | from .... import utils as __utils 8 | 9 | __all__ = tuple(__utils.include(__spec__)) 10 | -------------------------------------------------------------------------------- /ytm/apis/AbstractYouTubeMusic/methods/_search_filter.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: _search_filter 3 | ''' 4 | 5 | from .. import decorators 6 | from .... import constants 7 | from .... import parsers 8 | from ....types import SearchContinuation 9 | 10 | @decorators.method() 11 | def _search_filter \ 12 | ( 13 | self: object, 14 | filter: str, 15 | query: str = None, 16 | continuation: SearchContinuation = None, 17 | ) -> dict: 18 | ''' 19 | Perform a filtered search. 20 | 21 | Searching can be performed using *either* a query or a continuation. 22 | Both should not be specified at the same time, and one is required. 23 | 24 | Args: 25 | self: Class Instance 26 | filter: Search filter 27 | Example: 'albums' 28 | query: Search query 29 | Example: 'nirvana' 30 | continuation: Search continuation 31 | Example: 'EpIJEgVibHVlcxqICUVnLUtBUXdJQUJBQUdBQWdBQ2dCTUFCSUt...' 32 | 33 | Returns: 34 | Filtered search data. 35 | 36 | Raises: 37 | MethodError: Method encountered an error 38 | 39 | Example: 40 | >>> api = ytm.AbstractYouTubeMusic() 41 | >>> 42 | >>> data = api._search_filter(filter = 'playlists', query = 'love') 43 | >>> 44 | >>> data['items'][0]['name'] 45 | 'Love Metal' 46 | >>> 47 | >>> # Fetch more data using the continuation token 48 | >>> more_data = api._search_filter(filter = 'playlists', continuation = data['continuation']) 49 | >>> 50 | >>> more_data['items'][0]['name'] 51 | 'Love Your Inner Goddess' 52 | >>> 53 | ''' 54 | 55 | filter = filter.strip().lower() 56 | 57 | param = constants.SEARCH_PARAMS_MAP.get(filter) 58 | 59 | assert param, f'Invalid search filter: {repr(filter)}' 60 | 61 | if query: 62 | query = query.strip() 63 | 64 | assert query, 'No search query provided' 65 | 66 | data = self._base.search \ 67 | ( 68 | query = query, 69 | params = ''.join \ 70 | ( 71 | ( 72 | constants.SEARCH_PARAM_PREFIX, 73 | param, 74 | constants.SEARCH_PARAM_SUFFIX, 75 | ), 76 | ), 77 | ) 78 | elif continuation: 79 | data = self._base.search \ 80 | ( 81 | continuation = continuation, 82 | ) 83 | else: 84 | raise Exception \ 85 | ( 86 | 'Missing 1 required argument: \'query\' or \'continuation\'' 87 | ) 88 | 89 | parsed_data = parsers._search_filter(data, filter) 90 | 91 | return parsed_data 92 | -------------------------------------------------------------------------------- /ytm/apis/AbstractYouTubeMusic/methods/album.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: album 3 | ''' 4 | 5 | from .. import decorators 6 | from .... import parsers 7 | from .... import utils 8 | from .... import types 9 | from ....types import \ 10 | ( 11 | Union, 12 | AlbumPlaylistId, 13 | AlbumPlaylistBrowseId, 14 | AlbumBrowseId, 15 | AlbumId, 16 | AlbumRadioId, 17 | AlbumShuffleId, 18 | ) 19 | 20 | @decorators.method(parsers.album) 21 | def album \ 22 | ( 23 | self: object, 24 | album_id: Union 25 | ( 26 | AlbumPlaylistId, 27 | AlbumPlaylistBrowseId, 28 | AlbumBrowseId, 29 | AlbumId, 30 | AlbumRadioId, 31 | AlbumShuffleId, 32 | ), 33 | ) -> dict: 34 | ''' 35 | Fetch Album data. 36 | 37 | Args: 38 | self: Class Instance 39 | album_id: Album Id 40 | Example: 'MPREb_qQJJUiZlXaS' 41 | 42 | Returns: 43 | Album data 44 | 45 | Raises: 46 | MethodError: Method encountered an error 47 | 48 | Example: 49 | >>> api = ytm.AbstractYouTubeMusic() 50 | >>> 51 | >>> data = api.album('MPREb_qQJJUiZlXaS') 52 | >>> 53 | >>> data['name'] 54 | 'Apricot Princess' 55 | >>> 56 | ''' 57 | 58 | if utils.isinstance(album_id, types.AlbumBrowseId): 59 | browse_id = album_id 60 | else: 61 | album_id = types.AlbumPlaylistId(album_id) 62 | 63 | page = self._base.page_playlist \ 64 | ( 65 | list = album_id, 66 | ) 67 | 68 | browse_id = utils.get \ 69 | ( 70 | page, 71 | 'INITIAL_ENDPOINT', 72 | 'browseEndpoint', 73 | 'browseId', 74 | ) 75 | 76 | browse_id = types.AlbumBrowseId(browse_id) 77 | 78 | return self._base.browse_album \ 79 | ( 80 | browse_id = browse_id, 81 | ) 82 | -------------------------------------------------------------------------------- /ytm/apis/AbstractYouTubeMusic/methods/artist.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: artist 3 | ''' 4 | 5 | from .. import decorators 6 | from .... import parsers 7 | from .... import types 8 | from ....types import \ 9 | ( 10 | Union, 11 | ArtistId, 12 | ArtistBrowseId, 13 | ) 14 | 15 | @decorators.method(parsers.artist) 16 | def artist \ 17 | ( 18 | self: object, 19 | artist_id: Union \ 20 | ( 21 | ArtistId, 22 | ArtistBrowseId, 23 | ), 24 | ) -> dict: 25 | ''' 26 | Fetch Artist data. 27 | 28 | Args: 29 | self: Class Instance 30 | artist_id: Artist Id 31 | Example: 'UCRI-Ds5eY70A4oeHggAFBbg' 32 | 33 | Returns: 34 | Artist data. 35 | 36 | Raises: 37 | MethodError: Method encountered an error 38 | 39 | Example: 40 | >>> api = ytm.AbstractYouTubeMusic() 41 | >>> 42 | >>> artist = api.artist('UCTK1maAvqrDlD2agZDGZzjw') 43 | >>> 44 | >>> artist['name'] 45 | 'Take That' 46 | >>> 47 | ''' 48 | 49 | artist_browse_id = types.ArtistBrowseId(artist_id) 50 | 51 | return self._base.browse_artist \ 52 | ( 53 | browse_id = artist_browse_id, 54 | ) 55 | -------------------------------------------------------------------------------- /ytm/apis/AbstractYouTubeMusic/methods/artist_albums.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: artist_albums 3 | ''' 4 | 5 | from .. import decorators 6 | from .... import parsers 7 | from .... import types 8 | from ....types import \ 9 | ( 10 | Union, 11 | ArtistId, 12 | ArtistBrowseId, 13 | ArtistAlbumsParams, 14 | ) 15 | 16 | @decorators.method(parsers.artist_albums) 17 | def artist_albums \ 18 | ( 19 | self: object, 20 | artist_id: Union \ 21 | ( 22 | ArtistId, 23 | ArtistBrowseId, 24 | ), 25 | params: ArtistAlbumsParams, 26 | ) -> list: 27 | ''' 28 | Fetch Artist's Albums data. 29 | 30 | These are like a continuation on the list of albums from the Artist's 31 | channel page. 32 | These can only be fetched once the Artist's data has been obtained 33 | as this will contain the 'params' required. 34 | The Artist Id should also be the one obtained by fetching the Artist's data 35 | as it may return a different Artist Id. 36 | 37 | Args: 38 | self: Class Instance 39 | artist_id: Artist Id 40 | Example: 'UCRI-Ds5eY70A4oeHggAFBbg' 41 | params: Artist Albums Params 42 | Example: '6gPUAUNwd0JDbjBLYmdBQVpXNEFBVWRDQUFGSFFnQUJBRVpGY...' 43 | 44 | Returns: 45 | Artist's Albums data. 46 | 47 | Raises: 48 | MethodError: Method encountered an error 49 | 50 | Example: 51 | >>> api = ytm.AbstractYouTubeMusic() 52 | >>> 53 | >>> # Artist data should always be fetched first 54 | >>> artist = api.artist('UCTK1maAvqrDlD2agZDGZzjw') 55 | >>> 56 | >>> artist['name'] 57 | 'Take That' 58 | >>> 59 | >>> # Important: Extract the artist_id from the data, don't reuse the original 60 | >>> artist_id = artist['id'] 61 | >>> # Extract albums params 62 | >>> params = artist['albums']['params'] 63 | >>> 64 | >>> # Fetch albums 65 | >>> albums = api.artist_albums(artist_id, params) 66 | >>> 67 | >>> albums[0]['name'] 68 | 'Wonderland (Deluxe)' 69 | >>> 70 | ''' 71 | 72 | artist_browse_id = types.ArtistBrowseId(artist_id) 73 | 74 | return self._base.browse \ 75 | ( 76 | browse_id = artist_browse_id, 77 | params = params, 78 | ) 79 | -------------------------------------------------------------------------------- /ytm/apis/AbstractYouTubeMusic/methods/artist_singles.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: artist_singles 3 | ''' 4 | 5 | from .. import decorators 6 | from .... import parsers 7 | from .... import types 8 | from ....types import \ 9 | ( 10 | Union, 11 | ArtistId, 12 | ArtistBrowseId, 13 | ArtistSinglesParams, 14 | ) 15 | 16 | @decorators.method(parsers.artist_singles) 17 | def artist_singles \ 18 | ( 19 | self: object, 20 | artist_id: Union \ 21 | ( 22 | ArtistId, 23 | ArtistBrowseId, 24 | ), 25 | params: ArtistSinglesParams, 26 | ) -> list: 27 | ''' 28 | Fetch Artist's Singles data. 29 | 30 | These are like a continuation on the list of singles from the Artist's 31 | channel page. 32 | These can only be fetched once the Artist's data has been obtained 33 | as this will contain the 'params' required. 34 | The Artist Id should also be the one obtained by fetching the Artist's data 35 | as it may return a different Artist Id. 36 | 37 | Args: 38 | self: Class Instance 39 | artist_id: Artist Id 40 | Example: 'UCRI-Ds5eY70A4oeHggAFBbg' 41 | params: Artist Singles Params 42 | Example: '6gPUAUNwd0JDbjBLYmdBQVpXNEFBVWRDQUFGSFFnQUJBRVpGYlhWemF...' 43 | 44 | Returns: 45 | Artist's Singles data. 46 | 47 | Raises: 48 | MethodError: Method encountered an error 49 | 50 | Example: 51 | >>> api = ytm.AbstractYouTubeMusic() 52 | >>> 53 | >>> # Artist data should always be fetched first 54 | >>> artist = api.artist('UCTK1maAvqrDlD2agZDGZzjw') 55 | >>> 56 | >>> artist['name'] 57 | 'Take That' 58 | >>> 59 | >>> # Important: Extract the artist_id from the data, don't reuse the original 60 | >>> artist_id = artist['id'] 61 | >>> # Extract albums params 62 | >>> params = artist['singles']['params'] 63 | >>> 64 | >>> singles = api.artist_singles(artist_id, params) 65 | >>> 66 | >>> singles[0]['name'] 67 | 'Cry (Live)' 68 | >>> 69 | ''' 70 | 71 | artist_browse_id = types.ArtistBrowseId(artist_id) 72 | 73 | return self._base.browse \ 74 | ( 75 | browse_id = artist_browse_id, 76 | params = params, 77 | ) 78 | -------------------------------------------------------------------------------- /ytm/apis/AbstractYouTubeMusic/methods/guide.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: guide 3 | ''' 4 | 5 | from .. import decorators 6 | from .... import parsers 7 | 8 | @decorators.method(parsers.guide) 9 | def guide(self: object) -> dict: 10 | ''' 11 | Fetch Guide data. 12 | 13 | Args: 14 | self: Class Instance 15 | 16 | Returns: 17 | Guide data 18 | 19 | Raises: 20 | MethodError: Method encountered an error 21 | 22 | Example: 23 | >>> api = ytm.AbstractYouTubeMusic() 24 | >>> 25 | >>> guide = api.guide() 26 | >>> 27 | >>> guide['Home'] 28 | 'FEmusic_home' 29 | >>> 30 | ''' 31 | 32 | return self._base.guide() 33 | -------------------------------------------------------------------------------- /ytm/apis/AbstractYouTubeMusic/methods/home.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: home 3 | ''' 4 | 5 | from .. import decorators 6 | from .... import parsers 7 | from ....types import HomeContinuation 8 | 9 | @decorators.method(parsers.home) 10 | def home(self: object, continuation: HomeContinuation = None) -> dict: 11 | ''' 12 | Fetch Home data. 13 | 14 | Args: 15 | self: Class Instance 16 | continuation: Home Continuation 17 | Example: '4qmFsgKIARIMRkVtdXNpY19ob21lGnhDQU42VmtOcVJVRkJSMVoxUV...' 18 | 19 | Returns: 20 | Home data 21 | 22 | Raises: 23 | MethodError: Method encountered an error 24 | 25 | Example: 26 | >>> api = ytm.AbstractYouTubeMusic() 27 | >>> 28 | >>> home = api.home() 29 | >>> 30 | >>> home['shelves'][0]['name'] 31 | 'Morning sunshine' 32 | >>> 33 | >>> # Fetch more home data 34 | >>> more_home = api.home(home['continuation']) 35 | >>> 36 | >>> more_home['shelves'][0]['name'] 37 | 'Beast mode' 38 | >>> 39 | ''' 40 | 41 | if continuation: 42 | return self._base.browse \ 43 | ( 44 | continuation = str(continuation), 45 | ) 46 | else: 47 | return self._base.browse_home() 48 | -------------------------------------------------------------------------------- /ytm/apis/AbstractYouTubeMusic/methods/hotlist.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: hotlist 3 | ''' 4 | 5 | from .. import decorators 6 | from .... import parsers 7 | 8 | @decorators.method(parsers.hotlist) 9 | def hotlist(self: object): 10 | ''' 11 | Fetch Hotlist data. 12 | 13 | Args: 14 | self: Class Instance 15 | 16 | Returns: 17 | Hotlist data. 18 | 19 | Raises: 20 | MethodError: Method encountered an error 21 | 22 | Example: 23 | >>> api = ytm.AbstractYouTubeMusic() 24 | >>> 25 | >>> hotlist = api.hotlist() 26 | >>> 27 | >>> hotlist[0]['name'] 28 | 'Rosa' 29 | >>> 30 | ''' 31 | 32 | return self._base.browse_hotlist() 33 | -------------------------------------------------------------------------------- /ytm/apis/AbstractYouTubeMusic/methods/playlist.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: playlist 3 | ''' 4 | 5 | from .. import decorators 6 | from .... import parsers 7 | from .... import types 8 | from .... import utils 9 | from ....types import \ 10 | ( 11 | Union, 12 | PlaylistId, 13 | PlaylistBrowseId, 14 | PlaylistContinuation, 15 | ) 16 | 17 | @decorators.method(parsers.playlist) 18 | def playlist \ 19 | ( 20 | self: object, 21 | playlist_id: Union \ 22 | ( 23 | PlaylistId, 24 | PlaylistBrowseId, 25 | ) = None, 26 | continuation: PlaylistContinuation = None, 27 | ) -> dict: 28 | ''' 29 | Fetch Playlist data. 30 | 31 | Args: 32 | self: Class Instance 33 | playlist_id: Playlist Id 34 | Example: 'RDCLAK5uy_nUi8B-S9ckz5feHM7oMGyQQ_eKW2Zl9aE' 35 | continuation: Playlist Continuation 36 | Example: '4qmFsgJbEi1WTFJEQ0xBSzV1eV9uVWk4Qi1TOWNrejVmZUhNN29NR3...' 37 | 38 | Returns: 39 | Playlist data. 40 | 41 | Raises: 42 | MethodError: Method encountered an error 43 | 44 | Example: 45 | >>> api = ytm.AbstractYouTubeMusic() 46 | >>> 47 | >>> playlist = api.playlist('RDCLAK5uy_nUi8B-S9ckz5feHM7oMGyQQ_eKW2Zl9aE') 48 | >>> 49 | >>> playlist['name'] 50 | '00s Sing-Alongs' 51 | >>> 52 | >>> # More playlist data (only if it has 100+ tracks) 53 | >>> more_playlist = api.playlist(continuation = playlist['continuation']) 54 | >>> 55 | >>> more_playlist['tracks'][0]['name'] 56 | 'America' 57 | >>> 58 | ''' 59 | 60 | if playlist_id is not None: 61 | if utils.isinstance(playlist_id, types.PlaylistId): 62 | playlist_browse_id = types.PlaylistBrowseId(playlist_id) 63 | else: 64 | playlist_browse_id = playlist_id 65 | 66 | return self._base.browse_playlist \ 67 | ( 68 | browse_id = playlist_browse_id, 69 | ) 70 | elif continuation is not None: 71 | return self._base.browse \ 72 | ( 73 | continuation = continuation, 74 | ) 75 | else: 76 | raise Exception \ 77 | ( 78 | 'Missing 1 required argument: \'playlist_id\' or \'continuation\'' 79 | ) 80 | -------------------------------------------------------------------------------- /ytm/apis/AbstractYouTubeMusic/methods/queue.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: queue 3 | ''' 4 | 5 | from .. import decorators 6 | from .... import parsers 7 | from ....types import \ 8 | ( 9 | Union, 10 | SongId, 11 | SongRadioId, 12 | AlbumPlaylistBrowseId, 13 | AlbumPlaylistId, 14 | AlbumRadioId, 15 | AlbumShuffleId, 16 | ArtistRadioId, 17 | ArtistShuffleId, 18 | PlaylistBrowseId, 19 | PlaylistId, 20 | PlaylistRadioId, 21 | PlaylistShuffleId, 22 | ) 23 | 24 | @decorators.method(parsers.queue) 25 | def queue \ 26 | ( 27 | self: object, 28 | *song_ids: SongId, 29 | playlist_id: Union \ 30 | ( 31 | SongRadioId, 32 | AlbumPlaylistBrowseId, 33 | AlbumPlaylistId, 34 | AlbumRadioId, 35 | AlbumShuffleId, 36 | ArtistRadioId, 37 | ArtistShuffleId, 38 | PlaylistBrowseId, 39 | PlaylistId, 40 | PlaylistRadioId, 41 | PlaylistShuffleId, 42 | ) = None, 43 | ) -> list: 44 | ''' 45 | Fetch queue data. 46 | 47 | Returns up to 200 songs. 48 | 49 | Args: 50 | self: Class Instance 51 | *song_ids: Song Id to enqueue 52 | playlist_id: Playlist Id to enqueue 53 | 54 | Returns: 55 | List of songs in the queue 56 | 57 | Raises: 58 | MethodError: Method encountered an error 59 | 60 | Example: 61 | >>> api = ytm.YouTubeMusic() 62 | >>> 63 | >>> queue = api.queue('Gz3-4UuMWjQ', 'Ye8Er8MtiLk') 64 | >>> 65 | >>> for song in queue: 66 | print(song['artist']['name'], '-', song['name']) 67 | 68 | Amber Run - Amen 69 | Amber Run - Kites 70 | >>> 71 | ''' 72 | 73 | if song_ids: 74 | return self._base.queue(*song_ids) 75 | elif playlist_id: 76 | playlist_id_map = \ 77 | { 78 | AlbumPlaylistBrowseId: AlbumPlaylistId, 79 | PlaylistBrowseId: PlaylistId, 80 | } 81 | 82 | playlist_id_type = type(playlist_id) 83 | 84 | if playlist_id_type in playlist_id_map: 85 | playlist_id = playlist_id_map[playlist_id_type](str(playlist_id)) 86 | 87 | return self._base.queue(playlist_id = playlist_id) 88 | else: 89 | raise Exception \ 90 | ( 91 | 'Missing 1 required argument: \'song_ids\' or \'playlist_id\'' 92 | ) 93 | -------------------------------------------------------------------------------- /ytm/apis/AbstractYouTubeMusic/methods/search.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: search 3 | ''' 4 | 5 | from .. import decorators 6 | from .... import parsers 7 | 8 | @decorators.method(parsers.search) 9 | def search(self: object, query: str) -> dict: 10 | ''' 11 | Fetch Search data. 12 | 13 | Args: 14 | self: Class Instance 15 | query: Search query 16 | Example: 'down by the river' 17 | 18 | Returns: 19 | Search data 20 | 21 | Raises: 22 | MethodError: Method encountered an error 23 | 24 | Example: 25 | >>> api = ytm.AbstractYouTubeMusic() 26 | >>> 27 | >>> data = api.search('down by the river') 28 | >>> 29 | >>> top_result = data['top_result'] 30 | >>> top_result 31 | 'video' 32 | >>> 33 | >>> data[top_result + 's'][0]['name'] 34 | 'Down by the River' 35 | >>> 36 | ''' 37 | 38 | return self._base.search \ 39 | ( 40 | query = query, 41 | ) 42 | -------------------------------------------------------------------------------- /ytm/apis/AbstractYouTubeMusic/methods/search_albums.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: search_albums 3 | ''' 4 | 5 | from .. import decorators 6 | from .... import constants 7 | from ....types import SearchContinuation 8 | 9 | @decorators.method() 10 | def search_albums \ 11 | ( 12 | self: object, 13 | query: str = None, 14 | continuation: SearchContinuation = None, 15 | ) -> dict: 16 | ''' 17 | Perform a search for only Albums. 18 | 19 | Args: 20 | self: Class Instance 21 | query: Search query 22 | Example: 'in utero' 23 | continuation: Search Continuation 24 | Example: 'Eo0GEghpbiB1dGVybxqABkVnLUtBUXdJQUJBQUdBRWdBQ2dBTUFCSUZ...' 25 | 26 | Returns: 27 | Albums search data 28 | 29 | Raises: 30 | MethodError: Method encountered an error 31 | 32 | Example: 33 | >>> api = ytm.AbstractYouTubeMusic() 34 | >>> 35 | >>> data = api.search_albums('in utero') 36 | >>> 37 | >>> data['items'][0]['name'] 38 | 'In Utero (20th Anniversary Remaster)' 39 | >>> 40 | >>> # Fetch more results 41 | >>> more_data = api.search_albums(continuation = data['continuation']) 42 | >>> 43 | >>> more_data['items'][0]['name'] 44 | 'Nirvana' 45 | >>> 46 | ''' 47 | 48 | return self._search_filter \ 49 | ( 50 | filter = constants.SEARCH_FILTER_ALBUMS, 51 | query = query, 52 | continuation = continuation, 53 | ) 54 | -------------------------------------------------------------------------------- /ytm/apis/AbstractYouTubeMusic/methods/search_artists.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: search_artists 3 | ''' 4 | 5 | from .. import decorators 6 | from .... import constants 7 | from ....types import SearchContinuation 8 | 9 | @decorators.method() 10 | def search_artists \ 11 | ( 12 | self: object, 13 | query: str = None, 14 | continuation: SearchContinuation = None, 15 | ) -> dict: 16 | ''' 17 | Perform a search for only Artists. 18 | 19 | Args: 20 | self: Class Instance 21 | query: Search query 22 | Example: 'bastille' 23 | continuation: Search Continuation 24 | Example: 'Eo0GEghpbiB1dGVybxqABkVnLUtBUXdJQUJBQUdBRWdBQ2dBTUFCSUZ...' 25 | 26 | Returns: 27 | Artists search data 28 | 29 | Raises: 30 | MethodError: Method encountered an error 31 | 32 | Example: 33 | >>> api = ytm.AbstractYouTubeMusic() 34 | >>> 35 | >>> data = api.search_artists('bastille') 36 | >>> 37 | >>> data['items'][0]['name'] 38 | 'Bastille' 39 | >>> 40 | >>> more_data = api.search_artists(continuation = data['continuation']) 41 | >>> 42 | >>> more_data['items'][0]['name'] 43 | 'FRENSHIP' 44 | >>> 45 | ''' 46 | 47 | return self._search_filter \ 48 | ( 49 | filter = constants.SEARCH_FILTER_ARTISTS, 50 | query = query, 51 | continuation = continuation, 52 | ) 53 | -------------------------------------------------------------------------------- /ytm/apis/AbstractYouTubeMusic/methods/search_playlists.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: search_playlists 3 | ''' 4 | 5 | from .. import decorators 6 | from .... import constants 7 | from ....types import SearchContinuation 8 | 9 | @decorators.method() 10 | def search_playlists \ 11 | ( 12 | self: object, 13 | query: str = None, 14 | continuation: SearchContinuation = None, 15 | ) -> dict: 16 | ''' 17 | Perform a search for only Playlists. 18 | 19 | Args: 20 | self: Class Instance 21 | query: Search query 22 | Example: 'love' 23 | continuation: Search Continuation 24 | Example: 'Eo0GEghpbiB1dGVybxqABkVnLUtBUXdJQUJBQUdBRWdBQ2dBTUFCSUZ...' 25 | 26 | Returns: 27 | Playlists search data 28 | 29 | Raises: 30 | MethodError: Method encountered an error 31 | 32 | Example: 33 | >>> api = ytm.AbstractYouTubeMusic() 34 | >>> 35 | >>> data = api.search_playlists('love') 36 | >>> 37 | >>> data['items'][0]['name'] 38 | 'Love Metal' 39 | >>> 40 | >>> more_data = api.search_playlists(continuation = data['continuation']) 41 | >>> 42 | >>> more_data['items'][0]['name'] 43 | 'Love Your Inner Goddess' 44 | >>> 45 | ''' 46 | 47 | return self._search_filter \ 48 | ( 49 | filter = constants.SEARCH_FILTER_PLAYLISTS, 50 | query = query, 51 | continuation = continuation, 52 | ) 53 | -------------------------------------------------------------------------------- /ytm/apis/AbstractYouTubeMusic/methods/search_songs.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: search_songs 3 | ''' 4 | 5 | from .. import decorators 6 | from .... import constants 7 | from ....types import SearchContinuation 8 | 9 | @decorators.method() 10 | def search_songs \ 11 | ( 12 | self: object, 13 | query: str = None, 14 | continuation: SearchContinuation = None, 15 | ) -> dict: 16 | ''' 17 | Perform a search for only Songs. 18 | 19 | Args: 20 | self: Class Instance 21 | query: Search query 22 | Example: 'simple song' 23 | continuation: Search Continuation 24 | Example: 'Eo0GEghpbiB1dGVybxqABkVnLUtBUXdJQUJBQUdBRWdBQ2dBTUFCSUZ...' 25 | 26 | Returns: 27 | Songs search data 28 | 29 | Raises: 30 | MethodError: Method encountered an error 31 | 32 | Example: 33 | >>> api = ytm.AbstractYouTubeMusic() 34 | >>> 35 | >>> data = api.search_songs('simple song') 36 | >>> 37 | >>> data['items'][0]['name'] 38 | 'Simple Song' 39 | >>> 40 | >>> more_data = api.search_songs(continuation = data['continuation']) 41 | >>> 42 | >>> more_data['items'][0]['name'] 43 | 'The Simple Song' 44 | >>> 45 | ''' 46 | 47 | return self._search_filter \ 48 | ( 49 | filter = constants.SEARCH_FILTER_SONGS, 50 | query = query, 51 | continuation = continuation, 52 | ) 53 | -------------------------------------------------------------------------------- /ytm/apis/AbstractYouTubeMusic/methods/search_suggestions.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: search_suggestions 3 | ''' 4 | 5 | from .. import decorators 6 | from .... import parsers 7 | 8 | @decorators.method(parsers.search_suggestions) 9 | def search_suggestions(self: object, query: str) -> list: 10 | ''' 11 | Retrieve search suggestions. 12 | 13 | Args: 14 | self: Class Instance 15 | query: Search query 16 | Example: 'imagine' 17 | 18 | Returns: 19 | List of search suggestions 20 | 21 | Raises: 22 | MethodError: Method encountered an error 23 | 24 | Example: 25 | >>> api = ytm.AbstractYouTubeMusic() 26 | >>> 27 | >>> suggestions = api.search_suggestions('imagine') 28 | >>> 29 | >>> suggestions[0] 30 | 'imagine dragons' 31 | >>> 32 | ''' 33 | 34 | return self._base.search_suggestions \ 35 | ( 36 | query = query 37 | ) 38 | -------------------------------------------------------------------------------- /ytm/apis/AbstractYouTubeMusic/methods/search_videos.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: search_videos 3 | ''' 4 | 5 | from .. import decorators 6 | from .... import constants 7 | from ....types import SearchContinuation 8 | 9 | @decorators.method() 10 | def search_videos \ 11 | ( 12 | self: object, 13 | query: str = None, 14 | continuation: SearchContinuation = None, 15 | ) -> dict: 16 | ''' 17 | Perform a search for only Videos. 18 | 19 | Args: 20 | self: Class Instance 21 | query: Search query 22 | Example: 'about a girl' 23 | continuation: Search Continuation 24 | Example: 'Eo0GEghpbiB1dGVybxqABkVnLUtBUXdJQUJBQUdBRWdBQ2dBTUFCSUZ...' 25 | 26 | Returns: 27 | Videos search data 28 | 29 | Raises: 30 | MethodError: Method encountered an error 31 | 32 | Example: 33 | >>> api = ytm.AbstractYouTubeMusic() 34 | >>> 35 | >>> data = api.search_videos('about a girl') 36 | >>> 37 | >>> data['items'][0]['name'] 38 | 'About a Girl (MTV Unplugged)' 39 | >>> 40 | >>> more_data = api.search_videos(continuation = data['continuation']) 41 | >>> 42 | >>> more_data['items'][0]['name'] 43 | 'Nirvana - About A Girl [BBC Sessions]' 44 | >>> 45 | ''' 46 | 47 | return self._search_filter \ 48 | ( 49 | filter = constants.SEARCH_FILTER_VIDEOS, 50 | query = query, 51 | continuation = continuation, 52 | ) 53 | -------------------------------------------------------------------------------- /ytm/apis/AbstractYouTubeMusic/methods/song.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: song 3 | ''' 4 | 5 | from .. import decorators 6 | from .... import parsers 7 | from ....types import SongId 8 | 9 | @decorators.method(parsers.song) 10 | def song(self: object, song_id: SongId) -> dict: 11 | ''' 12 | Fetch Song data 13 | 14 | Args: 15 | self: Class Instance 16 | song_id: Song Id 17 | Example: '3G5Conn-b2o' 18 | 19 | Returns: 20 | Song data 21 | 22 | Raises: 23 | MethodError: Method encountered an error 24 | 25 | Example: 26 | >>> api = ytm.AbstractYouTubeMusic() 27 | >>> 28 | >>> data = api.song('3G5Conn-b2o') 29 | >>> 30 | >>> data['name'] 31 | 'A Different Age' 32 | >>> 33 | ''' 34 | 35 | # Accept a SongRadioId as well and just convert it? 36 | 37 | return self._base.video_info \ 38 | ( 39 | video_id = song_id, 40 | ) 41 | -------------------------------------------------------------------------------- /ytm/apis/AbstractYouTubeMusic/methods/watch.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: watch 3 | ''' 4 | 5 | from .. import decorators 6 | from .... import parsers 7 | from .... import utils 8 | from .... import types 9 | from ....types import \ 10 | ( 11 | Union, 12 | SongId, 13 | Params, 14 | WatchContinuation, 15 | ArtistRadioId, 16 | ArtistShuffleId, 17 | PlaylistId, 18 | PlaylistBrowseId, 19 | PlaylistRadioId, 20 | PlaylistShuffleId, 21 | AlbumPlaylistId, 22 | AlbumPlaylistBrowseId, 23 | AlbumRadioId, 24 | AlbumShuffleId, 25 | SongRadioId, 26 | ) 27 | 28 | @decorators.method(parsers.watch) 29 | def watch \ 30 | ( 31 | self: object, 32 | song_id: SongId = None, 33 | playlist_id: Union \ 34 | ( 35 | ArtistRadioId, 36 | ArtistShuffleId, 37 | PlaylistId, 38 | PlaylistBrowseId, 39 | PlaylistRadioId, 40 | PlaylistShuffleId, 41 | AlbumPlaylistId, 42 | AlbumPlaylistBrowseId, 43 | AlbumRadioId, 44 | AlbumShuffleId, 45 | SongRadioId, 46 | ) = None, 47 | params: Params = None, # Is this relevant if radio and shuffle is covered? 48 | continuation: WatchContinuation = None, 49 | ) -> dict: 50 | ''' 51 | Fetch Watch data. 52 | 53 | Args: 54 | self: Class Instance 55 | song_id: Song Id 56 | Example: '0nCYgT-rVSo' 57 | playlist_id: Playlist Id 58 | Example: 'OLAK5uy_kEQJGO2SZ0k-vJ8b-F2AJLfKnw0cFydNg' 59 | params: Watch Params 60 | Example: 'wAEB' 61 | continuation: Watch Continuation 62 | Example: 'CBkSSBILLUIzOVdoVW5Pc0UiKFJEQU1QTFBMSGNkcGVKUEVJdDlRLX...' 63 | 64 | Returns: 65 | Watch data 66 | 67 | Raises: 68 | MethodError: Method encountered an error 69 | 70 | Example: 71 | >>> api = ytm.AbstractYouTubeMusic() 72 | >>> 73 | >>> data = api.watch('Rf3-KrGyw8U', 'OLAK5uy_le-wYvLezyD8DZ_n99oJJswSaobGTiGgA') 74 | >>> 75 | >>> data['tracks'][0]['name'] 76 | 'B.I.T.M.' 77 | >>> 78 | ''' 79 | 80 | type_map = \ 81 | { 82 | types.PlaylistBrowseId: types.PlaylistId, 83 | types.AlbumPlaylistBrowseId: types.AlbumPlaylistId, 84 | } 85 | 86 | for type_src, type_dst in type_map.items(): 87 | if utils.isinstance(playlist_id, type_src): 88 | playlist_id = type_dst(playlist_id) 89 | 90 | break 91 | 92 | if song_id or playlist_id: 93 | return self._base.next \ 94 | ( 95 | video_id = song_id, 96 | playlist_id = playlist_id, 97 | params = params, 98 | ) 99 | elif continuation: 100 | return self._base.next \ 101 | ( 102 | continuation = continuation, 103 | ) 104 | else: 105 | raise Exception \ 106 | ( 107 | 'Missing 1 required argument: \'playlist_id\' or \'song_id\'' 108 | ) 109 | -------------------------------------------------------------------------------- /ytm/apis/AbstractYouTubeMusic/methods/watch_radio.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: watch_radio 3 | ''' 4 | 5 | from .. import decorators 6 | from .... import constants 7 | from .... import parsers 8 | from .... import utils 9 | from .... import types 10 | from ....types import \ 11 | ( 12 | Union, 13 | ArtistRadioId, 14 | ArtistShuffleId, 15 | PlaylistId, 16 | PlaylistBrowseId, 17 | PlaylistRadioId, 18 | PlaylistShuffleId, 19 | AlbumPlaylistId, 20 | AlbumPlaylistBrowseId, 21 | AlbumRadioId, 22 | AlbumShuffleId, 23 | SongRadioId, 24 | SongId, 25 | ) 26 | 27 | @decorators.method(parsers.watch_radio) 28 | def watch_radio \ 29 | ( 30 | self: object, 31 | song_id: SongId = None, 32 | playlist_id: Union \ 33 | ( 34 | ArtistRadioId, 35 | ArtistShuffleId, 36 | PlaylistId, 37 | PlaylistBrowseId, 38 | PlaylistRadioId, 39 | PlaylistShuffleId, 40 | AlbumPlaylistId, 41 | AlbumPlaylistBrowseId, 42 | AlbumRadioId, 43 | AlbumShuffleId, 44 | SongRadioId, 45 | ) = None, 46 | ) -> dict: 47 | ''' 48 | Fetch Radio Watch data. 49 | 50 | Use the watch() method to continue data. 51 | 52 | Args: 53 | self: Class Instance 54 | song_id: Song Id 55 | Example: '0nCYgT-rVSo' 56 | playlist_id: Playlist Id 57 | Example: 'RDEM8Tjy6KJDUmM5-nJ3baglrQ' 58 | 59 | Returns: 60 | Radio Watch data 61 | 62 | Raises: 63 | MethodError: Method encountered an error 64 | 65 | Example: 66 | >>> api = ytm.AbstractYouTubeMusic() 67 | >>> 68 | >>> data = api.watch_radio(song_id = '0nCYgT-rVSo') 69 | >>> 70 | >>> data['tracks'][0]['name'] 71 | 'Cool with You' 72 | >>> 73 | ''' 74 | 75 | type_map = \ 76 | { 77 | types.ArtistRadioId: \ 78 | ( 79 | types.ArtistShuffleId, 80 | ), 81 | types.PlaylistRadioId: \ 82 | ( 83 | types.PlaylistId, 84 | types.PlaylistBrowseId, 85 | types.PlaylistShuffleId, 86 | ), 87 | types.AlbumRadioId: \ 88 | ( 89 | types.AlbumPlaylistId, 90 | types.AlbumPlaylistBrowseId, 91 | types.AlbumShuffleId, 92 | ), 93 | } 94 | 95 | if playlist_id is not None: 96 | for type_target, type_sources in type_map.items(): 97 | for type_source in type_sources: 98 | if utils.isinstance(playlist_id, type_source): 99 | playlist_id = type_target(playlist_id) 100 | 101 | break 102 | else: 103 | continue 104 | 105 | break 106 | 107 | return self._base.next \ 108 | ( 109 | playlist_id = playlist_id, 110 | params = constants.PARAMS_RADIO, 111 | ) 112 | elif song_id is not None: 113 | return self._base.next \ 114 | ( 115 | video_id = song_id, 116 | params = constants.PARAMS_RADIO_SONG, 117 | player_params = constants.PLAYER_PARAMS_RADIO_SONG, 118 | # should playlist_id be passed through as well? 119 | ) 120 | else: 121 | raise Exception \ 122 | ( 123 | 'Missing 1 required argument: \'playlist_id\' or \'song_id\'' 124 | ) 125 | -------------------------------------------------------------------------------- /ytm/apis/AbstractYouTubeMusic/methods/watch_shuffle.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: watch_shuffle 3 | ''' 4 | 5 | from .. import decorators 6 | from .... import constants 7 | from .... import parsers 8 | from .... import utils 9 | from .... import types 10 | from ....types import \ 11 | ( 12 | Union, 13 | ArtistRadioId, 14 | ArtistShuffleId, 15 | PlaylistId, 16 | PlaylistBrowseId, 17 | PlaylistRadioId, 18 | PlaylistShuffleId, 19 | AlbumPlaylistId, 20 | AlbumPlaylistBrowseId, 21 | AlbumRadioId, 22 | AlbumShuffleId, 23 | ) 24 | 25 | @decorators.method(parsers.watch_shuffle) 26 | def watch_shuffle \ 27 | ( 28 | self: object, 29 | playlist_id: Union \ 30 | ( 31 | ArtistRadioId, 32 | ArtistShuffleId, 33 | PlaylistId, 34 | PlaylistBrowseId, 35 | PlaylistRadioId, 36 | PlaylistShuffleId, 37 | AlbumPlaylistId, 38 | AlbumPlaylistBrowseId, 39 | AlbumRadioId, 40 | AlbumShuffleId, 41 | ), 42 | ) -> dict: 43 | ''' 44 | Fetch Shuffle Watch data. 45 | 46 | Use the watch() method to continue data. 47 | 48 | Args: 49 | self: Class Instance 50 | playlist_id: Playlist Id 51 | Example: 'RDAODz952MrmDXLULMlo7SEXUw' 52 | song_id: Song Id 53 | Example: 'gyOF8CopyGo' 54 | 55 | Returns: 56 | Shuffle Watch data 57 | 58 | Raises: 59 | MethodError: Method encountered an error 60 | 61 | Example: 62 | >>> api = ytm.AbstractYouTubeMusic() 63 | >>> 64 | >>> data = api.watch_shuffle('OLAK5uy_kEQJGO2SZ0k-vJ8b-F2AJLfKnw0cFydNg') 65 | >>> 66 | >>> data['tracks'][0]['name'] 67 | 'Come as You Are' 68 | >>> 69 | ''' 70 | 71 | type_map = \ 72 | { 73 | types.ArtistShuffleId: \ 74 | ( 75 | types.ArtistRadioId, 76 | ), 77 | types.PlaylistShuffleId: \ 78 | ( 79 | types.PlaylistId, 80 | types.PlaylistBrowseId, 81 | types.PlaylistRadioId, 82 | ), 83 | types.AlbumShuffleId: \ 84 | ( 85 | types.AlbumPlaylistId, 86 | types.AlbumPlaylistBrowseId, 87 | types.AlbumRadioId, 88 | ), 89 | } 90 | 91 | for type_target, type_sources in type_map.items(): 92 | for type_source in type_sources: 93 | if utils.isinstance(playlist_id, type_source): 94 | playlist_id = type_target(playlist_id) 95 | 96 | break 97 | else: 98 | continue 99 | 100 | break 101 | 102 | return self._base.next \ 103 | ( 104 | playlist_id = playlist_id, 105 | params = constants.PARAMS_SHUFFLE, 106 | ) 107 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Package containing the Api class: BaseYouTubeMusic 3 | ''' 4 | 5 | from .BaseYouTubeMusic import BaseYouTubeMusic 6 | 7 | __all__ = (BaseYouTubeMusic.__name__,) 8 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/constants/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Package containing constants. 3 | 4 | These constants help the super package achieve general tasks. 5 | ''' 6 | 7 | from ....constants import * 8 | 9 | # Headers 10 | # HEADER_VISITOR_ID = 'CgtqQXdSZDY3c29hSSiPsr_uBQ%3D%3D' 11 | # HEADER_DO_NOT_TRACK = '1' 12 | # HEADER_TRAILERS = 'Trailers' 13 | 14 | # User Agents 15 | USER_AGENTS = \ 16 | ( 17 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0', 18 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36', 19 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.18363', 20 | ) 21 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/decorators/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Package containing decorators. 3 | 4 | These decorators range in purpose, but help the super package 5 | achieve general tasks 6 | ''' 7 | 8 | from .... import utils as __utils 9 | import types as __types 10 | 11 | __all__ = tuple \ 12 | ( 13 | __utils.include \ 14 | ( 15 | __spec__, 16 | lambda object: not isinstance(object, __types.ModuleType) \ 17 | and not object.__name__.startswith('_'), 18 | ), 19 | ) 20 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/decorators/catch.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the decorator: catch 3 | ''' 4 | 5 | import functools 6 | import requests.exceptions 7 | import urllib3.exceptions 8 | from .... import exceptions 9 | 10 | def catch(func): 11 | ''' 12 | Catch method errors and re-raise them appropriately. 13 | 14 | Methods can raise exceptions when something goes wrong, 15 | this decorator re-raises them. 16 | 17 | Args: 18 | func: Function to decorate 19 | 20 | Returns: 21 | Decorated func 22 | 23 | Raises: 24 | ConnectionError: Failed to connect to host 25 | YouTubeMusicApiError: YouTube Music Api Error 26 | YouTubeApiError: YouTube Api Error 27 | MethodError: Method encountered an error 28 | 29 | Example: 30 | >>> @catch 31 | def foo(): 32 | raise Exception('An error occured!') 33 | 34 | >>> foo() 35 | MethodError: An error occured! 36 | >>> 37 | ''' 38 | 39 | @functools.wraps(func) 40 | def wrapper(*args, **kwargs): 41 | ''' 42 | Wrap func to re-raise exceptions. 43 | 44 | Args: 45 | *args: Function arguments 46 | **kwargs: Function keyword arguments 47 | 48 | Returns: 49 | The wrapped functions return value 50 | ''' 51 | 52 | exception = None 53 | exception_message = None 54 | 55 | try: 56 | resp = func(*args, **kwargs) 57 | except requests.exceptions.ConnectionError as error: 58 | argument = error.args[0] 59 | 60 | if isinstance(argument, str): 61 | exception_message = argument 62 | elif isinstance(argument, urllib3.exceptions.MaxRetryError): 63 | error_name = argument.__class__.__name__ 64 | 65 | pool = argument.pool 66 | reason = argument.reason 67 | endpoint = argument.url 68 | 69 | host = pool.host 70 | port = pool.port 71 | protocol = pool.scheme 72 | 73 | reason_message = reason.args[0].split(':', 2)[1].strip() 74 | 75 | url = f'{protocol}://{host}:{port}{endpoint}' 76 | 77 | exception_message = f'{error_name} - {reason_message} ({repr(url)})' 78 | elif isinstance(argument, urllib3.exceptions.ProtocolError): 79 | error_name = argument.__class__.__name__ 80 | error_message = argument.args[0] 81 | 82 | exception_message = f'{error_name} - {error_message}' 83 | else: 84 | exception_message = str(error) 85 | 86 | exception = exceptions.ConnectionError 87 | except TimeoutError as error: 88 | error_name = error.__class__.__name__ 89 | 90 | error_number = error.errno 91 | error_message = error.strerror 92 | 93 | exception_message = f'{error_name} - [{error_number}] {error_message}' 94 | 95 | exception = exceptions.ConnectionError 96 | except Exception as error: 97 | exception_message = str(error) 98 | 99 | exception = exceptions.MethodError 100 | 101 | if exception: 102 | raise exception(exception_message) 103 | 104 | if isinstance(resp, dict): 105 | if 'error' in resp: 106 | raise exceptions.YouTubeMusicApiError(resp) 107 | if 'errorcode' in resp: 108 | raise exceptions.YouTubeApiError(resp) 109 | 110 | return resp 111 | 112 | return wrapper 113 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/methods/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Package containing methods. 3 | ''' 4 | 5 | from ....utils import include as __include 6 | 7 | __all__ = tuple(__include(__spec__)) 8 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/methods/_get_page.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: _get_page 3 | ''' 4 | 5 | import re 6 | import json 7 | from .. import utils 8 | from .. import constants 9 | from .. import decorators 10 | from .... import exceptions 11 | 12 | @decorators.catch 13 | def _get_page(self: object, *endpoints: str, params: dict = None) -> dict: 14 | ''' 15 | Return a page's configuration data. 16 | 17 | Fetches the page at https://music.youtube.com/{endpoint} and 18 | extracts the configuration dictionary 19 | 20 | Args: 21 | self: Class instance 22 | endpoints: Page endpoints. Get joined by '/' 23 | Example: 'hotlist', 'playlist' 24 | params: Params to be used for the GET request 25 | Example: {'q': 'search query'} 26 | 27 | Returns: 28 | Page configuration dictionary 29 | 30 | Example: 31 | >>> api = ytm.BaseYouTubeMusic() 32 | >>> 33 | >>> data = api._get_page \ 34 | ( 35 | 'search', 36 | params = {'q': 'foo fighters'}, 37 | ) 38 | >>> 39 | >>> data.get('YTFE_BUILD_TIMESTAMP') 40 | 'Thu Apr 16 21:13:20 2020 (1587096800)' 41 | >>> 42 | 43 | Raises: 44 | InvalidPageConfigurationError: Page has no configuration data 45 | PageNotFoundError: Page not found 46 | ''' 47 | 48 | endpoint = '/'.join(map(str.strip, endpoints)) 49 | 50 | url = self._url(endpoint) 51 | 52 | resp = self._session.get \ 53 | ( 54 | url = url, 55 | params = params, 56 | ) 57 | 58 | config_match = re.search \ 59 | ( 60 | pattern = r'ytcfg\.set\((?P.*)\);', 61 | string = resp.text, 62 | ) 63 | 64 | if config_match is None: 65 | raise exceptions.InvalidPageConfigurationError \ 66 | ( 67 | 'Page has no configuration data', 68 | ) 69 | 70 | config_data = config_match.group('data') 71 | config = json.loads(config_data) 72 | 73 | for key, val in config.items(): 74 | if isinstance(val, str): 75 | val = val.strip() 76 | 77 | json_match = re.match \ 78 | ( 79 | pattern = r'\{(.*)\}', 80 | string = val, 81 | ) 82 | 83 | if json_match is not None: 84 | config[key] = json.loads(val) 85 | 86 | initial_browse_id = utils.get \ 87 | ( 88 | config, 89 | 'INITIAL_ENDPOINT', 90 | 'browseEndpoint', 91 | 'browseId', 92 | ) 93 | 94 | if endpoint and initial_browse_id == constants.BROWSE_ID_HOME: 95 | raise exceptions.PageNotFoundError \ 96 | ( 97 | 'Page not found', 98 | ) 99 | 100 | return config 101 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/methods/_url.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: _url 3 | ''' 4 | 5 | from .. import utils 6 | 7 | @staticmethod 8 | def _url(*endpoints: str, params: dict = None) -> str: 9 | ''' 10 | Generate a YouTube Music URL. 11 | 12 | Args: 13 | *endpoints: Url endpoints 14 | params: Url query-string parameters 15 | 16 | Returns: 17 | YouTube Music URL 18 | 19 | Example: 20 | >>> api = ytm.BaseYouTubeMusic() 21 | >>> 22 | >>> api._url('watch', params = {'v': 'KuVt9Cnbhwg'}) 23 | 'https://music.youtube.com/watch?v=KuVt9Cnbhwg' 24 | >>> 25 | ''' 26 | 27 | return utils.url_ytm \ 28 | ( 29 | *endpoints, 30 | params = params, 31 | ) 32 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/methods/_url_api.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: _url_api 3 | ''' 4 | 5 | from .. import utils 6 | from .. import constants 7 | 8 | @staticmethod 9 | def _url_api(*endpoints: str, params: dict = None) -> str: 10 | ''' 11 | Generate a YouTube Music Api URL. 12 | 13 | Args: 14 | *endpoints: Url endpoints 15 | params: Url query-string parameters 16 | 17 | Returns: 18 | YouTube Music Api URL 19 | 20 | Example: 21 | >>> api = ytm.BaseYouTubeMusic() 22 | >>> 23 | >>> api._url_api('music', 'get_search_suggestions') 24 | 'https://music.youtube.com/youtubei/v1/music/get_search_suggestions' 25 | >>> 26 | ''' 27 | 28 | return utils.url_ytm \ 29 | ( 30 | constants.ENDPOINT_YTM_API, 31 | *endpoints, 32 | params = params, 33 | ) 34 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/methods/_url_yt.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: _url_yt 3 | ''' 4 | 5 | from .. import utils 6 | 7 | @staticmethod 8 | def _url_yt(*endpoints: str, params: dict = None) -> str: 9 | ''' 10 | Generate a YouTube URL. 11 | 12 | Args: 13 | *endpoints: Url endpoints 14 | params: Url query-string parameters 15 | 16 | Returns: 17 | YouTube URL 18 | 19 | Example: 20 | >>> api = ytm.BaseYouTubeMusic() 21 | >>> 22 | >>> api._url_yt('get_video_info', params = {'video_id': 'KuVt9Cnbhwg'}) 23 | 'https://www.youtube.com/get_video_info?video_id=KuVt9Cnbhwg' 24 | >>> 25 | ''' 26 | 27 | return utils.url_yt \ 28 | ( 29 | *endpoints, 30 | params = params, 31 | ) 32 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/methods/browse.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: browse 3 | ''' 4 | 5 | import copy 6 | from .. import constants 7 | from .. import decorators 8 | 9 | @decorators.catch 10 | def browse \ 11 | ( 12 | self: object, 13 | browse_id: str = None, 14 | page_type: str = None, 15 | continuation: str = None, 16 | params: str = None, 17 | ) -> dict: 18 | ''' 19 | Return browse data. 20 | 21 | Args: 22 | self: Class instance 23 | browse_id: Browse id 24 | Example: 'MPREb_GFKho1A5P3G' 25 | page_type: Page type 26 | Example: 'MUSIC_PAGE_TYPE_ALBUM' 27 | continuation: Continuation 28 | Example: '4qmFsgKMARIMRkVtdXNpY19ob2...' 29 | params: Params 30 | Example: 'wAEB' 31 | 32 | Returns: 33 | Browse data 34 | 35 | Example: 36 | >>> api = ytm.BaseYouTubeMusic() 37 | >>> 38 | >>> data = api.browse \ 39 | ( 40 | browse_id = 'VLPL4fGSI1pDJn5kI81J1fYWK5eZRl1zJ5kM', 41 | page_type = 'MUSIC_PAGE_TYPE_PLAYLIST', 42 | ) 43 | >>> 44 | >>> data['header']['musicDetailHeaderRenderer']['title'] 45 | {'runs': [{'text': 'Top 100 Music Videos Global'}]} 46 | >>> 47 | ''' 48 | 49 | url = self._url_api(constants.ENDPOINT_YTM_API_BROWSE) 50 | 51 | url_params = copy.deepcopy(self._params) 52 | payload = copy.deepcopy(self._payload) 53 | 54 | if continuation: 55 | url_params['continuation'] = continuation 56 | url_params['ctoken'] = continuation 57 | 58 | if browse_id: 59 | payload['browseId'] = browse_id 60 | 61 | if params: 62 | payload['params'] = params 63 | 64 | if page_type: 65 | payload['browseEndpointContextSupportedConfigs'] = \ 66 | { 67 | 'browseEndpointContextMusicConfig': \ 68 | { 69 | 'pageType': page_type, 70 | } 71 | } 72 | 73 | resp = self._session.post \ 74 | ( 75 | url = url, 76 | params = url_params, 77 | json = payload, 78 | ) 79 | 80 | data = resp.json() 81 | 82 | return data 83 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/methods/browse_album.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: browse_album 3 | ''' 4 | 5 | def browse_album(self: object, browse_id: str) -> dict: 6 | ''' 7 | Return browse data for: Album. 8 | 9 | See help for the 'browse' method for more information 10 | 11 | Args: 12 | self: Class instance 13 | browse_id: Album browse id 14 | Example: 'MPREb_GFKho1A5P3G' 15 | 16 | Returns: 17 | Album browse data 18 | 19 | Example: 20 | >>> api = ytm.BaseYouTubeMusic() 21 | >>> 22 | >>> data = api.browse_album('MPREb_GFKho1A5P3G') 23 | >>> 24 | >>> data.keys() 25 | dict_keys(['responseContext', 'contents', 'header', ...]) 26 | >>> 27 | ''' 28 | 29 | return self.browse \ 30 | ( 31 | browse_id = browse_id, 32 | ) 33 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/methods/browse_artist.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: browse_artist 3 | ''' 4 | 5 | def browse_artist(self: object, browse_id: str) -> dict: 6 | ''' 7 | Return browse data for: Artist. 8 | 9 | See help for the 'browse' method for more information 10 | 11 | Args: 12 | self: Class instance 13 | browse_id: Artist browse id 14 | Example: 'UC8Yu1_yfN5qPh601Y4btsYw' 15 | 16 | Returns: 17 | Artist browse data 18 | 19 | Example: 20 | >>> api = ytm.BaseYouTubeMusic() 21 | >>> 22 | >>> data = api.browse_artist('UC8Yu1_yfN5qPh601Y4btsYw') 23 | >>> 24 | >>> data['header']['musicImmersiveHeaderRenderer']['title'] 25 | {'runs': [{'text': 'Arctic Monkeys'}]} 26 | >>> 27 | ''' 28 | 29 | return self.browse \ 30 | ( 31 | browse_id = browse_id, 32 | ) 33 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/methods/browse_home.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: browse_home 3 | ''' 4 | 5 | from .. import constants 6 | 7 | def browse_home(self: object) -> dict: 8 | ''' 9 | Return browse data for: Home. 10 | 11 | See help for the 'browse' method for more information 12 | 13 | Args: 14 | self: Class instance 15 | 16 | Returns: 17 | Home browse data 18 | 19 | Example: 20 | >>> api = ytm.BaseYouTubeMusic() 21 | >>> 22 | >>> data = api.browse_home() 23 | >>> 24 | >>> tabs = data['contents']['singleColumnBrowseResultsRenderer']['tabs'] 25 | >>> tab = tabs[0]['tabRenderer'] 26 | >>> 27 | >>> tab['title'] 28 | 'Home' 29 | >>> 30 | ''' 31 | 32 | return self.browse \ 33 | ( 34 | browse_id = constants.BROWSE_ID_HOME, 35 | ) 36 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/methods/browse_hotlist.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: browse_hotlist 3 | ''' 4 | 5 | from .. import constants 6 | 7 | def browse_hotlist(self: object) -> dict: 8 | ''' 9 | Return browse data for: Hotlist. 10 | 11 | See help for the 'browse' method for more information 12 | 13 | Args: 14 | self: Class instance 15 | 16 | Returns: 17 | Hotlist browse data 18 | 19 | Example: 20 | >>> api = ytm.BaseYouTubeMusic() 21 | >>> 22 | >>> tabs = data['contents']['singleColumnBrowseResultsRenderer']['tabs'] 23 | >>> tab = tabs[0]['tabRenderer'] 24 | >>> 25 | >>> tab['title'] 26 | 'Hotlist' 27 | >>> 28 | ''' 29 | 30 | return self.browse \ 31 | ( 32 | browse_id = constants.BROWSE_ID_HOTLIST, 33 | ) 34 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/methods/browse_playlist.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: browse_playlist 3 | ''' 4 | 5 | def browse_playlist(self: object, browse_id: str) -> dict: 6 | ''' 7 | Return browse data for: Playlist. 8 | 9 | See help for the 'browse' method for more information 10 | 11 | Args: 12 | self: Class instance 13 | browse_id: Playlist browse id 14 | Example: 'VLPL4fGSI1pDJn5kI81J1fYWK5eZRl1zJ5kM' 15 | 16 | Returns: 17 | Playlist browse data 18 | 19 | Example: 20 | >>> api = ytm.BaseYouTubeMusic() 21 | >>> 22 | >>> data = api.browse_playlist('VLPL4fGSI1pDJn5kI81J1fYWK5eZRl1zJ5kM') 23 | >>> 24 | >>> data['header']['musicDetailHeaderRenderer']['title'] 25 | {'runs': [{'text': 'Top 100 Music Videos Global'}]} 26 | >>> 27 | ''' 28 | 29 | return self.browse \ 30 | ( 31 | browse_id = browse_id, 32 | ) 33 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/methods/guide.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: guide 3 | ''' 4 | 5 | from .. import constants 6 | from .. import decorators 7 | 8 | @decorators.catch 9 | def guide(self: object) -> dict: 10 | ''' 11 | Return guide data. 12 | 13 | Guide data returns tab browse id's 14 | 15 | Args: 16 | self: Class instance 17 | 18 | Returns: 19 | Guide data 20 | 21 | Example: 22 | >>> api = ytm.BaseYouTubeMusic() 23 | >>> 24 | >>> data = api.guide() 25 | >>> 26 | >>> pivot_items = data['items'][0]['pivotBarRenderer']['items'] 27 | >>> 28 | >>> for pivot_item in pivot_items: 29 | pivot_item = pivot_item['pivotBarItemRenderer'] 30 | print(pivot_item['pivotIdentifier']) 31 | 32 | FEmusic_home 33 | FEmusic_trending 34 | FEmusic_liked 35 | >>> 36 | ''' 37 | 38 | resp = self._session.post \ 39 | ( 40 | url = self._url_api(constants.ENDPOINT_YTM_API_GUIDE), 41 | params = self._params, 42 | json = self._payload, 43 | ) 44 | 45 | data = resp.json() 46 | 47 | return data 48 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/methods/next.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: next 3 | ''' 4 | 5 | import copy 6 | from .. import constants 7 | from .. import decorators 8 | 9 | @decorators.catch 10 | def next \ 11 | ( 12 | self: object, 13 | video_id: str = None, 14 | playlist_id: str = None, 15 | index: int = None, 16 | music_video_type: str = None, 17 | params: str = None, 18 | tuner_setting_value: str = None, 19 | player_params: str = None, 20 | continuation: str = None, 21 | ) -> dict: 22 | ''' 23 | Return next data. 24 | 25 | Next data is used when listening to a song/playlist and returns the 26 | tracks in the queue 27 | 28 | Args: 29 | self: Class instance 30 | video_id: Video Id 31 | Example: '-yDWjtrgkb0' 32 | playlist_id: Playlist Id 33 | Example: 'PL4fGSI1pDJn5kI81J1fYWK5eZRl1zJ5kM' 34 | Note: This is *not* a browse id 35 | index: Video index 36 | Example: 1 37 | Note: This is zero-based 38 | music_video_type: Music video type 39 | Example: 'MUSIC_VIDEO_TYPE_OMV' 40 | params: Params 41 | Example: 'OAHyAQIIAQ%3D%3D' 42 | tuner_setting_value: Tuner settings value 43 | Example: 'AUTOMIX_SETTING_NORMAL' 44 | player_params: Player params 45 | Example: 'igMDCNgE' 46 | continuation: Continuation 47 | Example: 'CDISPBILdUxIcXBqVzNhRHMiIlBMNGZHU0kxcERKbjVrSTgxSjFmWV...' 48 | 49 | Returns: 50 | Next data 51 | 52 | Example: 53 | >>> api = ytm.BaseYouTubeMusic() 54 | >>> 55 | >>> data = api.next \ 56 | ( 57 | video_id = 'l0U7SxXHkPY', 58 | playlist_id = 'PL4fGSI1pDJn5kI81J1fYWK5eZRl1zJ5kM', 59 | tuner_setting_value = 'AUTOMIX_SETTING_NORMAL', 60 | music_video_type = 'MUSIC_VIDEO_TYPE_OMV', 61 | ) 62 | >>> 63 | >>> data['currentVideoEndpoint']['watchEndpoint'] 64 | {'videoId': 'l0U7SxXHkPY', 'playlistId': 'PL4fGSI1pDJn5kI81J1fYWK5eZRl1zJ5kM', 'index': 0} 65 | >>> 66 | ''' 67 | 68 | url = self._url_api(constants.ENDPOINT_YTM_API_NEXT) 69 | 70 | url_params = copy.deepcopy(self._params) 71 | payload = copy.deepcopy(self._payload) 72 | 73 | payload.update \ 74 | ( 75 | { 76 | 'enablePersistentPlaylistPanel': True, 77 | 'isAudioOnly': True, 78 | 'params': params or constants.PARAMS_WATCH, 79 | 'tunerSettingValue': tuner_setting_value or constants.AUTOMIX_SETTING_NORMAL, 80 | } 81 | ) 82 | 83 | if playlist_id: 84 | payload['playlistId'] = playlist_id 85 | 86 | if video_id: 87 | payload.update \ 88 | ( 89 | { 90 | 'videoId': video_id, 91 | 'watchEndpointMusicSupportedConfigs': \ 92 | { 93 | 'watchEndpointMusicConfig': \ 94 | { 95 | 'hasPersistentPlaylistPanel': True, 96 | 'musicVideoType': music_video_type or constants.MUSIC_VIDEO_TYPE_OMV, 97 | }, 98 | }, 99 | } 100 | ) 101 | 102 | if index: 103 | payload['index'] = index 104 | 105 | if player_params: 106 | payload['playerParams'] = player_params 107 | 108 | if continuation: 109 | payload['continuation'] = continuation 110 | 111 | resp = self._session.post \ 112 | ( 113 | url = url, 114 | params = url_params, 115 | json = payload, 116 | ) 117 | 118 | data = resp.json() 119 | 120 | return data 121 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/methods/page_channel.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: page_channel 3 | ''' 4 | 5 | from .. import constants 6 | 7 | def page_channel(self: object, channel: str) -> dict: 8 | ''' 9 | Return page configuration data for: Channel. 10 | 11 | Page configuration data will contain player information and the initial 12 | endpoint 13 | 14 | Args: 15 | self: Class instance 16 | channel: Channel Id 17 | Example: 'UCN8aYfV4Em0pc0hxVXBTA-A' 18 | 19 | Returns: 20 | Channel page configuration data 21 | 22 | Example: 23 | >>> api = ytm.BaseYouTubeMusic() 24 | >>> 25 | >>> data = api.page_channel('UCN8aYfV4Em0pc0hxVXBTA-A') 26 | >>> 27 | >>> data['INITIAL_ENDPOINT']['browseEndpoint']['browseId'] 28 | 'UCN8aYfV4Em0pc0hxVXBTA-A' 29 | >>> 30 | ''' 31 | 32 | return self._get_page \ 33 | ( 34 | constants.ENDPOINT_YTM_CHANNEL, 35 | channel, 36 | ) 37 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/methods/page_home.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: page_home 3 | ''' 4 | 5 | from .. import constants 6 | 7 | def page_home(self: object) -> dict: 8 | ''' 9 | Return page configuration data for: Home. 10 | 11 | Page configuration data will contain player information and the initial 12 | endpoint 13 | 14 | Args: 15 | self: Class instance 16 | 17 | Returns: 18 | Home page configuration data 19 | 20 | Example: 21 | >>> api = ytm.BaseYouTubeMusic() 22 | >>> 23 | >>> data = api.page_home() 24 | >>> 25 | >>> data['INITIAL_ENDPOINT']['browseEndpoint']['browseId'] 26 | 'FEmusic_home' 27 | >>> 28 | ''' 29 | 30 | return self._get_page \ 31 | ( 32 | constants.ENDPOINT_YTM_HOME, 33 | ) 34 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/methods/page_hotlist.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: page_hotlist 3 | ''' 4 | 5 | from .. import constants 6 | 7 | def page_hotlist(self: object) -> dict: 8 | ''' 9 | Return page configuration data for: Hotlist. 10 | 11 | Page configuration data will contain player information and the initial 12 | endpoint 13 | 14 | Args: 15 | self: Class instance 16 | 17 | Returns: 18 | Hotlist page configuration data 19 | 20 | Example: 21 | >>> api = ytm.BaseYouTubeMusic() 22 | >>> 23 | >>> data = api.page_hotlist() 24 | >>> 25 | >>> data['INITIAL_ENDPOINT']['browseEndpoint']['browseId'] 26 | 'FEmusic_trending' 27 | >>> 28 | ''' 29 | 30 | return self._get_page \ 31 | ( 32 | constants.ENDPOINT_YTM_HOTLIST, 33 | ) 34 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/methods/page_playlist.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: page_playlist 3 | ''' 4 | 5 | from .. import constants 6 | 7 | def page_playlist(self: object, list: str) -> dict: 8 | ''' 9 | Return page configuration data for: Playlist. 10 | 11 | Page configuration data will contain player information and the initial 12 | endpoint 13 | 14 | Args: 15 | self: Class instance 16 | list: Playlist list id 17 | Example: 'RDCLAK5uy_ktv79aJ_zAi049KdFWFaAZEfgnm5jNZpk' 18 | 19 | Returns: 20 | Playlist page configuration data 21 | 22 | Example: 23 | >>> api = ytm.BaseYouTubeMusic() 24 | >>> 25 | >>> data = api.page_playlist('RDCLAK5uy_ktv79aJ_zAi049KdFWFaAZEfgnm5jNZpk') 26 | >>> 27 | >>> data['INITIAL_ENDPOINT']['browseEndpoint']['browseId'] 28 | 'VLRDCLAK5uy_ktv79aJ_zAi049KdFWFaAZEfgnm5jNZpk' 29 | >>> 30 | ''' 31 | 32 | return self._get_page \ 33 | ( 34 | constants.ENDPOINT_YTM_PLAYLIST, 35 | params = \ 36 | { 37 | 'list': list, 38 | }, 39 | ) 40 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/methods/page_search.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: page_search 3 | ''' 4 | 5 | from .. import constants 6 | 7 | def page_search(self: object, q: str) -> dict: 8 | ''' 9 | Return page configuration data for: Search. 10 | 11 | Page configuration data will contain player information and the initial 12 | endpoint 13 | 14 | Args: 15 | self: Class instance 16 | q: Search query 17 | Example: 'foo fighters' 18 | 19 | Returns: 20 | Search page configuration data 21 | 22 | Example: 23 | >>> api = ytm.BaseYouTubeMusic() 24 | >>> 25 | >>> data = api.page_search('foo fighters') 26 | >>> 27 | >>> data['INITIAL_ENDPOINT']['searchEndpoint'] 28 | {'query': 'foo fighters'} 29 | >>> 30 | ''' 31 | 32 | return self._get_page \ 33 | ( 34 | constants.ENDPOINT_YTM_SEARCH, 35 | params = \ 36 | { 37 | 'q': q, 38 | }, 39 | ) 40 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/methods/page_watch.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: page_watch 3 | ''' 4 | 5 | from .. import constants 6 | from .. import utils 7 | 8 | def page_watch(self: object, v: str, list: str = None) -> dict: 9 | ''' 10 | Return page configuration data for: Watch. 11 | 12 | Page configuration data will contain player information and the initial 13 | endpoint 14 | 15 | Args: 16 | self: Class instance 17 | v: Video Id 18 | Example: 'QtXby3twMmI' 19 | list: Playlist list id 20 | Example: 'RDCLAK5uy_ktv79aJ_zAi049KdFWFaAZEfgnm5jNZpk' 21 | 22 | Returns: 23 | Watch page configuration data 24 | 25 | Example: 26 | >>> api = ytm.BaseYouTubeMusic() 27 | >>> 28 | >>> data = api.page_watch \ 29 | ( 30 | v = 'QtXby3twMmI', 31 | list = 'RDCLAK5uy_ktv79aJ_zAi049KdFWFaAZEfgnm5jNZpk', 32 | ) 33 | >>> 34 | >>> data['INITIAL_ENDPOINT']['watchEndpoint'] 35 | {'videoId': 'QtXby3twMmI', 'playlistId': 'RDCLAK5uy_ktv...} 36 | >>> 37 | ''' 38 | 39 | return self._get_page \ 40 | ( 41 | constants.ENDPOINT_YTM_WATCH, 42 | params = utils.filter \ 43 | ( 44 | { 45 | 'v': v, 46 | 'list': list, 47 | } 48 | ), 49 | ) 50 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/methods/queue.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: queue 3 | ''' 4 | 5 | from .. import constants 6 | 7 | import copy 8 | 9 | def queue(self: object, *video_ids: str, playlist_id: str = None) -> dict: 10 | ''' 11 | Return queue data. 12 | 13 | Enqueues the specified video_id's or playlist_id and returns their songs data. 14 | The playlist_id argument should not be a browse id. 15 | 16 | Args: 17 | self: Class instance 18 | *video_ids: Video Ids to enqueue 19 | playlist_video: Playlist Id to enqueue 20 | 21 | Returns: 22 | Queue data 23 | 24 | Example: 25 | >>> api = ytm.BaseYouTubeMusic() 26 | >>> 27 | >>> data = api.queue('Gz3-4UuMWjQ', 'Ye8Er8MtiLk') 28 | >>> 29 | >>> for song in data['queueDatas']: 30 | song = song['content']['playlistPanelVideoRenderer'] 31 | 32 | print(song['title']) 33 | 34 | {'runs': [{'text': 'Amen'}]} 35 | {'runs': [{'text': 'Kites'}]} 36 | >>> 37 | ''' 38 | 39 | url_params = copy.deepcopy(self._params) 40 | payload = copy.deepcopy(self._payload) 41 | 42 | if playlist_id: 43 | payload['playlistId'] = playlist_id 44 | 45 | video_ids = (None,) 46 | 47 | payload['videoIds'] = video_ids 48 | 49 | resp = self._session.post \ 50 | ( 51 | url = self._url_api(constants.ENDPOINT_YTM_API_GET_QUEUE), 52 | params = url_params, 53 | json = payload, 54 | ) 55 | 56 | data = resp.json() 57 | 58 | return data 59 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/methods/search.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: search 3 | ''' 4 | 5 | import copy 6 | from .. import constants 7 | from .. import decorators 8 | 9 | @decorators.catch 10 | def search \ 11 | ( 12 | self: object, 13 | query: str = None, 14 | params: str = None, 15 | continuation: str = None, 16 | ) -> dict: 17 | ''' 18 | Return search data. 19 | 20 | Returns results for a search 21 | 22 | Args: 23 | self: Class instance 24 | query: Search query string 25 | Example: 'coldplay' 26 | params: Search params 27 | Example: 'Eg-KAQwIABAAGAAgASgAMABqChAKEAMQCRAEEAU%3D' 28 | Note: These are used to filter search results, e.g. only artists 29 | continuation: Search continuation 30 | Example: 'Eo0GEghjb2xkcGxheRqABkVnLUtBUXdJQUJBQUdBQWdBU2d...' 31 | 32 | Returns: 33 | Search data 34 | 35 | Example: 36 | >>> api = ytm.BaseYouTubeMusic() 37 | >>> 38 | >>> data = api.search('foo f') 39 | >>> 40 | >>> shelves = data['contents']['sectionListRenderer']['contents'] 41 | >>> 42 | >>> for shelf in shelves: 43 | shelf = shelf['musicShelfRenderer'] 44 | print(shelf['title']) 45 | 46 | {'runs': [{'text': 'Top result'}]} 47 | {'runs': [{'text': 'Songs'}]} 48 | {'runs': [{'text': 'Albums'}]} 49 | {'runs': [{'text': 'Videos'}]} 50 | {'runs': [{'text': 'Playlists'}]} 51 | {'runs': [{'text': 'Artists'}]} 52 | >>> 53 | ''' 54 | 55 | url = self._url_api(constants.ENDPOINT_YTM_API_SEARCH) 56 | 57 | url_params = copy.deepcopy(self._params) 58 | payload = copy.deepcopy(self._payload) 59 | 60 | if continuation: 61 | url_params['continuation'] = continuation 62 | 63 | if not query: 64 | query = '' 65 | 66 | payload['query'] = query 67 | 68 | if params: 69 | payload['params'] = params 70 | else: 71 | payload['suggestStats'] = \ 72 | { 73 | 'clientName': 'youtube-music', 74 | 'inputMethod': 'KEYBOARD', 75 | 'originalQuery': query, 76 | 'parameterValidationStatus': 'VALID_PARAMETERS', 77 | 'searchMethod': 'ENTER_KEY', 78 | 'validationStatus': 'VALID', 79 | 'zeroPrefixEnabled': True, 80 | } 81 | 82 | resp = self._session.post \ 83 | ( 84 | url = url, 85 | params = url_params, 86 | json = payload, 87 | ) 88 | 89 | data = resp.json() 90 | 91 | return data 92 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/methods/search_suggestions.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: search_suggestions 3 | ''' 4 | 5 | from .. import constants 6 | from .. import decorators 7 | from typing import List 8 | 9 | @decorators.catch 10 | def search_suggestions(self: object, query: str = None) -> List[str]: 11 | ''' 12 | Retrieve search suggestions for a query. 13 | 14 | Args: 15 | self: Class instance 16 | query: Search query string 17 | Example: 'foo f' 18 | 19 | Returns: 20 | List of search suggestions 21 | 22 | Example: 23 | >>> api = ytm.BaseYouTubeMusic() 24 | >>> 25 | >>> data = api.search_suggestions('foo f') 26 | >>> 27 | >>> suggestions = data['contents'][0]['searchSuggestionsSectionRenderer']['contents'] 28 | >>> 29 | >>> for suggestion in suggestions: 30 | suggestion = suggestion['searchSuggestionRenderer'] 31 | print(suggestion['suggestion']) 32 | 33 | {'runs': [{'text': 'foo f', 'bold': True}, {'text': 'ighters'}]} 34 | {'runs': [{'text': 'foo f', 'bold': True}, {'text': 'ighters everlong'}]} 35 | {'runs': [{'text': 'foo f', 'bold': True}, {'text': 'ighters learn to fly'}]} 36 | {'runs': [{'text': 'foo f', 'bold': True}, {'text': 'ighters times like these'}]} 37 | {'runs': [{'text': 'foo f', 'bold': True}, {'text': 'ighters best of you'}]} 38 | {'runs': [{'text': 'foo f', 'bold': True}, {'text': 'ighters live'}]} 39 | {'runs': [{'text': 'foo f', 'bold': True}, {'text': 'ighters pretender'}]} 40 | >>> 41 | ''' 42 | 43 | resp = self._session.post \ 44 | ( 45 | url = self._url_api(constants.ENDPOINT_YTM_API_SEARCH_SUGGESTIONS), 46 | params = self._params, 47 | json = \ 48 | { 49 | **self._payload, 50 | 'input': query or '', 51 | }, 52 | ) 53 | 54 | data = resp.json() 55 | 56 | return data 57 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/methods/video_info.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the method: video_info 3 | ''' 4 | 5 | from .. import constants 6 | from .. import utils 7 | from .. import decorators 8 | 9 | import urllib 10 | import base64 11 | import json 12 | import requests 13 | 14 | @decorators.catch 15 | def video_info(self: object, video_id: str) -> dict: 16 | ''' 17 | Retrieve video info data. 18 | 19 | Unlike other methods, this relies on YouTube instead of YouTube Music. 20 | YouTube Music itself uses this to get further information about videos. 21 | 22 | Args: 23 | self: Class instance 24 | video_id: Video id 25 | Example: 'CkOP828oL30' 26 | 27 | Returns: 28 | Video info data 29 | 30 | Example: 31 | >>> api = ytm.BaseYouTubeMusic() 32 | >>> 33 | >>> data = api.video_info('CkOP828oL30') 34 | >>> 35 | >>> data['player_response']['videoDetails']['title'] 36 | 'Passenger | Bullets (Official Album Audio)' 37 | >>> 38 | ''' 39 | 40 | video_id = str(video_id) 41 | 42 | resp = requests.get \ 43 | ( 44 | url = self._url_yt(constants.ENDPOINT_YT_VIDEO_INFO), 45 | params = \ 46 | { 47 | 'video_id': video_id, 48 | 'el': 'detailpage', 49 | 'ps': 'default', 50 | 'hl': 'en', 51 | 'gl': 'US', 52 | 'eurl': f'https://youtube.googleapis.com/v/{video_id}', # Make this use a url util 53 | } 54 | ) 55 | 56 | data = dict(urllib.parse.parse_qsl(resp.text)) 57 | 58 | # Offload this to /parsers/song? 59 | parsers = \ 60 | { 61 | 'fexp': lambda data: \ 62 | list(map(int, data.split(','))), 63 | 'fflags': lambda data: \ 64 | utils.parse_fflags(dict(urllib.parse.parse_qsl(data))), 65 | 'account_playback_token': lambda data: \ 66 | base64.b64decode(data.encode()).decode(), 67 | 'timestamp': lambda data: \ 68 | int(data), 69 | 'enablecsi': lambda data: \ 70 | bool(int(data)), 71 | 'use_miniplayer_ui': lambda data: \ 72 | bool(int(data)), 73 | 'autoplay_count': lambda data: \ 74 | int(data), 75 | 'player_response': lambda data: \ 76 | json.loads(data), 77 | 'watch_next_response': lambda data: \ 78 | json.loads(data), 79 | 'watermark': lambda data: \ 80 | data.strip(',').split(','), 81 | 'rvs': lambda data: \ 82 | dict(urllib.parse.parse_qsl(data)), 83 | } 84 | 85 | for key, val in data.items(): 86 | if key in parsers: 87 | data[key] = parsers[key](val) 88 | 89 | return data 90 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/utils/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Package containing utility functions. 3 | 4 | These utility functions range in purpose, but help the super package 5 | achieve general tasks. 6 | 7 | Example: 8 | >>> utils.__all__ 9 | ('is_float', 'parse_fflags', 'random_user_agent') 10 | >>> 11 | >>> utils.is_float('1.0') 12 | True 13 | >>> 14 | ''' 15 | 16 | from .... import utils as __utils 17 | 18 | locals().update(__utils.include(__utils.__spec__)) 19 | 20 | __all__ = \ 21 | ( 22 | *tuple(__utils.include(__spec__)), 23 | ) 24 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/utils/is_float.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the utility function: is_float 3 | ''' 4 | 5 | import re 6 | 7 | def is_float(string: str) -> bool: 8 | ''' 9 | Check whether a string represents a float. 10 | 11 | The string must follow the format {number(s)}.{number(s)} to quality 12 | as a float 13 | 14 | Args: 15 | string: String to check 16 | Note: The string will be stripped 17 | 18 | Returns: 19 | Whether the string represents a float 20 | 21 | Example: 22 | >>> is_float('1.0') 23 | True 24 | >>> is_float('.0') 25 | False 26 | >>> is_float('1') 27 | False 28 | >>> is_float('a') 29 | False 30 | >>> is_float('123923.908908992 ') 31 | True 32 | >>> 33 | ''' 34 | 35 | return re.match(r'^\d+\.\d+$', string.strip()) is not None 36 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/utils/parse_fflags.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the utility function: parse_fflags 3 | ''' 4 | 5 | from .is_float import is_float 6 | 7 | def parse_fflags(fflags: dict) -> dict: 8 | ''' 9 | Parse fflags. 10 | 11 | Converts JavaScript values to Python values. 12 | E.g. 'true' -> True 13 | 14 | Args: 15 | fflags: Fflags to parse 16 | 17 | Returns: 18 | Parsed fflags 19 | 20 | Example: 21 | >>> fflags = \ 22 | { 23 | 'html5_encrypted_vp9_firefox': 'true', 24 | 'html5_stop_video_in_cancel_playback': 'true', 25 | } 26 | >>> 27 | >>> parse_fflags(fflags) 28 | {'html5_encrypted_vp9_firefox': True, 'html5_stop_video_in_cancel_playback': True} 29 | >>> 30 | ''' 31 | 32 | js_types = \ 33 | { 34 | 'true': True, 35 | 'false': False, 36 | 'null': None, 37 | } 38 | 39 | new_fflags = {} 40 | 41 | for fflag_key, fflag_val in fflags.items(): 42 | if fflag_val in js_types: 43 | fflag_val = js_types[fflag_val] 44 | elif fflag_val.isdigit(): 45 | fflag_val = int(fflag_val) 46 | elif is_float(fflag_val): 47 | fflag_val = float(fflag_val) 48 | 49 | new_fflags[fflag_key] = fflag_val 50 | 51 | return new_fflags 52 | -------------------------------------------------------------------------------- /ytm/apis/BaseYouTubeMusic/utils/random_user_agent.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the utility function: random_user_agent 3 | ''' 4 | 5 | import random 6 | from .. import constants 7 | 8 | def random_user_agent() -> str: 9 | ''' 10 | Returns a random user agent. 11 | 12 | Returns: 13 | A random user agent 14 | 15 | Example: 16 | >>> random_user_agent() 17 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0' 18 | >>> 19 | ''' 20 | 21 | return random.choice(constants.USER_AGENTS) 22 | -------------------------------------------------------------------------------- /ytm/apis/YouTubeMusic/YouTubeMusic.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the Api class: YouTubeMusic 3 | ''' 4 | 5 | from ..AbstractYouTubeMusic import AbstractYouTubeMusic 6 | 7 | class YouTubeMusic(AbstractYouTubeMusic): 8 | ''' 9 | Highest level YouTubeMusic Api class. 10 | 11 | When using an Api class, if low-level interactions are not required, this 12 | is the class to use. Lower-level Api classes will be available through 13 | attributes. 14 | 15 | For now this class only inherits from AbstractYouTubeMusic as this achieves 16 | very high-level Api interactions. 17 | ''' 18 | 19 | pass 20 | -------------------------------------------------------------------------------- /ytm/apis/YouTubeMusic/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Package containing the Api class: YouTubeMusic 3 | ''' 4 | 5 | from .YouTubeMusic import YouTubeMusic 6 | 7 | __all__ = (YouTubeMusic.__name__,) 8 | -------------------------------------------------------------------------------- /ytm/apis/YouTubeMusicDL/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Package containing the Api class: YouTubeMusicDL 3 | ''' 4 | 5 | from .YouTubeMusicDL import YouTubeMusicDL 6 | 7 | __all__ = (YouTubeMusicDL.__name__,) 8 | -------------------------------------------------------------------------------- /ytm/apis/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Package containing Api classes. 3 | 4 | These Api classes are the focus of the entire super package. 5 | All important functionality should be available through these classes. 6 | ''' 7 | 8 | from ..utils import include as __include 9 | 10 | __all__ = tuple(__include(__spec__)) 11 | -------------------------------------------------------------------------------- /ytm/classes/BuiltinMeta.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the meta class: BuiltinMeta 3 | ''' 4 | 5 | class BuiltinMeta(type): 6 | ''' 7 | Meta class to allow a class to pretend to be a builtin class 8 | 9 | Example: 10 | Without meta class: 11 | >>> class Foo(): 12 | pass 13 | 14 | >>> Foo 15 | 16 | >>> 17 | 18 | With meta class: 19 | >>> class Foo(metaclass=BuiltinMeta): 20 | pass 21 | 22 | >>> Foo 23 | 24 | >>> 25 | ''' 26 | 27 | def __new__(cls: type, name: str, bases: tuple, attrs: dict) -> object: 28 | ''' 29 | Return a new class instance. 30 | 31 | Args: 32 | cls: Class type 33 | name: Class name 34 | bases: Class bases 35 | attrs: Class attributes 36 | 37 | Returns: 38 | New class instance 39 | ''' 40 | 41 | return super().__new__ \ 42 | ( 43 | cls, 44 | name, 45 | bases, 46 | { 47 | **attrs, 48 | '__module__': 'builtins', 49 | }, 50 | ) 51 | -------------------------------------------------------------------------------- /ytm/classes/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Package containing classes. 3 | 4 | These classes are used by the super package, usually as meta classes and 5 | super classes 6 | 7 | Example: 8 | >>> from ytm import classes 9 | >>> 10 | >>> classes.__all__ 11 | ... 12 | >>> 13 | ''' 14 | 15 | from .. import utils as __utils 16 | import types as __types 17 | 18 | __all__ = tuple \ 19 | ( 20 | __utils.include \ 21 | ( 22 | __spec__, 23 | lambda object: not isinstance(object, __types.ModuleType), 24 | ), 25 | ) 26 | -------------------------------------------------------------------------------- /ytm/decorators/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Package containing decorators. 3 | 4 | These decorators range in purpose, but help the super package 5 | achieve general tasks. 6 | 7 | Example: 8 | >>> from ytm import decorators 9 | >>> 10 | >>> decorators.__all__ 11 | ... 12 | >>> 13 | ''' 14 | 15 | from .. import utils as __utils 16 | import types as __types 17 | 18 | __all__ = tuple \ 19 | ( 20 | __utils.include \ 21 | ( 22 | __spec__, 23 | lambda object: not isinstance(object, __types.ModuleType) \ 24 | and not object.__name__.startswith('_'), 25 | ), 26 | ) 27 | -------------------------------------------------------------------------------- /ytm/decorators/_set_attrs.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the decorator: _set_attrs 3 | ''' 4 | 5 | from typing import Callable 6 | 7 | def _set_attrs(attrs: dict) -> Callable: 8 | ''' 9 | Update a class or functions attributes with attrs 10 | 11 | Args: 12 | attrs: Attributes to update the class or function with 13 | 14 | Returns: 15 | The attribute-setting decorator 16 | 17 | Example: 18 | >>> @_set_attrs({'__name__': 'NEW_NAME'}) 19 | def foo(a, b): 20 | return a + b 21 | 22 | >>> 23 | >>> foo.__name__ 24 | 'NEW_NAME' 25 | >>> 26 | ''' 27 | 28 | def decorator(func: Callable) -> Callable: 29 | ''' 30 | Decorate func to update its attributes as previously specified 31 | 32 | Args: 33 | func: Function to decorate 34 | 35 | Returns: 36 | The decorated func 37 | ''' 38 | 39 | for key, val in attrs.items(): 40 | setattr(func, key, val) 41 | 42 | return func 43 | 44 | return decorator 45 | -------------------------------------------------------------------------------- /ytm/decorators/enforce.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the decorator: enforce 3 | ''' 4 | 5 | from ._enforce import _enforce 6 | import functools 7 | from typing import Callable, Any 8 | 9 | def enforce(func: Callable) -> Callable: 10 | ''' 11 | Enforce function argument types and return value type as specified by the 12 | functions type hints. 13 | 14 | Args: 15 | func: Function to decorate 16 | 17 | Returns: 18 | Decorated func. 19 | 20 | Example: 21 | >>> @enforce 22 | def foo(x: int, func = str) -> str: 23 | return func(x) 24 | 25 | >>> 26 | >>> # Arguments: Acceptable 27 | >>> foo(1) 28 | '1' 29 | >>> # Arguments: Unacceptable 30 | >>> foo('a') 31 | TypeError: Expected argument 'x' to be of type 'int' not 'str' 32 | >>> 33 | >>> # Return Value: Acceptable 34 | >>> foo(1, func = str) 35 | '1' 36 | >>> # Return Value: Unacceptable 37 | >>> foo(1, func = int) 38 | TypeError: Expected return value to be of type 'str' not 'int' 39 | >>> 40 | ''' 41 | 42 | @_enforce \ 43 | ( 44 | parameters = True, 45 | return_value = True, 46 | ) 47 | @functools.wraps(func) 48 | def wrapper(*args: Any, **kwargs: Any) -> Any: 49 | ''' 50 | Wrap func to enforce argument and return value types 51 | 52 | Args: 53 | *args: Function arguments 54 | **kwargs: Function keyword arguments 55 | 56 | Returns: 57 | The wrapped functions return value 58 | ''' 59 | 60 | return func(*args, **kwargs) 61 | 62 | return wrapper 63 | -------------------------------------------------------------------------------- /ytm/decorators/enforce_parameters.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the decorator: enforce_parameters 3 | ''' 4 | 5 | from ._enforce import _enforce 6 | import functools 7 | from typing import Callable, Any 8 | 9 | def enforce_parameters(func: Callable) -> Callable: 10 | ''' 11 | Enforce function argument types as specified by the functions type hints. 12 | 13 | Args: 14 | func: Function to decorate 15 | 16 | Returns: 17 | Decorated func. 18 | 19 | Example: 20 | >>> @enforce_parameters 21 | def foo(x: int) -> bool: 22 | return 'not a bool' 23 | 24 | >>> # Acceptable 25 | >>> foo(1) 26 | 'not a bool' 27 | >>> 28 | >>> # Not acceptable 29 | >>> foo('a') 30 | TypeError: Expected argument 'x' to be of type 'int' not 'str' 31 | >>> 32 | ''' 33 | 34 | @_enforce \ 35 | ( 36 | parameters = True, 37 | return_value = False, 38 | ) 39 | @functools.wraps(func) 40 | def wrapper(*args: Any, **kwargs: Any) -> Any: 41 | ''' 42 | Wrap func to enforce argument types 43 | 44 | Args: 45 | *args: Function arguments 46 | **kwargs: Function keyword arguments 47 | 48 | Returns: 49 | The wrapped functions return value 50 | ''' 51 | 52 | return func(*args, **kwargs) 53 | 54 | return wrapper 55 | -------------------------------------------------------------------------------- /ytm/decorators/enforce_return_value.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the decorator: enforce_return_value 3 | ''' 4 | 5 | from ._enforce import _enforce 6 | import functools 7 | from typing import Callable, Any 8 | 9 | def enforce_return_value(func: Callable) -> Callable: 10 | ''' 11 | Enforce function return value type as specified by the functions type hints. 12 | 13 | Args: 14 | func: Function to decorate 15 | 16 | Returns: 17 | Decorated func. 18 | 19 | Example: 20 | >>> @enforce_return_value 21 | def foo(x) -> int: 22 | return x 23 | 24 | >>> # Acceptable 25 | >>> foo(1 + 3) 26 | 4 27 | >>> 28 | >>> # Not acceptable 29 | >>> foo('a') 30 | TypeError: Expected return value to be of type 'int' not 'str' 31 | >>> 32 | ''' 33 | 34 | @_enforce \ 35 | ( 36 | parameters = False, 37 | return_value = True, 38 | ) 39 | @functools.wraps(func) 40 | def wrapper(*args: Any, **kwargs: Any) -> Any: 41 | ''' 42 | Wrap func to enforce return value type 43 | 44 | Args: 45 | *args: Function arguments 46 | **kwargs: Function keyword arguments 47 | 48 | Returns: 49 | The wrapped functions return value 50 | ''' 51 | 52 | return func(*args, **kwargs) 53 | 54 | return wrapper 55 | -------------------------------------------------------------------------------- /ytm/decorators/parse.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the decorator: parse 3 | ''' 4 | 5 | import functools 6 | from typing import Callable, Any 7 | 8 | def parse(parser: Callable) -> Callable: 9 | ''' 10 | Returns a decorator used to parse a functions return value before 11 | it is returned. 12 | 13 | Args: 14 | parser: Function used to parse the decorated functions return value 15 | 16 | Returns: 17 | Function decorator 18 | 19 | Example: 20 | >>> @parse(int) 21 | def foo(): 22 | return '1' 23 | 24 | >>> foo() 25 | 1 26 | >>> 27 | ''' 28 | 29 | def decorator(func: Callable) -> Callable: 30 | ''' 31 | Decorate func to parse its return value 32 | 33 | Args: 34 | func: Function to decorate 35 | 36 | Returns: 37 | The decorated func 38 | ''' 39 | 40 | @functools.wraps(func) 41 | def wrapper(*args: Any, **kwargs: Any) -> Any: 42 | ''' 43 | Wrap func to parse its return value 44 | 45 | Args: 46 | *args: Function arguments 47 | **kwargs: Function keyword arguments 48 | 49 | Returns: 50 | The wrapped functions return value 51 | ''' 52 | 53 | return parser(func(*args, **kwargs)) 54 | 55 | return wrapper 56 | 57 | return decorator 58 | -------------------------------------------------------------------------------- /ytm/decorators/rename.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the decorator: rename 3 | ''' 4 | 5 | from typing import Callable 6 | from ._set_attrs import _set_attrs 7 | 8 | def rename(name: str) -> Callable: 9 | ''' 10 | Decorator to rename a functions __name__ attribute. 11 | 12 | Args: 13 | name: New function name 14 | 15 | Returns: 16 | The function renaming decorator 17 | 18 | Example: 19 | >>> @rename('NEW_NAME') 20 | def foo(): 21 | pass 22 | 23 | >>> foo.__name__ 24 | 'NEW_NAME' 25 | >>> 26 | ''' 27 | 28 | return _set_attrs({'__name__': name}) 29 | -------------------------------------------------------------------------------- /ytm/decorators/rename_module.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the decorator: rename_module 3 | ''' 4 | 5 | from typing import Callable 6 | from ._set_attrs import _set_attrs 7 | 8 | def rename_module(name: str) -> Callable: 9 | ''' 10 | Decorator to rename a functions __module__ attribute. 11 | 12 | Args: 13 | name: New module name 14 | 15 | Returns: 16 | The module renaming decorator 17 | 18 | Example: 19 | >>> @rename_module('NEW_NAME') 20 | def foo(): 21 | pass 22 | 23 | >>> foo.__module__ 24 | 'NEW_NAME' 25 | >>> 26 | ''' 27 | 28 | return _set_attrs({'__module__': name}) 29 | -------------------------------------------------------------------------------- /ytm/exceptions/ArgumentError.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the exception: ArgumentError 3 | ''' 4 | 5 | from . import base 6 | 7 | class ArgumentError(base.BaseException): 8 | ''' 9 | Exception raised when an argument is invalid 10 | ''' 11 | 12 | pass 13 | -------------------------------------------------------------------------------- /ytm/exceptions/ConnectionError.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the exception: ConnectionError 3 | ''' 4 | 5 | from . import base 6 | 7 | class ConnectionError(base.BaseException): 8 | ''' 9 | Exception raised when a connection to a server fails 10 | ''' 11 | 12 | pass 13 | -------------------------------------------------------------------------------- /ytm/exceptions/InvalidPageConfigurationError.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the exception: InvalidPageConfigurationError 3 | ''' 4 | 5 | from . import base 6 | 7 | class InvalidPageConfigurationError(base.BaseException): 8 | ''' 9 | Exception raised when a page contains invalid configuration data 10 | ''' 11 | 12 | pass 13 | -------------------------------------------------------------------------------- /ytm/exceptions/MethodError.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the exception: MethodError 3 | ''' 4 | 5 | from . import base 6 | 7 | class MethodError(base.BaseException): 8 | ''' 9 | Exception raised when a method encounters an error 10 | ''' 11 | 12 | pass 13 | -------------------------------------------------------------------------------- /ytm/exceptions/PageNotFoundError.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the exception: PageNotFoundError 3 | ''' 4 | 5 | from . import base 6 | 7 | class PageNotFoundError(base.BaseException): 8 | ''' 9 | Exception raised when a page is not found 10 | ''' 11 | 12 | pass 13 | -------------------------------------------------------------------------------- /ytm/exceptions/ParserError.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the exception: ParserError 3 | ''' 4 | 5 | from . import base 6 | 7 | class ParserError(base.BaseException): 8 | ''' 9 | Exception raised when a parser encounters an error 10 | 11 | Attributes: 12 | parser: Name of the parser function which encountered an error 13 | message: Exception message 14 | ''' 15 | 16 | parser: str = None 17 | message: str = None 18 | 19 | def __init__(self: object, parser: str, message: str): 20 | ''' 21 | Initialise the exception class. 22 | 23 | Args: 24 | self: Class instance 25 | parser: The name of the parser function which encountered an error 26 | ''' 27 | 28 | self.parser = parser 29 | self.message = message 30 | 31 | super().__init__(f'{self.parser}() encountered an error: {self.message}') 32 | -------------------------------------------------------------------------------- /ytm/exceptions/YouTubeApiError.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the exception: YouTubeApiError 3 | ''' 4 | 5 | from . import base 6 | 7 | class YouTubeApiError(base.BaseException): 8 | ''' 9 | Exception raised when the YouTube Api reports an error 10 | 11 | Attributes: 12 | data: YouTube Api error data 13 | code: Error code 14 | message: Error message 15 | status: Error status 16 | ''' 17 | 18 | data: dict = None 19 | code: int = None 20 | message: str = None 21 | status: str = None 22 | 23 | def __init__(self: object, data: dict) -> None: 24 | ''' 25 | Initialise the exception class. 26 | 27 | Args: 28 | self: Class instance 29 | data: YouTube Api error data 30 | ''' 31 | 32 | super().__init__() 33 | 34 | self.data = data 35 | 36 | self.code = self.data.get('errorcode') 37 | self.message = self.data.get('reason') 38 | self.status = self.data.get('status') 39 | 40 | def __str__(self: object) -> str: 41 | ''' 42 | Returns a string representation of the class. 43 | 44 | Args: 45 | self: Class instance 46 | 47 | Returns: 48 | String representation of the form: 49 | [{code}] {status} - {message} 50 | ''' 51 | 52 | return '[{code}] {status} - {message}'.format \ 53 | ( 54 | code = self.code, 55 | status = self.status, 56 | message = self.message, 57 | ) 58 | -------------------------------------------------------------------------------- /ytm/exceptions/YouTubeMusicApiError.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the exception: YouTubeMusicApiError 3 | ''' 4 | 5 | from . import base 6 | 7 | class YouTubeMusicApiError(base.BaseException): 8 | ''' 9 | Exception raised when the YouTube Api reports an error 10 | 11 | Attributes: 12 | data: YouTube Music Api error data 13 | code: Error code 14 | Example: 500 15 | errors: Errors 16 | Example: [{'domain': 'global', 17 | 'message': 'Unknown Error.', 18 | 'reason': 'backendError'}] 19 | message: Error message 20 | Example: 'Unknown Error.' 21 | status: Error status 22 | Example: 'UNKNOWN' 23 | ''' 24 | 25 | data: dict = None 26 | code: int = None 27 | errors: list = None 28 | message: str = None 29 | status: str = None 30 | 31 | def __init__(self: object, data: dict) -> None: 32 | ''' 33 | Initialise the exception class. 34 | 35 | Args: 36 | self: Class instance 37 | data: YouTube Music Api error data 38 | ''' 39 | 40 | super().__init__() 41 | 42 | self.data = data.get('error') 43 | 44 | self.code = self.data.get('code') 45 | self.errors = self.data.get('errors', ()) 46 | self.message = self.data.get('message') 47 | self.status = self.data.get('status') 48 | 49 | def __str__(self: object) -> str: 50 | ''' 51 | Returns a string representation of the class. 52 | 53 | Args: 54 | self: Class instance 55 | 56 | Returns: 57 | String representation of the form: 58 | [{code}] {status} - {message} ({domains}) 59 | ''' 60 | 61 | return '[{code}] {status} - {message} ({domains})'.format \ 62 | ( 63 | code = self.code, 64 | status = self.status, 65 | message = self.message, 66 | domains = ', '.join \ 67 | ( 68 | error.get('domain') 69 | for error in self.errors 70 | ), 71 | ) 72 | -------------------------------------------------------------------------------- /ytm/exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Package containing exception classes. 3 | 4 | These exceptions are used by the super package to raise appropriate errors 5 | 6 | Example: 7 | >>> from ytm import exceptions 8 | >>> 9 | >>> exceptions.__all__ 10 | ... 11 | >>> 12 | ''' 13 | 14 | from .. import utils as __utils 15 | import types as __types 16 | 17 | __all__ = tuple \ 18 | ( 19 | __utils.include \ 20 | ( 21 | __spec__, 22 | lambda object: not isinstance(object, __types.ModuleType), 23 | ), 24 | ) 25 | -------------------------------------------------------------------------------- /ytm/exceptions/base/BaseException.py: -------------------------------------------------------------------------------- 1 | from ... import classes 2 | 3 | class BaseException(Exception, metaclass=classes.BuiltinMeta): 4 | pass 5 | -------------------------------------------------------------------------------- /ytm/exceptions/base/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Package containing base exception classes. 3 | 4 | These base exceptions are used by derived exceptions to inherit core 5 | functionality 6 | 7 | Example: 8 | >>> from ytm.exceptions import base 9 | >>> 10 | >>> base.__all__ 11 | ... 12 | >>> 13 | ''' 14 | 15 | from ... import utils as __utils 16 | import types as __types 17 | 18 | __all__ = tuple \ 19 | ( 20 | __utils.include \ 21 | ( 22 | __spec__, 23 | lambda object: not isinstance(object, __types.ModuleType), 24 | ), 25 | ) 26 | -------------------------------------------------------------------------------- /ytm/parsers/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Package containing parser functions 3 | ''' 4 | 5 | from .. import utils as __utils 6 | import types as __types 7 | 8 | __all__ = tuple \ 9 | ( 10 | __utils.include \ 11 | ( 12 | __spec__, 13 | lambda object: not isinstance(object, __types.ModuleType), 14 | ), 15 | ) 16 | -------------------------------------------------------------------------------- /ytm/parsers/_search_filter.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the parser: _search_filter 3 | ''' 4 | 5 | from .. import utils 6 | from . import decorators 7 | from ._search import _search as parse_search 8 | 9 | @decorators.enforce_parameters 10 | @decorators.catch 11 | def _search_filter(data: dict, filter: str) -> dict: 12 | ''' 13 | Parse filtered search data. 14 | 15 | Args: 16 | data: Data to parse 17 | filter: Search filter 18 | Example: 'artists' 19 | 20 | Returns: 21 | Parsed data 22 | 23 | Raises: 24 | ParserError: The parser encountered an error 25 | 26 | Example: 27 | >>> api = ytm.BaseYouTubeMusic() 28 | >>> 29 | >>> params_artists = ytm.constants.SEARCH_PARAM_PREFIX \ 30 | + ytm.constants.SEARCH_PARAM_ARTISTS \ 31 | + ytm.constants.SEARCH_PARAM_SUFFIX 32 | >>> 33 | >>> data = api.search('foo fighters', params = params_artists) 34 | >>> 35 | >>> parsed_data = ytm.parsers._search_filter(data, 'artists') 36 | >>> 37 | >>> parsed_data['items'][0]['name'] 38 | 'Foo Fighters' 39 | >>> 40 | ''' 41 | 42 | assert data, 'No data to parse' 43 | 44 | if 'continuationContents' in data: 45 | shelf = utils.get \ 46 | ( 47 | data, 48 | 'continuationContents', 49 | 'musicShelfContinuation', 50 | default = (), 51 | ) 52 | 53 | assert shelf, 'Invalid continuation data' 54 | 55 | shelves = (shelf,) 56 | 57 | data['contents'] = \ 58 | { 59 | 'tabbedSearchResultsRenderer': \ 60 | { 61 | 'tabs': \ 62 | [ 63 | { 64 | 'tabRenderer': \ 65 | { 66 | 'content': \ 67 | { 68 | 'sectionListRenderer': \ 69 | { 70 | 'contents': \ 71 | ( 72 | { 73 | 'musicShelfRenderer': \ 74 | { 75 | **shelf, 76 | }, 77 | }, 78 | ), 79 | }, 80 | }, 81 | }, 82 | }, 83 | ], 84 | }, 85 | } 86 | 87 | shelves = utils.get \ 88 | ( 89 | data, 90 | 'contents', 91 | 'tabbedSearchResultsRenderer', 92 | 'tabs', 93 | 0, 94 | 'tabRenderer', 95 | 'content', 96 | 'sectionListRenderer', 97 | 'contents', 98 | ) 99 | 100 | assert shelves, 'No search results found' 101 | 102 | for shelf in shelves: 103 | shelf = utils.first(shelf) 104 | 105 | shelf['title'] = \ 106 | { 107 | 'runs': \ 108 | [ 109 | { 110 | 'text': filter.title(), 111 | }, 112 | ], 113 | } 114 | 115 | parsed_data = parse_search(data) 116 | filtered_data = utils.get(parsed_data, filter) 117 | 118 | return filtered_data 119 | -------------------------------------------------------------------------------- /ytm/parsers/cleansers/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Package containing cleansers. 3 | 4 | These functions clean data into a desired format. 5 | ''' 6 | 7 | from ...utils import include as __include 8 | 9 | __all__ = tuple(__include(__spec__)) 10 | -------------------------------------------------------------------------------- /ytm/parsers/cleansers/ascii_time.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the cleanser: ascii_time 3 | ''' 4 | 5 | def ascii_time(duration: str) -> int: 6 | ''' 7 | Convert an ASCII representation of time into duration in seconds. 8 | 9 | The ASCII representation follows the following format: 10 | {} hours, {} minutes, {} seconds 11 | Each group is optional and treated case-insensitively. 12 | 13 | Args: 14 | duration: ASCII duration string 15 | Example: '2 hours, 36 seconds' 16 | 17 | Returns: 18 | Duration in seconds 19 | 20 | Example: 21 | >>> time = '11 hours, 22 minutes, 33 seconds' 22 | >>> 23 | >>> ascii_time(time) 24 | 40953 25 | >>> 26 | ''' 27 | 28 | return sum \ 29 | ( 30 | 60 ** scalar * value 31 | for value_str, slot_name in map \ 32 | ( 33 | lambda segment: segment.strip().split(' '), 34 | duration.lower().split(','), 35 | ) 36 | for value, scalar in \ 37 | ( 38 | ( 39 | int(value_str), 40 | ( 41 | 'seconds', 42 | 'minutes', 43 | 'hours', 44 | ).index(slot_name) 45 | ), 46 | ) 47 | ) 48 | -------------------------------------------------------------------------------- /ytm/parsers/cleansers/iso_time.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the cleanser: iso_time 3 | ''' 4 | 5 | def iso_time(duration: str) -> int: 6 | ''' 7 | Convert an ISO duration string to duration in seconds 8 | 9 | An ISO duration string follows the format: 10 | h:m:s 11 | Where each group is optional (E.g. m:s is perfectly valid). 12 | 13 | Args: 14 | duration: ISO duration string 15 | Example: 11:22:33 16 | 17 | Returns: 18 | Duration in seconds 19 | 20 | Example: 21 | >>> time = '01:25:59' 22 | >>> 23 | >>> iso_time(time) 24 | 5159 25 | >>> 26 | ''' 27 | 28 | return sum \ 29 | ( 30 | 60 ** index * int(item) 31 | for index, item in enumerate(duration.strip().split(':')[::-1]) 32 | ) 33 | -------------------------------------------------------------------------------- /ytm/parsers/cleansers/type.py: -------------------------------------------------------------------------------- 1 | ''' 2 | ''' 3 | 4 | def type(data): 5 | ''' 6 | ''' 7 | 8 | return data.split('_')[-1].title() 9 | -------------------------------------------------------------------------------- /ytm/parsers/cleansers/views.py: -------------------------------------------------------------------------------- 1 | ''' 2 | ''' 3 | 4 | def views(data): 5 | ''' 6 | ''' 7 | 8 | return int(data.strip().split(' ')[0].replace(',', '')) 9 | -------------------------------------------------------------------------------- /ytm/parsers/constants/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | ''' 3 | 4 | BROWSE_ENDPOINT = ('browseEndpoint',) 5 | MENU_ITEMS = ('menu', 'menuRenderer', 'items') 6 | 7 | RUN_TEXT = ('runs', 0, 'text') 8 | BROWSE_ENDPOINT_ID = (*BROWSE_ENDPOINT, 'browseId') 9 | BROWSE_ENDPOINT_PARAMS = (*BROWSE_ENDPOINT, 'params') 10 | BROWSE_ENDPOINT_PAGE_TYPE = (*BROWSE_ENDPOINT, 'browseEndpointContextSupportedConfigs', 'browseEndpointContextMusicConfig', 'pageType') 11 | THUMBNAIL = ('thumbnail', 'thumbnails', -1) 12 | -------------------------------------------------------------------------------- /ytm/parsers/decorators/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Package containing decorators. 3 | ''' 4 | 5 | from ... import \ 6 | ( 7 | utils as __utils, 8 | decorators as __decorators, 9 | ) 10 | 11 | __inherit = __utils.include(__decorators.__spec__) 12 | 13 | locals().update(__inherit) 14 | 15 | __all__ = \ 16 | ( 17 | *tuple(__utils.include(__spec__)), 18 | ) 19 | -------------------------------------------------------------------------------- /ytm/parsers/decorators/catch.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the decorator: catch 3 | ''' 4 | 5 | from ... import exceptions 6 | 7 | import functools 8 | from typing import Callable, Any 9 | 10 | def catch(func: Callable) -> Callable: 11 | ''' 12 | Catch and re-raise errors encountered during parsing. 13 | 14 | Args: 15 | func: Parser to decorate 16 | 17 | Returns: 18 | Decorated function 19 | 20 | Example: 21 | >>> @catch 22 | def my_parser(data: dict) -> dict: 23 | assert 'age' in data, 'Data has no age' 24 | return data 25 | 26 | >>> 27 | >>> my_parser({'age': 12}) 28 | {'age': 12} 29 | >>> 30 | >>> my_parser({'name': 'Foo'}) 31 | ParserError: my_parser() encountered an error: Data has no age 32 | >>> 33 | >>> 34 | ''' 35 | 36 | @functools.wraps(func) 37 | def wrapper(*args: Any, **kwargs: Any) -> Any: 38 | ''' 39 | Wrap {func} to re-raise any exceptions 40 | 41 | Args: 42 | *args: Function arguments 43 | **kwargs: Function keyword arguments 44 | 45 | Returns: 46 | Function return value 47 | ''' 48 | 49 | try: 50 | return func(*args, **kwargs) 51 | except exceptions.ParserError as error: 52 | error_message = f'{error.parser}() - {error.message}' 53 | except Exception as error: 54 | error_message = str(error) or 'Unknown' 55 | 56 | raise exceptions.ParserError \ 57 | ( 58 | parser = func.__name__, 59 | message = error_message, 60 | ) 61 | 62 | return wrapper 63 | -------------------------------------------------------------------------------- /ytm/parsers/formatters/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Package containing formatters. 3 | ''' 4 | 5 | from ... import utils as __utils 6 | 7 | __all__ = \ 8 | ( 9 | *tuple(__utils.include(__spec__)), 10 | ) 11 | -------------------------------------------------------------------------------- /ytm/parsers/formatters/menu_items.py: -------------------------------------------------------------------------------- 1 | ''' 2 | ''' 3 | 4 | from ... import utils 5 | 6 | def menu_items(data): 7 | ''' 8 | ''' 9 | 10 | menu_data = {} 11 | 12 | for menu_item in data: 13 | menu_item = utils.first(menu_item) 14 | 15 | for key, val in menu_item.copy().items(): 16 | if not key.startswith('default'): 17 | continue 18 | 19 | new_key = key.replace('default', '') 20 | new_key = new_key[0].lower() + new_key[1:] 21 | 22 | menu_item[new_key] = menu_item.pop(key) 23 | 24 | menu_text = utils.get \ 25 | ( 26 | menu_item, 27 | 'text', 28 | 'runs', 29 | 0, 30 | 'text', 31 | ) 32 | menu_icon = utils.get \ 33 | ( 34 | menu_item, 35 | 'icon', 36 | 'iconType', 37 | ) 38 | menu_endpoint = utils.get \ 39 | ( 40 | menu_item, 41 | 'navigationEndpoint', 42 | ) 43 | 44 | if not menu_endpoint: 45 | continue 46 | 47 | menu_identifier = menu_text[0].lower() + menu_text.title()[1:].replace(' ', '') \ 48 | if menu_text else None 49 | 50 | menu_item_data = \ 51 | { 52 | 'text': menu_text, 53 | 'icon': menu_icon, 54 | 'endpoint': menu_endpoint, 55 | } 56 | 57 | menu_data[menu_identifier] = menu_item_data 58 | 59 | return menu_data 60 | -------------------------------------------------------------------------------- /ytm/parsers/guide.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the parser: guide 3 | ''' 4 | 5 | from .. import utils 6 | from . import decorators 7 | 8 | @decorators.enforce_parameters 9 | @decorators.catch 10 | def guide(data: dict) -> dict: 11 | ''' 12 | Parse data: Guide. 13 | 14 | Args: 15 | data: Data to parse 16 | 17 | Returns: 18 | Parsed data 19 | 20 | Raises: 21 | ParserError: The parser encountered an error 22 | 23 | Example: 24 | >>> api = ytm.BaseYouTubeMusic() 25 | >>> 26 | >>> data = api.guide() 27 | >>> 28 | >>> parsed_data = ytm.parsers.guide(data) 29 | >>> 30 | >>> parsed_data['Home'] 31 | 'FEmusic_home' 32 | >>> 33 | ''' 34 | 35 | assert data, 'No data to parse' 36 | 37 | scraped = {} 38 | 39 | pivot_items = utils.get \ 40 | ( 41 | data, 42 | 'items', 43 | 0, 44 | 'pivotBarRenderer', 45 | 'items', 46 | default = (), 47 | ) 48 | 49 | assert pivot_items, 'Data has no pivot items' 50 | 51 | for pivot_item in pivot_items: 52 | pivot_item = utils.get \ 53 | ( 54 | pivot_item, 55 | 'pivotBarItemRenderer', 56 | ) 57 | 58 | pivot_item_title = utils.get \ 59 | ( 60 | pivot_item, 61 | 'title', 62 | 'runs', 63 | 0, 64 | 'text', 65 | ) 66 | pivot_item_browse_id = utils.get \ 67 | ( 68 | pivot_item, 69 | 'navigationEndpoint', 70 | 'browseEndpoint', 71 | 'browseId', 72 | ) 73 | 74 | scraped[pivot_item_title] = pivot_item_browse_id 75 | 76 | return scraped 77 | -------------------------------------------------------------------------------- /ytm/parsers/hotlist.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the parser: hotlist 3 | ''' 4 | 5 | from .. import utils 6 | from . import decorators 7 | 8 | @decorators.enforce_parameters 9 | @decorators.catch 10 | def hotlist(data: dict) -> list: 11 | ''' 12 | Parse data: Hotlist. 13 | 14 | Args: 15 | data: Data to parse 16 | 17 | Returns: 18 | Parsed data 19 | 20 | Raises: 21 | ParserError: The parser encountered an error 22 | 23 | Example: 24 | >>> api = ytm.BaseYouTubeMusic() 25 | >>> 26 | >>> data = api.browse_hotlist() 27 | >>> 28 | >>> parsed_data = ytm.parsers.hotlist(data) 29 | >>> 30 | >>> parsed_data[0]['name'] 31 | 'Me & You Together Song' 32 | >>> 33 | ''' 34 | 35 | assert data, 'No data to parse' 36 | 37 | grid_items = utils.get \ 38 | ( 39 | data, 40 | 'contents', 41 | 'singleColumnBrowseResultsRenderer', 42 | 'tabs', 43 | 0, 44 | 'tabRenderer', 45 | 'content', 46 | 'sectionListRenderer', 47 | 'contents', 48 | 0, 49 | 'itemSectionRenderer', 50 | 'contents', 51 | 0, 52 | 'gridRenderer', 53 | 'items', 54 | default = (), 55 | ) 56 | 57 | assert grid_items 58 | 59 | tracks = [] 60 | 61 | for track in grid_items: 62 | track = utils.first(track) 63 | 64 | track_title = utils.get \ 65 | ( 66 | track, 67 | 'title', 68 | 'runs', 69 | 0, 70 | 'text', 71 | ) 72 | track_views = utils.get \ 73 | ( 74 | track, 75 | 'subtitle', 76 | 'runs', 77 | -1, 78 | 'text', 79 | func = lambda views: views.strip().split(' ')[0] 80 | ) 81 | track_artist_id = utils.get \ 82 | ( 83 | track, 84 | 'menu', 85 | 'menuRenderer', 86 | 'items', 87 | 5, 88 | 'menuNavigationItemRenderer', 89 | 'navigationEndpoint', 90 | 'browseEndpoint', 91 | 'browseId', 92 | ) 93 | track_thumbnail = utils.get \ 94 | ( 95 | track, 96 | 'backgroundImage', 97 | 'musicThumbnailRenderer', 98 | 'thumbnail', 99 | 'thumbnails', 100 | -1, 101 | ) 102 | track_id = utils.get \ 103 | ( 104 | track, 105 | 'onTap', 106 | 'watchEndpoint', 107 | 'videoId', 108 | ) 109 | track_music_video_type = utils.get \ 110 | ( 111 | track, 112 | 'onTap', 113 | 'watchEndpoint', 114 | 'watchEndpointMusicSupportedConfigs', 115 | 'watchEndpointMusicConfig', 116 | 'musicVideoType', 117 | ) 118 | 119 | raw_track_artists = utils.get \ 120 | ( 121 | track, 122 | 'subtitle', 123 | 'runs', 124 | default = (), 125 | )[:-1:2] 126 | 127 | track_artists = [] 128 | 129 | for track_artist in raw_track_artists: 130 | track_artist_title = utils.get \ 131 | ( 132 | track_artist, 133 | 'text', 134 | ) 135 | 136 | track_artists.append(track_artist_title) 137 | 138 | track_data = \ 139 | { 140 | 'id': track_id, 141 | 'name': track_title, 142 | 'views': track_views, 143 | 'artist': \ 144 | { 145 | 'id': track_artist_id, 146 | 'names': track_artists, 147 | }, 148 | # 'artists': track_artists, 149 | # 'artist_id': track_artist_id, 150 | 'thumbnail': track_thumbnail, 151 | # 'music_video_type': track_music_video_type, 152 | } 153 | 154 | tracks.append(track_data) 155 | 156 | return tracks 157 | -------------------------------------------------------------------------------- /ytm/parsers/queue.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the parser: queue 3 | ''' 4 | 5 | from .. import utils 6 | from . import decorators 7 | from . import constants 8 | from . import formatters 9 | from . import cleansers 10 | 11 | @decorators.enforce_parameters 12 | @decorators.catch 13 | def queue(data: dict) -> list: 14 | ''' 15 | Parse data: Queue. 16 | 17 | Args: 18 | data: Data to parse 19 | 20 | Returns: 21 | Parsed data 22 | 23 | Raises: 24 | ParserError: The parser encountered an error 25 | 26 | Example: 27 | >>> api = ytm.BaseYouTubeMusic() 28 | >>> 29 | >>> data = api.queue('Gz3-4UuMWjQ') 30 | >>> 31 | >>> parsed_data = ytm.parsers.queue(data) 32 | >>> 33 | >>> parsed_data[0]['name'] 34 | 'Amen' 35 | >>> 36 | ''' 37 | 38 | assert data, 'No data to parse' 39 | 40 | queue_items = utils.get(data, 'queueDatas', default=()) 41 | 42 | assert queue_items, 'No queded items' 43 | 44 | songs = [] 45 | 46 | for queue_item in queue_items: 47 | queue_item_content = utils.get(queue_item, 'content') 48 | panel_video = utils.first(queue_item_content) 49 | panel_video_menu = utils.get(panel_video, *constants.MENU_ITEMS, func = formatters.menu_items) 50 | 51 | video_title = utils.get(panel_video, 'title', *constants.RUN_TEXT) 52 | video_thumbnail = utils.get(panel_video, *constants.THUMBNAIL) 53 | video_duration = utils.get(panel_video, 'lengthText', *constants.RUN_TEXT, func = cleansers.iso_time) 54 | video_selected = utils.get(panel_video, 'selected') 55 | video_params = utils.get(panel_video, 'navigationEndpoint', 'watchEndpoint', 'params') 56 | video_type = utils.get(panel_video, 'navigationEndpoint', 'watchEndpoint', 'watchEndpointMusicSupportedConfigs', 'watchEndpointMusicConfig', 'musicVideoType', func = cleansers.type) 57 | video_id = utils.get(panel_video, 'videoId') 58 | video_artist_name = utils.get(panel_video, 'shortBylineText', *constants.RUN_TEXT) 59 | video_artist_id = utils.get(panel_video_menu, 'goToArtist', 'endpoint', *constants.BROWSE_ENDPOINT_ID) 60 | video_album_id = utils.get(panel_video_menu, 'goToAlbum', 'endpoint', *constants.BROWSE_ENDPOINT_ID) 61 | video_album_type = utils.get(panel_video_menu, 'goToAlbum', 'endpoint', *constants.BROWSE_ENDPOINT_PAGE_TYPE, func = cleansers.type) 62 | video_radio_params = utils.get(panel_video_menu, 'startRadio', 'endpoint', 'watchEndpoint', 'params') 63 | video_radio_playlist_id = utils.get(panel_video_menu, 'startRadio', 'endpoint', 'watchEndpoint', 'playlistId') 64 | 65 | video_data = \ 66 | { 67 | 'id': video_id, 68 | 'name': video_title, 69 | 'thumbnail': video_thumbnail, 70 | 'duration': video_duration, 71 | 'type': video_type, 72 | 'artist': \ 73 | { 74 | 'id': video_artist_id, 75 | 'name': video_artist_name, 76 | }, 77 | 'album': \ 78 | { 79 | 'id': video_album_id, 80 | }, 81 | 'radio': \ 82 | { 83 | 'params': video_radio_params, 84 | 'playlist_id': video_radio_playlist_id, 85 | }, 86 | } 87 | 88 | songs.append(video_data) 89 | 90 | return songs 91 | -------------------------------------------------------------------------------- /ytm/parsers/search.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the parser: search 3 | ''' 4 | 5 | from ._search import _search as parse_search 6 | from . import decorators 7 | 8 | @decorators.enforce_parameters 9 | @decorators.catch 10 | def search(data: dict) -> dict: 11 | ''' 12 | Parse data: Search. 13 | 14 | Args: 15 | data: Data to parse 16 | 17 | Returns: 18 | Parsed data 19 | 20 | Raises: 21 | ParserError: The parser encountered an error 22 | 23 | Example: 24 | >>> api = ytm.BaseYouTubeMusic() 25 | >>> 26 | >>> data = api.search('bears den') 27 | >>> 28 | >>> parsed_data = ytm.parsers.search(data) 29 | >>> 30 | >>> parsed_data['artists'][0]['name'] 31 | "Bear's Den" 32 | >>> 33 | ''' 34 | 35 | parsed = parse_search(data) 36 | 37 | for shelf_name, shelf in parsed.items(): 38 | if 'items' in shelf: 39 | parsed[shelf_name] = shelf.get('items') 40 | 41 | return parsed 42 | -------------------------------------------------------------------------------- /ytm/parsers/search_albums.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the parser: search_albums 3 | ''' 4 | 5 | from ._search_filter import _search_filter as parse_search_filter 6 | from . import decorators 7 | 8 | @decorators.enforce_parameters 9 | @decorators.catch 10 | def search_albums(data: dict) -> dict: 11 | ''' 12 | Parse data: Search Albums. 13 | 14 | Args: 15 | data: Data to parse 16 | 17 | Returns: 18 | Parsed data 19 | 20 | Raises: 21 | ParserError: The parser encountered an error 22 | 23 | Example: 24 | >>> api = ytm.BaseYouTubeMusic() 25 | >>> 26 | >>> params = ytm.constants.SEARCH_PARAM_PREFIX \ 27 | + ytm.constants.SEARCH_PARAM_ALBUMS \ 28 | + ytm.constants.SEARCH_PARAM_SUFFIX 29 | >>> 30 | >>> data = api.search('bad blood', params = params) 31 | >>> 32 | >>> parsed_data = ytm.parsers.search_albums(data) 33 | >>> 34 | >>> parsed_data['items'][0]['name'] 35 | 'Bad Blood (The Extended Cut)' 36 | >>> 37 | ''' 38 | 39 | return parse_search_filter \ 40 | ( 41 | data = data, 42 | filter = 'albums', 43 | ) 44 | -------------------------------------------------------------------------------- /ytm/parsers/search_artists.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the parser: search_artists 3 | ''' 4 | 5 | from ._search_filter import _search_filter as parse_search_filter 6 | from . import decorators 7 | 8 | @decorators.enforce_parameters 9 | @decorators.catch 10 | def search_artists(data: dict) -> dict: 11 | ''' 12 | Parse data: Search Artists. 13 | 14 | Args: 15 | data: Data to parse 16 | 17 | Returns: 18 | Parsed data 19 | 20 | Raises: 21 | ParserError: The parser encountered an error 22 | 23 | Example: 24 | >>> api = ytm.BaseYouTubeMusic() 25 | >>> 26 | >>> params = ytm.constants.SEARCH_PARAM_PREFIX \ 27 | + ytm.constants.SEARCH_PARAM_ARTISTS \ 28 | + ytm.constants.SEARCH_PARAM_SUFFIX 29 | >>> 30 | >>> data = api.search('easy life', params = params) 31 | >>> 32 | >>> parsed_data = ytm.parsers.search_artists(data) 33 | >>> 34 | >>> parsed_data['items'][0]['name'] 35 | 'Easy Life' 36 | >>> 37 | ''' 38 | 39 | return parse_search_filter \ 40 | ( 41 | data = data, 42 | filter = 'artists', 43 | ) 44 | -------------------------------------------------------------------------------- /ytm/parsers/search_playlists.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the parser: search_playlists 3 | ''' 4 | 5 | from ._search_filter import _search_filter as parse_search_filter 6 | from . import decorators 7 | 8 | @decorators.enforce_parameters 9 | @decorators.catch 10 | def search_playlists(data: dict) -> dict: 11 | ''' 12 | Parse data: Search Playlists. 13 | 14 | Args: 15 | data: Data to parse 16 | 17 | Returns: 18 | Parsed data 19 | 20 | Raises: 21 | ParserError: The parser encountered an error 22 | 23 | Example: 24 | >>> api = ytm.BaseYouTubeMusic() 25 | >>> 26 | >>> params = ytm.constants.SEARCH_PARAM_PREFIX \ 27 | + ytm.constants.SEARCH_PARAM_PLAYLISTS \ 28 | + ytm.constants.SEARCH_PARAM_SUFFIX 29 | >>> 30 | >>> data = api.search('indie rock', params = params) 31 | >>> 32 | >>> parsed_data = ytm.parsers.search_playlists(data) 33 | >>> 34 | >>> parsed_data['items'][0]['name'] 35 | 'Indie Rock Chasers' 36 | >>> 37 | ''' 38 | 39 | return parse_search_filter \ 40 | ( 41 | data = data, 42 | filter = 'playlists', 43 | ) 44 | -------------------------------------------------------------------------------- /ytm/parsers/search_songs.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the parser: search_songs 3 | ''' 4 | 5 | from ._search_filter import _search_filter as parse_search_filter 6 | from . import decorators 7 | 8 | @decorators.enforce_parameters 9 | @decorators.catch 10 | def search_songs(data: dict) -> dict: 11 | ''' 12 | Parse data: Search Songs. 13 | 14 | Args: 15 | data: Data to parse 16 | 17 | Returns: 18 | Parsed data 19 | 20 | Raises: 21 | ParserError: The parser encountered an error 22 | 23 | Example: 24 | >>> api = ytm.BaseYouTubeMusic() 25 | >>> 26 | >>> params = ytm.constants.SEARCH_PARAM_PREFIX \ 27 | + ytm.constants.SEARCH_PARAM_SONGS \ 28 | + ytm.constants.SEARCH_PARAM_SUFFIX 29 | >>> 30 | >>> data = api.search('left hand free', params = params) 31 | >>> 32 | >>> parsed_data = ytm.parsers.search_songs(data) 33 | >>> 34 | >>> parsed_data['items'][0]['name'] 35 | 'Left Hand Free' 36 | >>> 37 | ''' 38 | 39 | return parse_search_filter \ 40 | ( 41 | data = data, 42 | filter = 'songs', 43 | ) 44 | -------------------------------------------------------------------------------- /ytm/parsers/search_suggestions.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the parser: search_suggestions 3 | ''' 4 | 5 | from .. import utils 6 | from . import decorators 7 | 8 | @decorators.enforce_parameters 9 | @decorators.catch 10 | def search_suggestions(data: dict) -> list: 11 | ''' 12 | Parse data: Search Suggestions. 13 | 14 | Args: 15 | data: Data to parse 16 | 17 | Returns: 18 | Parsed data 19 | 20 | Raises: 21 | ParserError: The parser encountered an error 22 | 23 | Example: 24 | >>> api = ytm.BaseYouTubeMusic() 25 | >>> 26 | >>> data = api.search_suggestions('band of') 27 | >>> 28 | >>> parsed_data = ytm.parsers.search_suggestions(data) 29 | >>> 30 | >>> for suggestion in parsed_data: 31 | print(suggestion) 32 | 33 | band of horses the funeral 34 | band of horses 35 | band of heathens hurricane 36 | band of gold 37 | band of brothers theme 38 | band of boys 39 | band of horses no one's gonna love you 40 | >>> 41 | ''' 42 | 43 | assert data, 'No data to parse' 44 | 45 | contents = utils.get \ 46 | ( 47 | data, 48 | 'contents', 49 | 0, 50 | 'searchSuggestionsSectionRenderer', 51 | 'contents', 52 | default = (), 53 | ) 54 | 55 | assert contents, 'No contents' 56 | 57 | suggestions = [] 58 | 59 | for item in contents: 60 | item_runs = utils.get \ 61 | ( 62 | item, 63 | 'searchSuggestionRenderer', 64 | 'suggestion', 65 | 'runs', 66 | default = (), 67 | ) 68 | 69 | if not item_runs: 70 | continue 71 | 72 | item_suggestion = '' 73 | 74 | for item_run in item_runs: 75 | item_run_text = utils.get \ 76 | ( 77 | item_run, 78 | 'text', 79 | ) 80 | 81 | if item_run_text: 82 | item_suggestion += item_run_text 83 | 84 | if item_suggestion: 85 | suggestions.append(item_suggestion) 86 | 87 | return suggestions 88 | -------------------------------------------------------------------------------- /ytm/parsers/search_videos.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the parser: search_videos 3 | ''' 4 | 5 | from ._search_filter import _search_filter as parse_search_filter 6 | from . import decorators 7 | 8 | @decorators.enforce_parameters 9 | @decorators.catch 10 | def search_videos(data: dict) -> dict: 11 | ''' 12 | Parse data: Search Videos. 13 | 14 | Args: 15 | data: Data to parse 16 | 17 | Returns: 18 | Parsed data 19 | 20 | Raises: 21 | ParserError: The parser encountered an error 22 | 23 | Example: 24 | >>> api = ytm.BaseYouTubeMusic() 25 | >>> 26 | >>> params = ytm.constants.SEARCH_PARAM_PREFIX \ 27 | + ytm.constants.SEARCH_PARAM_VIDEOS \ 28 | + ytm.constants.SEARCH_PARAM_SUFFIX 29 | >>> 30 | >>> data = api.search('grace kelly', params = params) 31 | >>> 32 | >>> parsed_data = ytm.parsers.search_videos(data) 33 | >>> 34 | >>> parsed_data['items'][0]['name'] 35 | 'Grace Kelly' 36 | >>> 37 | ''' 38 | 39 | return parse_search_filter \ 40 | ( 41 | data = data, 42 | filter = 'videos', 43 | ) 44 | -------------------------------------------------------------------------------- /ytm/parsers/watch_radio.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the parser: watch_radio 3 | ''' 4 | 5 | from .watch import watch as parse_watch 6 | from . import decorators 7 | 8 | @decorators.enforce_parameters 9 | @decorators.catch 10 | def watch_radio(data: dict) -> dict: 11 | ''' 12 | Parse data: Watch Radio. 13 | 14 | Args: 15 | data: Data to parse 16 | 17 | Returns: 18 | Parsed data 19 | 20 | Raises: 21 | ParserError: The parser encountered an error 22 | 23 | Example: 24 | >>> api = ytm.BaseYouTubeMusic() 25 | >>> 26 | >>> data = api.next \ 27 | ( 28 | playlist_id = 'RDCLAK5uy_kzInc7BXjYqbrGEiqW9fBhOZoroJvfsao', 29 | params = ytm.constants.PARAMS_RADIO, 30 | ) 31 | >>> 32 | >>> parsed_data = ytm.parsers.watch_radio(data) 33 | >>> 34 | >>> parsed_data['tracks'][0]['name'] 35 | 'My Humps' 36 | >>> 37 | ''' 38 | 39 | return parse_watch(data) 40 | -------------------------------------------------------------------------------- /ytm/parsers/watch_shuffle.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the parser: watch_shuffle 3 | ''' 4 | 5 | from .watch import watch as parse_watch 6 | from . import decorators 7 | 8 | @decorators.enforce_parameters 9 | @decorators.catch 10 | def watch_shuffle(data: dict): 11 | ''' 12 | Parse data: Watch Shuffle. 13 | 14 | Args: 15 | data: Data to parse 16 | 17 | Returns: 18 | Parsed data 19 | 20 | Raises: 21 | ParserError: The parser encountered an error 22 | 23 | Example: 24 | >>> api = ytm.BaseYouTubeMusic() 25 | >>> 26 | >>> data = api.next \ 27 | ( 28 | playlist_id = 'RDCLAK5uy_kzInc7BXjYqbrGEiqW9fBhOZoroJvfsao', 29 | params = ytm.constants.PARAMS_SHUFFLE, 30 | ) 31 | >>> 32 | >>> parsed_data = ytm.parsers.watch_shuffle(data) 33 | >>> 34 | >>> parsed_data['tracks'][0]['name'] 35 | 'California Gurls (feat. Snoop Dogg)' 36 | >>> 37 | ''' 38 | 39 | return parse_watch(data) 40 | -------------------------------------------------------------------------------- /ytm/types/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Package containing type classes. 3 | ''' 4 | 5 | from ..utils import include as __include 6 | from . import \ 7 | ( 8 | base, 9 | continuations, 10 | ids, 11 | params, 12 | ) 13 | 14 | __inherit = \ 15 | ( 16 | base, 17 | continuations, 18 | ids, 19 | params, 20 | ) 21 | 22 | locals().update \ 23 | ( 24 | { 25 | key: val 26 | for spec in \ 27 | ( 28 | __spec__, 29 | * \ 30 | ( 31 | getattr(module, '__spec__') 32 | for module in __inherit 33 | ), 34 | ) 35 | for key, val in __include(spec).items() 36 | } 37 | ) 38 | 39 | __all__ = tuple \ 40 | ( 41 | item 42 | for module in __inherit 43 | for item in getattr(module, '__all__') 44 | ) 45 | -------------------------------------------------------------------------------- /ytm/types/base/Continuation.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the base type: Continuation 3 | ''' 4 | 5 | from .TypeB64 import TypeB64 6 | from .. import constants 7 | 8 | class Continuation(TypeB64): 9 | ''' 10 | Base Type: Continuation. 11 | ''' 12 | 13 | pass 14 | -------------------------------------------------------------------------------- /ytm/types/base/Id.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the base type: Id 3 | ''' 4 | 5 | from .TypeStr import TypeStr 6 | from .. import constants 7 | 8 | class Id(TypeStr): 9 | ''' 10 | Base Type: Id. 11 | 12 | Attributes: 13 | _pattern: Regular expression pattern used to extract data 14 | ''' 15 | 16 | _pattern: str = f'^(?P[{constants.CHARS_ID}]*)$' 17 | -------------------------------------------------------------------------------- /ytm/types/base/Params.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the base type: Params 3 | ''' 4 | 5 | from .TypeB64 import TypeB64 6 | from .. import constants 7 | 8 | class Params(TypeB64): 9 | ''' 10 | Base Type: Params. 11 | ''' 12 | 13 | pass 14 | -------------------------------------------------------------------------------- /ytm/types/base/TypeB64.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the base type: TypeB64 3 | ''' 4 | 5 | from .TypeStr import TypeStr 6 | from .. import utils 7 | 8 | import urllib.parse 9 | import base64 10 | import re 11 | from typing import Any 12 | 13 | class TypeB64(TypeStr): 14 | ''' 15 | Base Type: TypeB64. 16 | 17 | This type contains a series of base64 encoded strings and byte sequences. 18 | 19 | Attributes: 20 | _pattern: Regular expression pattern used to extract data 21 | ''' 22 | 23 | _pattern: bytes = b'^(?P.*)$' 24 | 25 | @classmethod 26 | def _parse(cls: type, value: str, pattern: bytes = None) -> dict: 27 | ''' 28 | Parse a string value to extract data. 29 | 30 | In no data is extracted, the value is not a valid instance of this type. 31 | The data contained in the value will have been url-encoded, then 32 | base64-encoded. 33 | 34 | Args: 35 | cls: This class 36 | value: String value to parse 37 | pattern: Pattern used to extract data from the value 38 | 39 | Returns: 40 | Values extracted during parsing 41 | 42 | Example: 43 | >>> TypeB64._parse('Zm9vIGJhci4gMSArIDEgPSAyIQ%3D%3D') 44 | {'data': 'Zm9vIGJhci4gMSArIDEgPSAyIQ%3D%3D', 'parsed': b'foo bar. 1 + 1 = 2!'} 45 | >>> 46 | ''' 47 | 48 | value = str(value) 49 | 50 | if pattern is None: 51 | pattern = cls._pattern 52 | 53 | parsed = {} 54 | 55 | url_decoded = urllib.parse.unquote(value) 56 | 57 | # Replacing - with + has been checked and is correct. 58 | # Is replacing _ with + correct? 59 | url_decoded = url_decoded.replace('-', '+').replace('_', '+') 60 | 61 | if not utils.is_base64(url_decoded): 62 | return parsed 63 | 64 | b64_decoded = base64.b64decode(url_decoded) 65 | 66 | match = re.match \ 67 | ( 68 | pattern = pattern, 69 | string = b64_decoded, 70 | flags = re.DOTALL, 71 | ) 72 | 73 | if match: 74 | parsed = match.groupdict() 75 | 76 | parsed.update \ 77 | ( 78 | { 79 | 'parsed': parsed.get('data'), 80 | 'data': value, 81 | } 82 | ) 83 | 84 | return parsed 85 | 86 | @staticmethod 87 | def _get(groups: dict, type: type, key: str) -> Any: 88 | ''' 89 | Retrieve an extracted value and decode it appropriately. 90 | 91 | Args: 92 | groups: Extracted groups 93 | type: Type of the target data 94 | key: Key of the target data 95 | 96 | Returns: 97 | The value from specified by appropriately decoded 98 | or None if there is no value in 99 | 100 | Example: 101 | >>> import re 102 | >>> 103 | >>> pattern = b'^(?P.{3})(?P.)(?P.)(?P.{2})$' 104 | >>> string = b'foo\x3d\x01\xaf\xbc' 105 | >>> 106 | >>> match = re.match(pattern, string, flags=re.DOTALL) 107 | >>> groups = match.groupdict() 108 | >>> 109 | >>> groups 110 | {'str': b'foo', 'int': b'=', 'bool': b'\x01', 'bytes': b'\xaf\xbc'} 111 | >>> 112 | >>> TypeB64._get(groups, str, 'str') 113 | 'foo' 114 | >>> TypeB64._get(groups, int, 'int') 115 | 61 116 | >>> TypeB64._get(groups, bool, 'bool') 117 | True 118 | >>> TypeB64._get(groups, bytes, 'bytes') 119 | b'\xaf\xbc' 120 | >>> 121 | ''' 122 | 123 | if type is str: 124 | return (groups.get(key) or b'').decode() or None 125 | elif type is bytes: 126 | return (groups.get(key) or b'') or None 127 | elif type is int: 128 | return ord(groups.get(key) or b'\x00') or None 129 | elif type is bool: 130 | return bool(ord(groups.get(key) or b'\x00')) 131 | -------------------------------------------------------------------------------- /ytm/types/base/TypeStr.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the base type: TypeStr 3 | ''' 4 | 5 | from ... import classes 6 | from .. import utils 7 | 8 | import re 9 | 10 | class TypeStr(str, metaclass = classes.BuiltinMeta): 11 | ''' 12 | Base Type: TypeStr. 13 | 14 | Base string type. 15 | 16 | Attributes: 17 | _pattern: Regular expression pattern used to extract data 18 | ''' 19 | 20 | _pattern: str = '^(?P.*)$' 21 | 22 | def __new__(cls: type, value: str) -> object: 23 | ''' 24 | Create a new class instance. 25 | 26 | Args: 27 | cls: This class 28 | value: Value to initialise class with 29 | 30 | Returns: 31 | New class instance 32 | ''' 33 | 34 | parsed = cls._parse(value) 35 | data = parsed.get('data') 36 | 37 | if not parsed: 38 | raise TypeError \ 39 | ( 40 | 'Invalid {class_name}: {value}'.format \ 41 | ( 42 | class_name = cls.__name__, 43 | value = repr(str(value)), 44 | ) 45 | ) 46 | 47 | return super().__new__(cls, data) 48 | 49 | def __repr__(self: object) -> str: 50 | ''' 51 | Create a string representation of the class. 52 | 53 | The representation is of the form: <{class_name}({value})> 54 | 55 | Args: 56 | self: Class instance 57 | 58 | Returns: 59 | String representation of the class 60 | ''' 61 | 62 | return '<{class_name}({value})>'.format \ 63 | ( 64 | class_name = self.__class__.__name__, 65 | value = repr(utils.truncate(str(self))), 66 | ) 67 | 68 | @classmethod 69 | def _parse(cls: type, value: str) -> bool: 70 | ''' 71 | Parse a string value to extract data. 72 | 73 | In no data is extracted, the value is not a valid instance of this type. 74 | 75 | Args: 76 | cls: This class 77 | value: String value to parse 78 | 79 | Returns: 80 | Values extracted during parsing 81 | 82 | Example: 83 | >>> TypeStr._parse('foo bar') 84 | {'data': 'foo bar'} 85 | >>> 86 | ''' 87 | 88 | parsed = {} 89 | groups = {} 90 | 91 | if not isinstance(value, str) or not cls._pattern: 92 | return parsed 93 | 94 | match = re.match \ 95 | ( 96 | pattern = cls._pattern, 97 | string = value, 98 | flags = re.DOTALL, 99 | ) 100 | 101 | if match: 102 | groups = match.groupdict() 103 | 104 | data = groups.get('data') 105 | 106 | if data: 107 | groups['data'] = cls._clean(data) 108 | 109 | return groups 110 | 111 | @classmethod 112 | def _isinstance(cls: type, value: str) -> bool: 113 | ''' 114 | Check whether a string value is a valid instance of this type. 115 | 116 | Args: 117 | cls: This class 118 | value: String value to check 119 | 120 | Returns: 121 | Whether the value is a valid instance of this type 122 | 123 | Example: 124 | >>> class MyStrType(TypeStr): 125 | _pattern = '^(?PABC.{3})$' 126 | 127 | >>> 128 | >>> MyStrType._isinstance('AAAfoo') 129 | False 130 | >>> MyStrType._isinstance('ABCfoo') 131 | True 132 | >>> 133 | ''' 134 | 135 | parsed = cls._parse(value) 136 | data = parsed.get('data') 137 | 138 | return bool(data) and data == value 139 | 140 | @classmethod 141 | def _clean(cls: type, value: str) -> str: 142 | ''' 143 | Clean the data after it has been extracted. 144 | 145 | Args: 146 | cls: This class 147 | value: Data value to clean 148 | 149 | Returns: 150 | Cleaned data value 151 | 152 | Example: 153 | >>> class MyStrType(TypeStr): 154 | _pattern = '^original-prefix(?P.{3})$' 155 | 156 | @classmethod 157 | def _clean(cls, value): 158 | return 'new-prefix' + value 159 | 160 | >>> 161 | >>> my_str = MyStrType('original-prefixABC') 162 | >>> my_str 163 | 164 | >>> 165 | ''' 166 | 167 | return value 168 | -------------------------------------------------------------------------------- /ytm/types/base/Union.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the base type: Union 3 | ''' 4 | 5 | from ... import classes 6 | from .. import utils 7 | 8 | class Union(tuple, metaclass = classes.BuiltinMeta): 9 | ''' 10 | Base Type: Union. 11 | 12 | A Union is a collection of other types. Its main purpose is for docstrings. 13 | 14 | Example: 15 | >>> def foo(value: Union(Continuation, Id, Params)): 16 | """ Returns 'some value' """ 17 | return 'some value' 18 | 19 | >>> 20 | >>> help(foo) 21 | Help on function foo in module __main__: 22 | 23 | foo(value: Union(Continuation, Id, Params)) 24 | Returns 'some value' 25 | 26 | >>> 27 | ''' 28 | 29 | def __new__(cls: type, *types: type) -> object: 30 | ''' 31 | Create a new class instance. 32 | 33 | Args: 34 | cls: This class 35 | *types: Types for the Union 36 | 37 | Returns: 38 | New class instance 39 | ''' 40 | 41 | for item_type in types: 42 | if not isinstance(item_type, type): 43 | raise TypeError \ 44 | ( 45 | 'Invalid {class_name} type: {value}'.format \ 46 | ( 47 | class_name = cls.__name__, 48 | value = repr(str(item_type)), 49 | ) 50 | ) 51 | 52 | return super().__new__(cls, types) 53 | 54 | def __repr__(self: object) -> str: 55 | ''' 56 | Create a string representation of the class. 57 | 58 | The representation is of the form: <{class_name}({types})> 59 | 60 | Args: 61 | self: Class instance 62 | 63 | Returns: 64 | String representation of the class 65 | 66 | Example: 67 | >>> my_union = Union(str, int, bool) 68 | >>> 69 | >>> my_union 70 | Union(str, int, bool) 71 | >>> 72 | ''' 73 | 74 | return '{class_name}({types})'.format \ 75 | ( 76 | class_name = self.__class__.__name__, 77 | types = ', '.join(type.__name__ for type in self) 78 | ) 79 | 80 | def _isinstance(self: object, value: object) -> bool: 81 | ''' 82 | Check whether a value is an instance of any of the types in this union. 83 | 84 | Args: 85 | self: Class instance 86 | value: Value to check 87 | 88 | Returns: 89 | Whether the value is an instance of any of the types in the union 90 | 91 | Example: 92 | >>> my_union = Union(str, int, bool) 93 | >>> 94 | >>> my_union 95 | Union(str, int, bool) 96 | >>> 97 | >>> my_union._isinstance(43) 98 | True 99 | >>> my_union._isinstance(b'foo') 100 | False 101 | >>> 102 | ''' 103 | 104 | for type in self: 105 | if utils.isinstance(value, type): 106 | return True 107 | 108 | return False 109 | -------------------------------------------------------------------------------- /ytm/types/base/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Package containing base types. 3 | 4 | These types form the base of other types and carry out core functionality. 5 | 6 | Example: 7 | >>> from ytm.types import base 8 | >>> 9 | >>> base.__all__ 10 | ... 11 | >>> 12 | ''' 13 | 14 | from ...utils import include as __include 15 | 16 | __all__ = tuple(__include(__spec__)) 17 | -------------------------------------------------------------------------------- /ytm/types/constants/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Package containing constants. 3 | 4 | These constants help the super package achieve general tasks. 5 | 6 | Example: 7 | >>> from ytm import constants 8 | >>> 9 | >>> constants.__all__ 10 | ... 11 | >>> 12 | ''' 13 | 14 | from ...constants import * 15 | 16 | CHARS_ID = '-a-zA-Z0-9_' 17 | CHARS_PARAMS = f'{CHARS_ID}%' 18 | CHARS_CONTINUATION = CHARS_PARAMS 19 | 20 | # Prefixes 21 | PREFIX_ALBUM_ID = 'MPREb_' 22 | PREFIX_ALBUM_PLAYLIST_ID = 'OLAK5uy_' 23 | PREFIX_ARTIST_ID = 'UC' 24 | PREFIX_ARTIST_RADIO_ID = 'RDEM' 25 | PREFIX_ARTIST_SHUFFLE_ID = 'RDAO' 26 | PREFIX_PLAYLIST_BROWSE_ID = 'VL' 27 | PREFIX_PLAYLIST_RADIO_ID = 'RDAMPL' 28 | PREFIX_CHART_PLAYLIST_ID = 'PL' 29 | PREFIX_PLAYLIST_ID = 'RDCLAK5uy_' 30 | PREFIX_SONG_RADIO_ID = 'RDAMVM' 31 | PREFIX_ARTIST_PLAYLIST_ID = PREFIX_PLAYLIST_BROWSE_ID + PREFIX_ALBUM_PLAYLIST_ID 32 | 33 | # Lengths 34 | LEN_ALBUM_ID = 17 35 | LEN_ALBUM_PLAYLIST_ID = 41 36 | LEN_ARTIST_ID = 24 37 | LEN_CHART_PLAYLIST_ID = 34 38 | LEN_PLAYLIST_ID = 43 39 | LEN_SONG_ID = 11 40 | 41 | # Entropy Lengths 42 | LEN_ENTROPY_ALBUM_ID = LEN_ALBUM_ID - len(PREFIX_ALBUM_ID) 43 | LEN_ENTROPY_ALBUM_PLAYLIST_ID = LEN_ALBUM_PLAYLIST_ID - len(PREFIX_ALBUM_PLAYLIST_ID) 44 | LEN_ENTROPY_ARTIST_ID = LEN_ARTIST_ID - len(PREFIX_ARTIST_ID) 45 | LEN_ENTROPY_CHART_PLAYLIST_ID = LEN_CHART_PLAYLIST_ID - len(PREFIX_CHART_PLAYLIST_ID) 46 | LEN_ENTROPY_PLAYLIST_ID = LEN_PLAYLIST_ID - len(PREFIX_PLAYLIST_ID) 47 | -------------------------------------------------------------------------------- /ytm/types/continuations/PlaylistContinuation.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the continuation type: PlaylistContinuation 3 | ''' 4 | 5 | from .. import base 6 | 7 | class PlaylistContinuation(base.Continuation): 8 | ''' 9 | Continuation class: PlaylistContinuation. 10 | 11 | Example: 12 | >>> api = ytm.YouTubeMusic() 13 | >>> 14 | >>> playlist = api.playlist('RDCLAK5uy_lXWhlJsihey6xq1b50d7Uv93NLqle8TSc') 15 | >>> 16 | >>> continuation = ytm.types.PlaylistContinuation(playlist['continuation']) 17 | >>> 18 | >>> continuation 19 | 20 | >>> 21 | ''' 22 | 23 | @classmethod 24 | def _parse(cls: type, value: str) -> dict: 25 | ''' 26 | Parse a continuation string. 27 | 28 | Args: 29 | cls: This class 30 | value: Value to parse 31 | 32 | Returns: 33 | Values extracted during parsing 34 | 35 | Example: 36 | >>> api = ytm.YouTubeMusic() 37 | >>> 38 | >>> playlist = api.playlist('RDCLAK5uy_lXWhlJsihey6xq1b50d7Uv93NLqle8TSc') 39 | >>> 40 | >>> continuation = ytm.types.PlaylistContinuation(playlist['continuation']) 41 | >>> 42 | >>> from pprint import pprint 43 | >>> 44 | >>> parsed = continuation._parse(continuation) 45 | >>> 46 | >>> pprint(parsed) 47 | {'data': '4qmFsgJbEi1WTFJEQ0xBSzV1eV9sWFdobEpzaWhleTZ4cTFiNTBkN1...', 48 | 'params': 'PT:EgtyS1RLVGEzU1o3UQ', 49 | 'playlist_browse_id': 'VLRDCLAK5uy_lXWhlJsihey6xq1b50d7Uv93NLqle8TSc'} 50 | >>> 51 | ''' 52 | 53 | value = str(value) 54 | 55 | pattern_1 = \ 56 | ( 57 | b'^' 58 | b'\xe2\xa9\x85\xb2\x02' 59 | b'.' 60 | b'\x12' 61 | b'(?P.)' 62 | b'(?P.+)' 63 | b'\x1a' 64 | b'(?P.)' 65 | b'(?P.+)' 66 | b'$' 67 | ) 68 | pattern_2 = \ 69 | ( 70 | b'^' 71 | b'z' 72 | b'(?P.)' 73 | b'(?P.+)' 74 | b'\x92\x01\x03\x08\xba\x04' 75 | ) 76 | 77 | data = {} 78 | 79 | parsed_1 = super()._parse(value, pattern_1) 80 | 81 | if not parsed_1: 82 | return data 83 | 84 | len_playlist_browse_id = cls._get(parsed_1, int, 'len_playlist_browse_id') 85 | playlist_browse_id = cls._get(parsed_1, str, 'playlist_browse_id') 86 | len_suffix = cls._get(parsed_1, int, 'len_suffix') 87 | suffix = cls._get(parsed_1, str, 'suffix') 88 | 89 | lengths_1 = \ 90 | ( 91 | (playlist_browse_id, len_playlist_browse_id), 92 | (suffix, len_suffix), 93 | ) 94 | 95 | for item, length in lengths_1: 96 | if not item or len(item) != length: 97 | return data 98 | 99 | if not playlist_browse_id or len(playlist_browse_id) != len_playlist_browse_id: 100 | return data 101 | 102 | parsed_2 = super()._parse(suffix, pattern_2) 103 | 104 | if not parsed_2: 105 | return data 106 | 107 | len_params = cls._get(parsed_2, int, 'len_params') 108 | params = cls._get(parsed_2, str, 'params') 109 | 110 | if not params or len(params) != len_params: 111 | return data 112 | 113 | data = \ 114 | { 115 | 'playlist_browse_id': playlist_browse_id, 116 | 'params': params, 117 | 'data': value, 118 | } 119 | 120 | return data 121 | -------------------------------------------------------------------------------- /ytm/types/continuations/SearchContinuation.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the continuation type: SearchContinuation 3 | ''' 4 | 5 | from .. import base 6 | from .. import constants 7 | 8 | class SearchContinuation(base.Continuation): 9 | ''' 10 | Continuation class: SearchContinuation. 11 | 12 | Example: 13 | >>> api = ytm.YouTubeMusic() 14 | >>> 15 | >>> albums = api.search_albums('strange trails') 16 | >>> 17 | >>> continuation = ytm.types.SearchContinuation(albums['continuation']) 18 | >>> 19 | >>> continuation 20 | 21 | >>> 22 | ''' 23 | 24 | @classmethod 25 | def _parse(cls: type, value: str) -> dict: 26 | ''' 27 | Parse a continuation string. 28 | 29 | Args: 30 | cls: This class 31 | value: Value to parse 32 | 33 | Returns: 34 | Values extracted during parsing 35 | 36 | Example: 37 | >>> api = ytm.YouTubeMusic() 38 | >>> 39 | >>> albums = api.search_albums('strange trails') 40 | >>> 41 | >>> continuation = ytm.types.SearchContinuation(albums['continuation']) 42 | >>> 43 | >>> from pprint import pprint 44 | >>> 45 | >>> parsed = continuation._parse(continuation) 46 | >>> 47 | >>> pprint(parsed) 48 | {'data': '4qmFsgJbEi1WTFJEQ0xBSzV1eV9sWFdobEpzaWhleTZ4cTFiNTBkN1...', 49 | 'params': 'PT:EgtyS1RLVGEzU1o3UQ', 50 | 'playlist_browse_id': 'VLRDCLAK5uy_lXWhlJsihey6xq1b50d7Uv93NLqle8TSc'} 51 | >>> 52 | ''' 53 | 54 | value = str(value) 55 | 56 | pattern_1 = b''.join \ 57 | ( 58 | ( 59 | b'^' 60 | b'\x12' 61 | b'.{2}' 62 | b'\x12' 63 | b'(?P.)' 64 | b'(?P.+)' 65 | b'\x1a' 66 | b'.{2}' 67 | b'Eg-KAQwIA', 68 | f'(?P{"|".join(constants.SEARCH_PARAMS_MAP_REV)})'.encode(), 69 | b'MABI' 70 | b'.' 71 | b'GoKEA' 72 | b'.' 73 | b'QBBA' 74 | b'.' 75 | b'EA' 76 | b'.' 77 | b'QBYIB' 78 | b'(?P.+)' 79 | b'\x18\xf1\xea\xd0' 80 | b'\.' 81 | b'$' 82 | ) 83 | ) 84 | pattern_2 = b'^(?P.*)$' 85 | 86 | data = {} 87 | 88 | parsed_1 = super()._parse(value, pattern_1) 89 | 90 | if not parsed_1: 91 | return data 92 | 93 | len_query = cls._get(parsed_1, int, 'len_query') 94 | query = cls._get(parsed_1, str, 'query') 95 | param = cls._get(parsed_1, str, 'param') 96 | suffix = cls._get(parsed_1, str, 'suffix') 97 | 98 | if not query or len(query) != len_query: 99 | return data 100 | 101 | filter = constants.SEARCH_PARAMS_MAP_REV.get(param) 102 | 103 | parsed_2 = super()._parse(suffix, pattern_2) 104 | 105 | if not parsed_2: 106 | return data 107 | 108 | entropy = cls._get(parsed_2, bytes, 'entropy') 109 | 110 | ids = [] 111 | 112 | for segment in entropy.split(b'\x82\x01'): 113 | id_len = segment[0] 114 | id = segment[1:].decode() 115 | 116 | if len(segment) != id_len + 1: 117 | return False 118 | 119 | # Check ID is valid format here 120 | 121 | ids.append(id) 122 | 123 | data = \ 124 | { 125 | 'query': query, 126 | 'param': param, 127 | 'filter': filter, 128 | 'ids': ids, 129 | 'data': value, 130 | } 131 | 132 | return data 133 | -------------------------------------------------------------------------------- /ytm/types/continuations/WatchContinuation.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the continuation type: WatchContinuation 3 | ''' 4 | 5 | from .. import base 6 | 7 | class WatchContinuation(base.Continuation): 8 | ''' 9 | Continuation class: WatchContinuation. 10 | 11 | Example: 12 | >>> api = ytm.YouTubeMusic() 13 | >>> 14 | >>> watch = api.watch('Kil4Abhm9KA') 15 | >>> 16 | >>> continuation = ytm.types.WatchContinuation(watch['continuation']) 17 | >>> 18 | >>> continuation 19 | 20 | >>> 21 | ''' 22 | 23 | @classmethod 24 | def _parse(cls: type, value: str) -> dict: 25 | ''' 26 | Parse a continuation string. 27 | 28 | Args: 29 | cls: This class 30 | value: Value to parse 31 | 32 | Returns: 33 | Values extracted during parsing 34 | 35 | Example: 36 | >>> api = ytm.YouTubeMusic() 37 | >>> 38 | >>> watch = api.watch('Kil4Abhm9KA') 39 | >>> 40 | >>> continuation = ytm.types.WatchContinuation(watch['continuation']) 41 | >>> 42 | >>> from pprint import pprint 43 | >>> 44 | >>> parsed = continuation._parse(continuation) 45 | >>> 46 | >>> pprint(parsed) 47 | {'data': 'CBkSMRILaHFYaFBfX2lHT1EiEVJEQU1WTUtpbDRBYmhtOUtBMgR3QU...', 48 | 'params': 'wAEB', 49 | 'playlist_id': 'RDAMVMKil4Abhm9KA', 50 | 'video_id': 'hqXhP__iGOQ'} 51 | >>> 52 | ''' 53 | 54 | value = str(value) 55 | 56 | pattern = \ 57 | ( 58 | b'\x08\x19\x12' 59 | b'.' 60 | b'\x12' 61 | b'(?P.)' 62 | b'(?P.{11})' 63 | b'"' 64 | b'(?P.)' 65 | b'(?P.+)' 66 | b'2' 67 | b'(?P.)' 68 | b'(?P.+)' 69 | b'8' 70 | b'\x18\xb8\x01\x02\xd0\x01\x01\xf0\x01\x01\x18\n' 71 | ) 72 | 73 | data = {} 74 | 75 | parsed = super()._parse(value, pattern) 76 | 77 | if not parsed: 78 | return data 79 | 80 | len_video_id = cls._get(parsed, int, 'len_video_id') 81 | len_playlist_id = cls._get(parsed, int, 'len_playlist_id') 82 | len_params = cls._get(parsed, int, 'len_params') 83 | video_id = cls._get(parsed, str, 'video_id') 84 | playlist_id = cls._get(parsed, str, 'playlist_id') 85 | params = cls._get(parsed, str, 'params') 86 | 87 | lengths = \ 88 | ( 89 | (video_id, len_video_id), 90 | (playlist_id, len_playlist_id), 91 | (params, len_params), 92 | ) 93 | 94 | for item, length in lengths: 95 | if not item or len(item) != length: 96 | return data 97 | 98 | data = \ 99 | { 100 | 'video_id': video_id, # Check format | next video id? 101 | 'playlist_id': playlist_id, # Check format 102 | 'params': params, # Check format 103 | 'data': value, 104 | } 105 | 106 | return data 107 | -------------------------------------------------------------------------------- /ytm/types/continuations/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Package containing Continuation types. 3 | ''' 4 | 5 | from ...utils import include as __include 6 | 7 | __all__ = tuple(__include(__spec__)) 8 | -------------------------------------------------------------------------------- /ytm/types/ids/AlbumBrowseId.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the Id type: AlbumBrowseId 3 | ''' 4 | 5 | from .AlbumId import AlbumId 6 | 7 | class AlbumBrowseId(AlbumId): 8 | ''' 9 | Id class: AlbumBrowseId 10 | 11 | Example: 12 | >>> id = AlbumBrowseId('MPREb_K8qWMWVqXGi') 13 | >>> 14 | >>> id 15 | 16 | >>> 17 | >>> str(id) 18 | 'MPREb_K8qWMWVqXGi' 19 | >>> 20 | ''' 21 | 22 | pass 23 | -------------------------------------------------------------------------------- /ytm/types/ids/AlbumId.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the Id type: AlbumId 3 | ''' 4 | 5 | from .. import base 6 | from .. import constants 7 | 8 | class AlbumId(base.Id): 9 | ''' 10 | Id class: AlbumId 11 | 12 | Example: 13 | >>> id = AlbumId('MPREb_K8qWMWVqXGi') 14 | >>> 15 | >>> id 16 | 17 | >>> 18 | >>> str(id) 19 | 'MPREb_K8qWMWVqXGi' 20 | >>> 21 | ''' 22 | 23 | _pattern: str = '^(?P{prefix}[{chars}]{{{entropy_length}}})$'.format \ 24 | ( 25 | prefix = constants.PREFIX_ALBUM_ID, 26 | chars = constants.CHARS_ID, 27 | entropy_length = constants.LEN_ENTROPY_ALBUM_ID, 28 | ) 29 | -------------------------------------------------------------------------------- /ytm/types/ids/AlbumPlaylistBrowseId.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the Id type: AlbumPlaylistBrowseId 3 | ''' 4 | 5 | from .AlbumPlaylistId import AlbumPlaylistId 6 | 7 | class AlbumPlaylistBrowseId(AlbumPlaylistId): 8 | ''' 9 | Id class: AlbumPlaylistBrowseId 10 | 11 | Example: 12 | >>> id = AlbumPlaylistBrowseId('OLAK5uy_nZZjkBu_E4olFSb5Ey-fQ-4a0ZCqJICdQ') 13 | >>> 14 | >>> id 15 | 16 | >>> 17 | >>> str(id) 18 | 'OLAK5uy_nZZjkBu_E4olFSb5Ey-fQ-4a0ZCqJICdQ' 19 | >>> 20 | ''' 21 | 22 | pass 23 | -------------------------------------------------------------------------------- /ytm/types/ids/AlbumPlaylistId.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the Id type: AlbumPlaylistId 3 | ''' 4 | 5 | from .. import base 6 | from .. import constants 7 | 8 | class AlbumPlaylistId(base.Id): 9 | ''' 10 | Id class: AlbumPlaylistId 11 | 12 | Example: 13 | >>> id = AlbumPlaylistId('OLAK5uy_nZZjkBu_E4olFSb5Ey-fQ-4a0ZCqJICdQ') 14 | >>> 15 | >>> id 16 | 17 | >>> 18 | >>> str(id) 19 | 'OLAK5uy_nZZjkBu_E4olFSb5Ey-fQ-4a0ZCqJICdQ' 20 | >>> 21 | ''' 22 | 23 | _pattern = '^(?P{prefixes})?(?P{prefix}[{chars}]{{{entropy_length}}})$'.format \ 24 | ( 25 | prefixes = constants.PREFIX_PLAYLIST_RADIO_ID, 26 | prefix = constants.PREFIX_ALBUM_PLAYLIST_ID, 27 | chars = constants.CHARS_ID, 28 | entropy_length = constants.LEN_ENTROPY_ALBUM_PLAYLIST_ID, 29 | ) 30 | -------------------------------------------------------------------------------- /ytm/types/ids/AlbumRadioId.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the Id type: AlbumRadioId 3 | ''' 4 | 5 | from .AlbumPlaylistId import AlbumPlaylistId 6 | from .. import constants 7 | 8 | class AlbumRadioId(AlbumPlaylistId): 9 | ''' 10 | Id class: AlbumRadioId 11 | 12 | Example: 13 | >>> id = AlbumRadioId('RDAMPLOLAK5uy_nZZjkBu_E4olFSb5Ey-fQ-4a0ZCqJICdQ') 14 | >>> 15 | >>> id 16 | 17 | >>> 18 | >>> str(id) 19 | 'RDAMPLOLAK5uy_nZZjkBu_E4olFSb5Ey-fQ-4a0ZCqJICdQ' 20 | >>> 21 | ''' 22 | 23 | @classmethod 24 | def _clean(cls: type, value: str) -> dict: 25 | ''' 26 | Clean the extracted data value. 27 | 28 | Append the playlist radio prefix to the value. 29 | 30 | Args: 31 | cls: This class 32 | value: The extracted data value to clean 33 | 34 | Returns: 35 | Cleaned value 36 | 37 | Example: 38 | >>> AlbumRadioId._clean('OLAK5uy_nZZjkBu_E4olFSb5Ey-fQ-4a0ZCqJICdQ') 39 | 'RDAMPLOLAK5uy_nZZjkBu_E4olFSb5Ey-fQ-4a0ZCqJICdQ' 40 | >>> 41 | ''' 42 | 43 | return constants.PREFIX_PLAYLIST_RADIO_ID + value 44 | -------------------------------------------------------------------------------- /ytm/types/ids/AlbumShuffleId.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the Id type: AlbumShuffleId 3 | ''' 4 | 5 | from .AlbumPlaylistId import AlbumPlaylistId 6 | 7 | class AlbumShuffleId(AlbumPlaylistId): 8 | ''' 9 | Id class: AlbumShuffleId 10 | 11 | Example: 12 | >>> id = AlbumShuffleId('OLAK5uy_nZZjkBu_E4olFSb5Ey-fQ-4a0ZCqJICdQ') 13 | >>> 14 | >>> id 15 | 16 | >>> 17 | >>> str(id) 18 | 'OLAK5uy_nZZjkBu_E4olFSb5Ey-fQ-4a0ZCqJICdQ' 19 | >>> 20 | ''' 21 | 22 | pass 23 | -------------------------------------------------------------------------------- /ytm/types/ids/ArtistBrowseId.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the Id type: ArtistBrowseId 3 | ''' 4 | 5 | from .ArtistId import ArtistId 6 | 7 | class ArtistBrowseId(ArtistId): 8 | ''' 9 | Id class: ArtistBrowseId 10 | 11 | Example: 12 | >>> id = ArtistBrowseId('UCRr1xG_2WIDs18a6cIiCxeA') 13 | >>> 14 | >>> id 15 | 16 | >>> 17 | >>> str(id) 18 | 'UCRr1xG_2WIDs18a6cIiCxeA' 19 | >>> 20 | ''' 21 | 22 | pass 23 | -------------------------------------------------------------------------------- /ytm/types/ids/ArtistId.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the Id type: ArtistId 3 | ''' 4 | 5 | from .. import base 6 | from .. import constants 7 | 8 | class ArtistId(base.Id): 9 | ''' 10 | Id class: ArtistId 11 | 12 | Example: 13 | >>> id = ArtistId('UCRr1xG_2WIDs18a6cIiCxeA') 14 | >>> 15 | >>> id 16 | 17 | >>> 18 | >>> str(id) 19 | 'UCRr1xG_2WIDs18a6cIiCxeA' 20 | >>> 21 | ''' 22 | 23 | _pattern: str = '^(?P{prefix}[{chars}]{{{entropy_length}}})$'.format \ 24 | ( 25 | prefix = constants.PREFIX_ARTIST_ID, 26 | chars = constants.CHARS_ID, 27 | entropy_length = constants.LEN_ENTROPY_ARTIST_ID, 28 | ) 29 | -------------------------------------------------------------------------------- /ytm/types/ids/ArtistRadioId.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the Id type: ArtistRadioId 3 | ''' 4 | 5 | from .. import base 6 | from .. import constants 7 | 8 | class ArtistRadioId(base.Id): 9 | ''' 10 | Id class: ArtistRadioId 11 | 12 | Example: 13 | >>> id = ArtistRadioId('RDEMHSpo_Uv9STIRtF73zMywLg') 14 | >>> 15 | >>> id 16 | 17 | >>> 18 | >>> str(id) 19 | 'RDEMHSpo_Uv9STIRtF73zMywLg' 20 | >>> 21 | ''' 22 | 23 | _pattern: str = '^(?P{prefixes})(?P[{chars}]{{{entropy_length}}})$'.format \ 24 | ( 25 | prefixes = '|'.join \ 26 | ( 27 | ( 28 | constants.PREFIX_ARTIST_RADIO_ID, 29 | constants.PREFIX_ARTIST_SHUFFLE_ID, 30 | ), 31 | ), 32 | chars = constants.CHARS_ID, 33 | entropy_length = constants.LEN_ENTROPY_ARTIST_ID, 34 | ) 35 | 36 | @classmethod 37 | def _clean(cls: type, value: str) -> str: 38 | ''' 39 | Clean the extracted data value. 40 | 41 | Append the artist radio prefix to the value. 42 | 43 | Args: 44 | cls: This class 45 | value: The extracted data value to clean 46 | 47 | Returns: 48 | Cleaned value 49 | 50 | Example: 51 | >>> ArtistRadioId._clean('HSpo_Uv9STIRtF73zMywLg') 52 | 'RDEMHSpo_Uv9STIRtF73zMywLg' 53 | >>> 54 | ''' 55 | 56 | return constants.PREFIX_ARTIST_RADIO_ID + value 57 | -------------------------------------------------------------------------------- /ytm/types/ids/ArtistShuffleId.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the Id type: ArtistShuffleId 3 | ''' 4 | 5 | from .ArtistRadioId import ArtistRadioId 6 | from .. import constants 7 | 8 | class ArtistShuffleId(ArtistRadioId): 9 | ''' 10 | Id class: ArtistShuffleId 11 | 12 | Example: 13 | >>> id = ArtistShuffleId('RDAOHSpo_Uv9STIRtF73zMywLg') 14 | >>> 15 | >>> id 16 | 17 | >>> 18 | >>> str(id) 19 | 'RDAOHSpo_Uv9STIRtF73zMywLg' 20 | >>> 21 | ''' 22 | 23 | @classmethod 24 | def _clean(cls: type, value: str) -> str: 25 | ''' 26 | Clean the extracted data value. 27 | 28 | Append the artist shuffle prefix to the value. 29 | 30 | Args: 31 | cls: This class 32 | value: The extracted data value to clean 33 | 34 | Returns: 35 | Cleaned value 36 | 37 | Example: 38 | >>> ArtistShuffleId._clean('HSpo_Uv9STIRtF73zMywLg') 39 | 'RDAOHSpo_Uv9STIRtF73zMywLg' 40 | >>> 41 | ''' 42 | 43 | return constants.PREFIX_ARTIST_SHUFFLE_ID + value 44 | -------------------------------------------------------------------------------- /ytm/types/ids/ArtistSongsPlaylistId.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the Id type: ArtistSongsPlaylistId 3 | ''' 4 | 5 | from .. import base 6 | from .. import constants 7 | 8 | class ArtistSongsPlaylistId(base.Id): 9 | ''' 10 | Id class: ArtistSongsPlaylistId 11 | 12 | Example: 13 | >>> id = ArtistSongsPlaylistId('VLOLAK5uy_mZabX2FA-77tDc8EpOSgCG-O-f37h7uFc') 14 | >>> 15 | >>> id 16 | 17 | >>> 18 | >>> str(id) 19 | 'VLOLAK5uy_mZabX2FA-77tDc8EpOSgCG-O-f37h7uFc' 20 | >>> 21 | ''' 22 | 23 | _pattern: str = '^(?P{prefix}[{chars}]{{{entropy_length}}})$'.format \ 24 | ( 25 | prefix = constants.PREFIX_ARTIST_PLAYLIST_ID, 26 | chars = constants.CHARS_ID, 27 | entropy_length = constants.LEN_ENTROPY_PLAYLIST_ID, 28 | ) 29 | -------------------------------------------------------------------------------- /ytm/types/ids/PlaylistBrowseId.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the Id type: PlaylistBrowseId 3 | ''' 4 | 5 | from .PlaylistId import PlaylistId 6 | from .. import constants 7 | 8 | class PlaylistBrowseId(PlaylistId): 9 | ''' 10 | Id class: PlaylistBrowseId 11 | 12 | Example: 13 | >>> id = PlaylistBrowseId('VLRDCLAK5uy_kqSXUdZBZlDrNwxWgVm3xlQ7q0I6h9Zsc') 14 | >>> 15 | >>> id 16 | 17 | >>> 18 | >>> str(id) 19 | 'VLRDCLAK5uy_kqSXUdZBZlDrNwxWgVm3xlQ7q0I6h9Zsc' 20 | >>> 21 | ''' 22 | 23 | @classmethod 24 | def _clean(cls: type, value: str) -> str: 25 | ''' 26 | Clean the extracted data value. 27 | 28 | Append the playlist browse id prefix to the value. 29 | 30 | Args: 31 | cls: This class 32 | value: The extracted data value to clean 33 | 34 | Returns: 35 | Cleaned value 36 | 37 | Example: 38 | >>> PlaylistBrowseId._clean('RDCLAK5uy_kqSXUdZBZlDrNwxWgVm3xlQ7q0I6h9Zsc') 39 | 'VLRDCLAK5uy_kqSXUdZBZlDrNwxWgVm3xlQ7q0I6h9Zsc' 40 | >>> 41 | ''' 42 | 43 | return constants.PREFIX_PLAYLIST_BROWSE_ID + super()._clean(value) 44 | -------------------------------------------------------------------------------- /ytm/types/ids/PlaylistId.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the Id type: PlaylistId 3 | ''' 4 | 5 | from .. import base 6 | from .. import constants 7 | 8 | class PlaylistId(base.Id): 9 | ''' 10 | Id class: PlaylistId 11 | 12 | Example: 13 | >>> id = PlaylistId('RDCLAK5uy_kqSXUdZBZlDrNwxWgVm3xlQ7q0I6h9Zsc') 14 | >>> 15 | >>> id 16 | 17 | >>> 18 | >>> str(id) 19 | 'RDCLAK5uy_kqSXUdZBZlDrNwxWgVm3xlQ7q0I6h9Zsc' 20 | >>> 21 | ''' 22 | 23 | _pattern: str = \ 24 | ( 25 | '^(?P{prefixes})?(?P{patterns})$' 26 | ).format \ 27 | ( 28 | prefixes = '|'.join \ 29 | ( 30 | ( 31 | constants.PREFIX_PLAYLIST_BROWSE_ID, 32 | constants.PREFIX_PLAYLIST_RADIO_ID, 33 | ), 34 | ), 35 | patterns = '|'.join \ 36 | ( 37 | '(?:{prefix}[{chars}]{{{entropy_length}}})'.format(**data) 38 | for data in \ 39 | ( 40 | { 41 | 'prefix': constants.PREFIX_PLAYLIST_ID, 42 | 'chars': constants.CHARS_ID, 43 | 'entropy_length': constants.LEN_ENTROPY_PLAYLIST_ID, 44 | }, 45 | { 46 | 'prefix': constants.PREFIX_CHART_PLAYLIST_ID, 47 | 'chars': constants.CHARS_ID, 48 | 'entropy_length': constants.LEN_ENTROPY_CHART_PLAYLIST_ID, 49 | }, 50 | ) 51 | ), 52 | ) 53 | -------------------------------------------------------------------------------- /ytm/types/ids/PlaylistRadioId.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the Id type: PlaylistRadioId 3 | ''' 4 | 5 | from .PlaylistId import PlaylistId 6 | from .. import constants 7 | 8 | class PlaylistRadioId(PlaylistId): 9 | ''' 10 | Id class: PlaylistRadioId 11 | 12 | Example: 13 | >>> id = PlaylistRadioId('RDAMPLRDCLAK5uy_kqSXUdZBZlDrNwxWgVm3xlQ7q0I6h9Zsc') 14 | >>> 15 | >>> id 16 | 17 | >>> 18 | >>> str(id) 19 | 'RDAMPLRDCLAK5uy_kqSXUdZBZlDrNwxWgVm3xlQ7q0I6h9Zsc' 20 | >>> 21 | ''' 22 | 23 | @classmethod 24 | def _clean(cls: type, value: str) -> str: 25 | ''' 26 | Clean the extracted data value. 27 | 28 | Append the playlist radio prefix to the value. 29 | 30 | Args: 31 | cls: This class 32 | value: The extracted data value to clean 33 | 34 | Returns: 35 | Cleaned value 36 | 37 | Example: 38 | >>> PlaylistRadioId._clean('RDCLAK5uy_kqSXUdZBZlDrNwxWgVm3xlQ7q0I6h9Zsc') 39 | 'RDAMPLRDCLAK5uy_kqSXUdZBZlDrNwxWgVm3xlQ7q0I6h9Zsc' 40 | >>> 41 | ''' 42 | 43 | return constants.PREFIX_PLAYLIST_RADIO_ID + super()._clean(value) 44 | -------------------------------------------------------------------------------- /ytm/types/ids/PlaylistShuffleId.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the Id type: PlaylistShuffleId 3 | ''' 4 | 5 | from .PlaylistId import PlaylistId 6 | 7 | class PlaylistShuffleId(PlaylistId): 8 | ''' 9 | Id class: PlaylistShuffleId 10 | 11 | Example: 12 | >>> id = PlaylistShuffleId('RDCLAK5uy_kqSXUdZBZlDrNwxWgVm3xlQ7q0I6h9Zsc') 13 | >>> 14 | >>> id 15 | 16 | >>> 17 | >>> str(id) 18 | 'RDCLAK5uy_kqSXUdZBZlDrNwxWgVm3xlQ7q0I6h9Zsc' 19 | >>> 20 | ''' 21 | 22 | pass 23 | -------------------------------------------------------------------------------- /ytm/types/ids/SongId.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the Id type: SongId 3 | ''' 4 | 5 | from .. import base 6 | from .. import constants 7 | 8 | class SongId(base.Id): 9 | ''' 10 | Id class: SongId 11 | 12 | Example: 13 | >>> id = SongId('XnwpXfwXp6w') 14 | >>> 15 | >>> id 16 | 17 | >>> 18 | >>> str(id) 19 | 'XnwpXfwXp6w' 20 | >>> 21 | ''' 22 | 23 | _pattern = '^(?P{prefixes})?(?P[{chars}]{{{length}}})$'.format \ 24 | ( 25 | prefixes = constants.PREFIX_SONG_RADIO_ID, 26 | chars = constants.CHARS_ID, 27 | length = constants.LEN_SONG_ID, 28 | ) 29 | -------------------------------------------------------------------------------- /ytm/types/ids/SongRadioId.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the Id type: SongRadioId 3 | ''' 4 | 5 | from .SongId import SongId 6 | from .. import constants 7 | 8 | class SongRadioId(SongId): 9 | ''' 10 | Id class: SongRadioId 11 | 12 | Example: 13 | >>> id = SongRadioId('RDAMVMXnwpXfwXp6w') 14 | >>> 15 | >>> id 16 | 17 | >>> 18 | >>> str(id) 19 | 'RDAMVMXnwpXfwXp6w' 20 | >>> 21 | ''' 22 | 23 | @classmethod 24 | def _clean(cls: type, value: str) -> str: 25 | ''' 26 | Clean the extracted data value. 27 | 28 | Append the song radio prefix to the value. 29 | 30 | Args: 31 | cls: This class 32 | value: The extracted data value to clean 33 | 34 | Returns: 35 | Cleaned value 36 | 37 | Example: 38 | >>> SongRadioId._clean('XnwpXfwXp6w') 39 | 'RDAMVMXnwpXfwXp6w' 40 | >>> 41 | ''' 42 | 43 | return constants.PREFIX_SONG_RADIO_ID + super()._clean(value) 44 | -------------------------------------------------------------------------------- /ytm/types/ids/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Package containing Id types. 3 | ''' 4 | 5 | from ...utils import include as __include 6 | 7 | __all__ = tuple(__include(__spec__)) 8 | -------------------------------------------------------------------------------- /ytm/types/params/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Package containing Params types. 3 | ''' 4 | 5 | from ...utils import include as __include 6 | 7 | __all__ = tuple(__include(__spec__)) 8 | -------------------------------------------------------------------------------- /ytm/types/utils/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Package containing utility functions. 3 | 4 | These utility functions range in purpose, but help the super package 5 | achieve general tasks. 6 | 7 | Example: 8 | >>> from ytm.types import utils 9 | >>> 10 | >>> utils.__all__ 11 | ... 12 | >>> 13 | ''' 14 | 15 | from ... import utils as __utils 16 | 17 | locals().update(__utils.include(__utils.__spec__)) 18 | 19 | __all__ = \ 20 | ( 21 | *tuple(__utils.include(__spec__)), 22 | ) 23 | -------------------------------------------------------------------------------- /ytm/types/utils/is_base64.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the utility function: is_base64 3 | ''' 4 | 5 | import string 6 | import re 7 | 8 | def is_base64(data: str) -> bool: 9 | ''' 10 | Check whether some data is a valid base64 string. 11 | 12 | The data is valid base64 if its length is a multiple of 4, has only been 13 | padded with the '=' character and contains only ascii letters, digits or 14 | the '+' character. 15 | 16 | Args: 17 | data: Data to check 18 | 19 | Returns: 20 | Whether the data is a valid base64 string 21 | 22 | Example: 23 | >>> is_base64('foo') 24 | False 25 | >>> is_base64('foo=') 26 | True 27 | >>> 28 | ''' 29 | 30 | characters = string.ascii_letters + string.digits + '+/' 31 | character_pad = '=' 32 | 33 | if not re.match(f'^[{characters}]+{character_pad}*$', data): 34 | return False 35 | 36 | return len(data) % 4 == 0 37 | -------------------------------------------------------------------------------- /ytm/types/utils/pad_base64.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the utility function: pad_base64 3 | ''' 4 | 5 | def pad_base64(data: str) -> bool: 6 | ''' 7 | Pad some base64 data. 8 | 9 | Args: 10 | data: Data to pad 11 | 12 | Returns: 13 | Padded data 14 | 15 | Example: 16 | >>> pad_base64('foo') 17 | 'foo=' 18 | >>> pad_base64('abcdabcd') 19 | 'abcdabcd' 20 | >>> 21 | ''' 22 | 23 | mod = len(data) % 4 24 | 25 | if mod: 26 | data += '=' * (4 - mod) 27 | 28 | return data 29 | -------------------------------------------------------------------------------- /ytm/types/utils/truncate.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the utility function: truncate 3 | ''' 4 | 5 | def truncate(string: str, length: int = 50, suffix: str = '...') -> str: 6 | ''' 7 | Truncate a string. 8 | 9 | Args: 10 | string: String to truncate 11 | length: Maximum string length before it gets truncated 12 | Default: 50 13 | suffix: String added to the end to indicate the data has been truncated 14 | Default: '...' 15 | 16 | Returns: 17 | if len(string) < length else truncated using 18 | to indicate more data exists 19 | 20 | Example: 21 | >>> truncate('foo') 22 | 'foo' 23 | >>> truncate('foo' * 20) 24 | 'foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofo...' 25 | >>> 26 | >>> truncate('foo' * 20, length = 30, suffix = '(tbc.)') 27 | 'foofoofoofoofoofoofoofoo(tbc.)' 28 | >>> 29 | ''' 30 | 31 | if len(string) < length: 32 | return string 33 | else: 34 | return string[:length - len(suffix)] + suffix 35 | -------------------------------------------------------------------------------- /ytm/utils/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Package containing utility functions. 3 | 4 | These utility functions range in purpose, but help the super package 5 | achieve general tasks. 6 | 7 | Example: 8 | >>> from ytm import utils 9 | >>> 10 | >>> utils.__all__ 11 | ... 12 | >>> 13 | ''' 14 | 15 | from .include import include as __include 16 | 17 | __all__ = tuple(__include(__spec__)) 18 | -------------------------------------------------------------------------------- /ytm/utils/_url.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the utility function: _url 3 | ''' 4 | 5 | import urllib.parse 6 | 7 | def _url(protocol: str, domain: str, *endpoints: str, params: dict = None) -> str: 8 | ''' 9 | Create a URL. 10 | 11 | Formulates a string URL using the arguments provided. 12 | 13 | Args: 14 | protocol: Domain protocol 15 | Example: 'http' or 'https' 16 | domain: Domain name 17 | Example: 'google.com' 18 | *endpoints: URL endpoints 19 | Example: ['directory', 'file.ext'] 20 | params: URL query string parameters 21 | Example: {'username': 'admin', 'password': 'Password1'} 22 | 23 | Returns: 24 | A URL 25 | 26 | Example: 27 | >>> _url('http', 'www.google.co.uk', 'search', params={'q': 'test'}) 28 | 'http://www.google.co.uk/search?q=test' 29 | >>> 30 | ''' 31 | 32 | return '{protocol}://{domain}/{endpoint}{parameters}'.format \ 33 | ( 34 | protocol = protocol, 35 | domain = domain, 36 | endpoint = '/'.join(map(str.strip, endpoints)) if endpoints else '', 37 | parameters = f'?{urllib.parse.urlencode(params)}' if params else '', 38 | ) 39 | -------------------------------------------------------------------------------- /ytm/utils/filter.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the utility function: filter 3 | ''' 4 | 5 | from typing import Callable, Iterable 6 | 7 | def filter \ 8 | ( 9 | iterable: Iterable, 10 | func: Callable = None, 11 | ) -> Iterable: 12 | ''' 13 | Filter an iterable. 14 | 15 | Return an iterable containing those items of iterable for which func(item) 16 | or func(key, item), depending on the iterable type, are true. 17 | 18 | Args: 19 | iterable: An iterable to filter 20 | Example: {'a': 1, 'b': None} or [1, None, 3, 4] 21 | func: A function to filter items by 22 | Note: If the iterable *is* a dictionary, the function signature 23 | is func(key: Any, item: Any) -> bool 24 | Note: If the iterable is *not* a dictionary, the function signature 25 | is func(item: Any) -> bool 26 | Example: lambda item: item is not None 27 | Returns: 28 | If isinstance(iterable, dict): 29 | Returns dict 30 | Else: 31 | Returns list 32 | Example: 33 | If isinstance(iterable, dict): 34 | >>> data = {'a': 1, 'b': None, 'c': 3} 35 | >>> filter(data, func = lambda key, val: val is not None) 36 | {'a': 1, 'c': 3} 37 | >>> 38 | Else: 39 | >>> data = [1, None, 3] 40 | >>> filter(data, func = lambda val: val is not None) 41 | [1, 3] 42 | >>> 43 | ''' 44 | 45 | if isinstance(iterable, dict): 46 | if not func: 47 | func = lambda key, val: bool(val) 48 | 49 | return \ 50 | { 51 | key: val 52 | for key, val in iterable.items() 53 | if func(key, val) 54 | } 55 | else: 56 | if func is None: 57 | func = bool 58 | 59 | return \ 60 | [ 61 | val 62 | for val in iterable 63 | if func(val) 64 | ] 65 | -------------------------------------------------------------------------------- /ytm/utils/first.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the utility function: first 3 | ''' 4 | 5 | from typing import Any, Iterable 6 | 7 | def first(iterable: Iterable, default: Any = None) -> Any: 8 | ''' 9 | Retrieve the first item from an iterable. 10 | 11 | Return the first item from an iterable, or the first value from 12 | from a dictionary 13 | 14 | Args: 15 | iterable: An iterable to retrieve the first item from 16 | Example: [1, 2, 3] or {'a': 1, 'b': 2} 17 | default: Default value for if the iterable is empty 18 | Example: -1 19 | 20 | Returns: 21 | The first item or value from the iterable 22 | 23 | Example: 24 | If isinstance(iterable, dict): 25 | >>> data = {'a': 1, 'b': 2} 26 | >>> first(data) 27 | 1 28 | >>> 29 | Elif iterable: 30 | >>> data = [1, 2, 3] 31 | >>> first(data) 32 | 1 33 | >>> 34 | Else: 35 | >>> data = [] 36 | >>> first(data, default = 'Nothing there') 37 | 'Nothing there' 38 | >>> 39 | ''' 40 | 41 | if not iterable: 42 | iterable = (default,) 43 | 44 | if isinstance(iterable, dict): 45 | iterable = iterable.values() 46 | 47 | return next(item for item in iterable) 48 | -------------------------------------------------------------------------------- /ytm/utils/get.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the utility function: get 3 | ''' 4 | 5 | from typing import Any, Callable, Iterable 6 | 7 | def get \ 8 | ( 9 | iterable: Iterable, 10 | *keys: Any, 11 | default: Any = None, 12 | func: Callable = None, 13 | ) -> Any: 14 | ''' 15 | Get a value from a multi-dimensional iterable. 16 | 17 | Get a value by specifying all of the keys needed to extract it 18 | 19 | Args: 20 | iterable: An iterable to retrieve a value from 21 | Example: ['a', ['b']] 22 | keys: The keys or indexes needed to access the desired item 23 | Example: [1, 0] or ['a', 'b', 0] 24 | default: The value to return if the item doesn't exist 25 | at the specified location 26 | Example: 'Default Value' 27 | func: Function to clean the retrieved item if it existed 28 | Example: int 29 | 30 | Returns: 31 | The retrieved item, parsed using func, or the default value 32 | 33 | Example: 34 | Item exists, no parsing: 35 | >>> data = ['Nope', ['Nope', {'key': 'Yes!'}]] 36 | >>> ytm.utils.get(data, 1, 1, 'key') 37 | 'Yes!' 38 | >>> 39 | Item exists, parsing: 40 | >>> data = ['Nope', ['Nope', {'key': 'Yes!'}]] 41 | >>> ytm.utils.get(data, 1, 1, 'key', func = lambda item: item + ' :)') 42 | 'Yes! :)' 43 | >>> 44 | Item doesn't exist: 45 | >>> data = ['Nope', ['Nope', {'key': 'Yes!'}]] 46 | >>> ytm.utils.get(data, 1, 1, 'invalid_key', default = 'Not there :(') 47 | 'Not there :(' 48 | >>> 49 | ''' 50 | 51 | if not isinstance(iterable, Iterable) \ 52 | or not iterable \ 53 | or not keys: 54 | return default 55 | 56 | item = iterable 57 | 58 | for key in keys: 59 | if not isinstance(item, dict): 60 | if isinstance(key, int) and key < 0: 61 | key += len(item) 62 | 63 | item = dict(enumerate(item)) 64 | 65 | if key not in item: 66 | return default 67 | 68 | item = item[key] 69 | 70 | if func: 71 | try: 72 | item = func(item) 73 | except: 74 | item = default 75 | 76 | return item 77 | -------------------------------------------------------------------------------- /ytm/utils/include.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the utility function: include 3 | ''' 4 | 5 | import pkgutil 6 | import importlib 7 | import sys 8 | from typing import Callable 9 | from _frozen_importlib import ModuleSpec 10 | 11 | def include(spec: ModuleSpec, func: Callable = None) -> dict: 12 | ''' 13 | Include relative libraries, or an object they contain. 14 | 15 | If a relative library exists with an object of the same name inside, 16 | it returns that object, otherwise, it returns the library. Libraries 17 | or objects are only returned if func(library or object) is true. 18 | 19 | Args: 20 | spec: The specification of the module to import from 21 | Example: utils.__spec__ 22 | func: A function to filter items by 23 | Example: lambda object: object.__name__ in ('foo',) 24 | 25 | Returns: 26 | A dictionary of imported libraries or objects 27 | 28 | Example: 29 | Package named utils containing foo.py, with a function inside named foo: 30 | >>> include(utils.__spec__) 31 | {'foo': } 32 | >>> 33 | Package named utils containing foo.py with a function inside named bar: 34 | >>> include(utils.__spec__) 35 | 36 | >>> 37 | ''' 38 | 39 | if not func: 40 | func = lambda module_name: True 41 | 42 | importlib.util.module_from_spec(spec) 43 | 44 | module = sys.modules[spec.name] 45 | 46 | sub_modules = pkgutil.iter_modules(spec.submodule_search_locations) 47 | 48 | imported = {} 49 | 50 | for sub_module in sub_modules: 51 | sub_module_name = sub_module.name 52 | 53 | sub_module = importlib.import_module \ 54 | ( 55 | name = f'.{sub_module_name}', 56 | package = spec.name, 57 | ) 58 | 59 | 60 | object = getattr(sub_module, sub_module_name, None) or sub_module 61 | 62 | if not func(object): continue 63 | 64 | setattr(module, sub_module_name, object) 65 | 66 | imported[sub_module_name] = object 67 | 68 | return imported 69 | -------------------------------------------------------------------------------- /ytm/utils/isinstance.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the utility function: isinstance 3 | ''' 4 | 5 | import builtins 6 | 7 | def isinstance(object: object, class_: type) -> bool: 8 | ''' 9 | Return whether an object is an instance of a class or of a subclass thereof. 10 | 11 | Modified to work with custom types, specifically with a _validate method 12 | 13 | Args: 14 | object: Object to check whether it's an instance of class_ 15 | Example: 'foo' or types.SongId('5HzWqJ8HZ5g') 16 | class_: Class to check whether object is an instance of it 17 | Example: str or types.SongId 18 | 19 | Returns: 20 | Whether object is an instance of class_ 21 | 22 | Example: 23 | Normal type: 24 | >>> isinstance('foo', str) 25 | True 26 | >>> 27 | Custom type: 28 | >>> isinstance('invalid_song_id', types.SongId) 29 | False 30 | >>> 31 | ''' 32 | 33 | return (builtins.isinstance(class_, type) and builtins.isinstance(object, class_)) \ 34 | or getattr(class_, '_isinstance', lambda object: False)(object) 35 | -------------------------------------------------------------------------------- /ytm/utils/lstrip.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the utility function: lstrip 3 | ''' 4 | 5 | def lstrip(string: str, sub_string: str, count: int = None) -> str: 6 | """ 7 | Left strip 's from . 8 | 9 | Strips a phrase instead of a set of characters. 10 | If is <= 0 or not an integer, it will be set to 1. 11 | 12 | Args: 13 | string: Source string to strip from 14 | sub_string: Sub string to strip from string 15 | count: Maximum number of sub strings to strip 16 | Default: None 17 | 18 | Returns: 19 | left-stripped of times 20 | 21 | Examples: 22 | >>> lstrip('someData', 'some') 23 | 'Data' 24 | >>> lstrip('somesomeData', 'some', 2) 25 | 'Data' 26 | """ 27 | 28 | if not isinstance(count, int) or count <= 0: 29 | count = 1 30 | 31 | for _ in range(count): 32 | if string.startswith(sub_string): 33 | string = string[len(sub_string):] 34 | 35 | return string 36 | -------------------------------------------------------------------------------- /ytm/utils/rstrip.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the utility function: rstrip 3 | ''' 4 | 5 | def rstrip(string: str, sub_string: str, count: int = None) -> str: 6 | """ 7 | Right strip 's from . 8 | 9 | Strips a phrase instead of a set of characters 10 | If is <= 0 or not an integer, it will be set to 1. 11 | 12 | Args: 13 | string: Source string to strip from 14 | sub_string: Sub string to strip from string 15 | count: Maximum number of sub strings to strip 16 | Default: None 17 | 18 | Returns: 19 | right-stripped of times 20 | 21 | Example: 22 | >>> rstrip('someData', 'Data') 23 | 'some' 24 | >>> rstrip('someDataData', 'Data', 2) 25 | 'some' 26 | """ 27 | 28 | if not isinstance(count, int) or count <= 0: 29 | count = 1 30 | 31 | for _ in range(count): 32 | if string.endswith(sub_string): 33 | string = string[:-len(sub_string)] 34 | 35 | return string 36 | -------------------------------------------------------------------------------- /ytm/utils/url_yt.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the utility function: url_yt 3 | ''' 4 | 5 | from .. import constants 6 | from ._url import _url 7 | 8 | def url_yt(*endpoints: str, params: dict = None) -> str: 9 | ''' 10 | Create a YouTube URL. 11 | 12 | Formulates a YouTube URL using the provided arguments 13 | 14 | Args: 15 | *endpoints: URL endpoints 16 | Example: ['feed', 'trending'] 17 | params: URL query string parameters 18 | Example: {'search_query': 'test'} 19 | 20 | Returns: 21 | A YouTube URL 22 | 23 | Example: 24 | >>> url_yt('results', params = {'search_query': 'test'}) 25 | 'https://www.youtube.com/results?search_query=test' 26 | >>> 27 | ''' 28 | 29 | return _url \ 30 | ( 31 | constants.PROTOCOL_YOUTUBE, 32 | constants.DOMAIN_YOUTUBE, 33 | *endpoints, 34 | params = params, 35 | ) 36 | -------------------------------------------------------------------------------- /ytm/utils/url_ytm.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Module containing the utility function: url_ytm 3 | ''' 4 | 5 | from .. import constants 6 | from ._url import _url 7 | 8 | def url_ytm(*endpoints: str, params: dict = None) -> str: 9 | ''' 10 | Create a YouTubeMusic URL. 11 | 12 | Formulates a YouTube Music URL using the provided arguments 13 | 14 | Args: 15 | *endpoints: URL endpoints 16 | Example: ['playlist'] 17 | params: URL query string parameters 18 | Example: {'q': 'test'} 19 | 20 | Returns: 21 | A YouTube Music URL 22 | 23 | Example: 24 | >>> url_ytm('search', params = {'q': 'test'}) 25 | 'https://music.youtube.com/search?q=test' 26 | >>> 27 | ''' 28 | 29 | return _url \ 30 | ( 31 | constants.PROTOCOL_YOUTUBE_MUSIC, 32 | constants.DOMAIN_YOUTUBE_MUSIC, 33 | *endpoints, 34 | params = params, 35 | ) 36 | --------------------------------------------------------------------------------