Source code for platypush.plugins.youtube

from typing import Any, Collection, Dict, List, Optional, Type

from platypush.plugins import Plugin, action

from .backends import BaseBackend, GoogleBackend, PipedBackend, InvidiousBackend


[docs] class YoutubePlugin(Plugin): r""" YouTube plugin. This plugin supports multiple backends to interact with YouTube: - ``piped``: Uses a `Piped <https://docs.piped.video/>`_ instance. - ``invidious``: Uses an `Invidious <https://github.com/iv-org/invidious>`_ instance. - ``google``: Uses the official Google YouTube API. You can specify the backend configuration in the plugin configuration through the ``backends`` parameter: .. code-block:: yaml youtube: backends: # For Piped piped: instance_url: https://pipedapi.kavin.rocks auth_token: <auth_token> frontend_url: https://piped.kavin.rocks # For Invidious invidious: instance_url: https://yewtu.be auth_token: <auth_token> # For the official YouTube API google: # OAuth authentication will be performed through the Google plugin # the first time you run the plugin. Piped ----- .. warning:: At the time of writing (February 2025), the Piped backend isn't actively tested. That's because most of the instances seem to be either down or `blocked by Google <https://github.com/TeamPiped/Piped/issues/3658>`_. ``invidious`` is the recommended backend. Parameters: - ``instance_url``: Base URL of the Piped instance (default: ``https://pipedapi.kavin.rocks``). **NOTE**: This should be the URL of the Piped API, not the Piped instance/frontend itself. - ``auth_token``: Optional authentication token from the Piped instance. This is required if you want to access your private feed and playlists, but not for searching public videos. - ``frontend_url``: Optional URL of the Piped frontend. This is needed to generate channels and playlists URLs. If not provided, the plugin will try and infer it by stripping the ``api`` string from ``instance_url``. In order to retrieve an authentication token: 1. Login to your configured Piped instance. 2. Copy the RSS/Atom feed URL on the _Feed_ tab. 3. Copy the ``auth_token`` query parameter from the URL. 4. Enter it in the ``auth_token`` field in the ``youtube`` section of the configuration file. Invidious --------- Parameters: - ``instance_url``: Base URL of the Invidious instance (default: ``https://yewtu.be``). - ``auth_token``: Optional authentication token from the Invidious instance. This is required if you want to access your private feed and playlists, but not for searching public videos. In order to retrieve an authentication token: 1. Login to your configured Invidious instance. 2. Open the URL ``https://<instance_url>/authorize_token?scopes=:*`` in your browser. Replace ``<instance_url>`` with the URL of your Invidious instance, and ``:*`` with the scopes you want to assign to the token (although an all-access token is recommended for full functionality). 3. Copy the generated token. If both are provided, the Invidious backend will be used. If none is provided, the plugin will fallback to the default Invidious instance (``https://yewtu.be``), but authenticated actions will not be available. Google ------ .. note:: The Google backend requires you to register a project on the Google Cloud Platform and enable the YouTube Data API v3. This plugin will use the OAuth2 authentication provided by the Google plugin, and quota limits apply. .. note:: The Google backend doesn't support :meth:`get_feed`. The `YouTube Data API v3 <https://developers.google.com/youtube/v3/docs/>`_ does not support an endpoint to retrieve the feed, and the alternative (searching for all the recent videos of all the subscribed channels) would be too slow, besides probably burning the whole YouTube API quota. Follow the `same instructions as other Google plugins <https://docs.platypush.tech/platypush/plugins/google.calendar.html>`_. Create a project, enable the YouTube Data API v3, and download the credentials file to ``<workdir>/credentials/google/client_secret.json``. Authentication will be performed the first time you run the plugin. It can be run: - *Automatically*: when the plugin is started, it will open an authentication page if the ``BROWSER`` environment variable is set. Otherwise, it will log the URL that should be opened in a browser to authenticate the plugin. You can also copy the authenticated session to other machines by copying the ``<workdir>/credentials/google`` folder. - *Manually*: by running the ``platypush.plugins.google.credentials`` command (see documentation of other Google plugins). Note that if you opt to generate the credentials manually, you will need a token with the following scopes: - ``https://www.googleapis.com/auth/youtube`` - ``https://www.googleapis.com/auth/youtube.force-ssl`` """ _timeout = 20
[docs] def __init__( self, backends: Optional[Dict[str, dict]] = None, **kwargs, ): """ :param backends: Configuration for the backends. """ super().__init__(**kwargs) backends = backends or {} piped = backends.get('piped') if kwargs.get('piped_api_url'): piped = backends['piped'] = backends.get('piped') or {} self.logger.warning( 'The "piped_api_url" parameter is deprecated. Use "piped.instance_url" instead.' ) if not piped.get('instance_url'): piped['instance_url'] = kwargs['piped_api_url'] if kwargs.get('auth_token'): piped = backends['piped'] = backends.get('piped') or {} self.logger.warning( 'The "auth_token" parameter is deprecated. Use "piped.auth_token" instead.' ) if not piped.get('auth_token'): piped['auth_token'] = kwargs['auth_token'] self._backends: Dict[Type[BaseBackend], BaseBackend] = {} if 'piped' in backends: self._backends[PipedBackend] = PipedBackend(**(backends.get('piped') or {})) if 'invidious' in backends: self._backends[InvidiousBackend] = InvidiousBackend( **(backends.get('invidious') or {}) ) if 'google' in backends: self._backends[GoogleBackend] = GoogleBackend( **(backends.get('google') or {}) ) if len(self._backends) > 1: self.logger.warning( 'Multiple backends provided. Defaulting to "invidious" as the primary backend.' ) elif not self._backends: # Fallback to the default Invidious instance self.logger.warning( 'No backends provided. Defaulting to the Invidious instance (https://yewtu.be). ' 'No authenticated actions will be available.' ) self._backends[InvidiousBackend] = InvidiousBackend()
def _default_backend(self) -> BaseBackend: # Prefer Invidious over Piped/Google if self._backends.get(InvidiousBackend): return self._backends[InvidiousBackend] return next(iter(self._backends.values())) def _get_backend(self, backend: Optional[str]) -> BaseBackend: if not backend: return self._default_backend() backend = backend.lower() for backend_instance in self._backends.values(): if backend_instance.name == backend: return backend_instance raise ValueError(f'Unknown backend: {backend}')
[docs] @action def search(self, query: str, backend: Optional[str] = None, **_) -> List[dict]: """ Search for YouTube videos. :param query: Query string. :param backend: Optional backend to use. If not specified, the default one will be used. :return: .. schema:: piped.PipedVideoSchema(many=True) """ api = self._get_backend(backend) self.logger.info( 'Searching YouTube through the %s backend for "%s"', api.name, query ) results = [item.to_dict() for item in api.search(query)] self.logger.info( '%d YouTube results for the search query "%s"', len(results), query, ) return results
[docs] @action def get_feed( self, page: Optional[Any] = None, backend: Optional[str] = None ) -> List[dict]: """ Retrieve the YouTube feed. If you use the ``piped`` backend, depending on your account settings on the configured Piped instance, this may return either the latest videos uploaded by your subscribed channels (if you provided an authentication token), or the trending videos in the configured area (if you didn't). If you use the ``invidious`` backend, this requires the user to be authenticated - it will return the latest videos uploaded by the subscribed channels. If you use the ``google`` backend, this method is not supported - the native YouTube API doesn't provide an endpoint to retrieve the feed, and the alternative (searching for all the recent videos of all the subscribed channels) would be too slow, besides probably burning the whole YouTube API quota. :param page: (Optional) ID/index of the page to retrieve. This isn't supported by the Piped backend (all the videos are returned at once), and it's instead an integer >= 1 on the Invidious backend. :param backend: Optional backend to use. If not specified, the default one will be used. :return: .. schema:: piped.PipedVideoSchema(many=True) """ return [ item.to_dict() for item in self._get_backend(backend).get_feed(page=page) ]
[docs] @action def get_playlists( self, backend: Optional[str] = None, page: Optional[Any] = None ) -> List[dict]: """ Retrieve the playlists saved by the user logged in to the Piped instance. :param backend: Optional backend to use. If not specified, the default one will be used. :param page: (Optional) ID/index of the page to retrieve. This is only supported by the YouTube backend. Both the Piped and Invidious backends will return all the playlists at once. :return: .. schema:: piped.PipedPlaylistSchema(many=True) """ return [ item.to_dict() for item in self._get_backend(backend).get_playlists(page=page) ]
[docs] @action def get_playlist( self, id: str, backend: Optional[str] = None, page: Optional[Any] = None ) -> List[dict]: # pylint: disable=redefined-builtin """ Retrieve the videos in a playlist. :param id: Playlist ID as returned by the backend. :param backend: Optional backend to use. If not specified, the default one will be used. :param page: (Optional) ID/index of the page to retrieve. This is only supported by the YouTube backend. Both the Piped and Invidious backends will return all the videos at once. :return: .. schema:: piped.PipedVideoSchema(many=True) """ return [ item.to_dict() for item in self._get_backend(backend).get_playlist(id, page=page) ]
[docs] @action def get_subscriptions( self, backend: Optional[str] = None, page: Optional[Any] = None ) -> List[dict]: """ Retrieve the channels subscribed by the user logged in to the Piped instance. :param backend: Optional backend to use. If not specified, the default one will be used. :param page: (Optional) ID/index of the page to retrieve. This is only supported by the YouTube backend. Both the Piped and Invidious backends will return all the subscriptions at once. :return: .. schema:: piped.PipedChannelSchema(many=True) """ return [ item.to_dict() for item in self._get_backend(backend).get_subscriptions(page=page) ]
[docs] @action def get_channel( self, id: str, # pylint: disable=redefined-builtin page: Optional[str] = None, backend: Optional[str] = None, ) -> dict: """ Retrieve the information and videos of a channel given its ID or URL. :param id: Channel ID or URL. :param page: (Optional) ID/index of the page to retrieve. :param backend: Optional backend to use. If not specified, the default one will be used. :return: .. schema:: piped.PipedChannelSchema """ return self._get_backend(backend).get_channel(id, page=page).to_dict()
[docs] @action def add_to_playlist( self, playlist_id: str, item_ids: Optional[Collection[str]] = None, backend: Optional[str] = None, **kwargs, ): """ Add a video to a playlist. :param playlist_id: Playlist ID. :param item_ids: YouTube IDs or URLs to add to the playlist. :param backend: Optional backend to use. If not specified, the default one will be used. """ self._get_backend(backend).add_to_playlist(playlist_id, item_ids, **kwargs)
[docs] @action def remove_from_playlist( self, playlist_id: str, item_ids: Optional[Collection[str]] = None, indices: Optional[Collection[int]] = None, backend: Optional[str] = None, **kwargs, ): """ Remove a video from a playlist. Note that either ``item_ids`` or ``indices`` must be provided. ``piped`` and ``invidious`` backends support both ``item_ids`` and ``indices``. ``google`` backend only supports ``item_ids``, and they must match ``item_id`` fields returned by :meth:`get_playlist`. :param item_ids: YouTube video IDs or URLs to remove from the playlist. :param indices: (0-based) indices of the items in the playlist to remove. :param playlist_id: Playlist ID. :param backend: Optional backend to use. If not specified, the default one will be used. """ self._get_backend(backend).remove_from_playlist( playlist_id=playlist_id, item_ids=item_ids, indices=indices, **kwargs )
[docs] @action def create_playlist( self, name: str, privacy: Optional[str] = 'private', backend: Optional[str] = None, ) -> dict: """ Create a new playlist. :param name: Playlist name. :param backend: Optional backend to use. If not specified, the default one will be used. :param privacy: Privacy level of the playlist (only supported by Invidious). Supported values are: - ``private``: Only you can see the playlist. - ``public``: Everyone can see the playlist. - ``unlisted``: Everyone with the link can see the playlist. :return: Playlist information. """ return ( self._get_backend(backend).create_playlist(name, privacy=privacy).to_dict() )
[docs] @action def edit_playlist( self, id: str, name: Optional[str] = None, description: Optional[str] = None, privacy: Optional[str] = None, backend: Optional[str] = None, ): # pylint: disable=redefined-builtin """ Edit a playlist. :param id: Playlist ID. :param name: New playlist name. :param description: New playlist description. :param privacy: New privacy level of the playlist (only supported by Invidious). Supported values are: - ``private``: Only you can see the playlist. - ``public``: Everyone can see the playlist. - ``unlisted``: Everyone with the link can see the playlist. :param backend: Optional backend to use. If not specified, the default one will be used. """ self._get_backend(backend).edit_playlist( id, name=name, description=description, privacy=privacy )
[docs] @action def delete_playlist( self, id: str, backend: Optional[str] = None ): # pylint: disable=redefined-builtin """ Delete a playlist. :param id: Playlist ID. :param backend: Optional backend to use. If not specified, the default one will be used. """ self._get_backend(backend).delete_playlist(id)
[docs] @action def is_subscribed(self, channel_id: str, backend: Optional[str] = None) -> bool: """ Check if the user is subscribed to a channel. :param channel_id: YouTube channel ID. :param backend: Optional backend to use. If not specified, the default one will be used. :return: True if the user is subscribed to the channel, False otherwise. """ return self._get_backend(backend).is_subscribed(channel_id)
[docs] @action def subscribe(self, channel_id: str, backend: Optional[str] = None): """ Subscribe to a channel. :param channel_id: YouTube channel ID. :param backend: Optional backend to use. If not specified, the default one will be used. """ self._get_backend(backend).subscribe(channel_id)
[docs] @action def unsubscribe(self, channel_id: str, backend: Optional[str] = None): """ Unsubscribe from a channel. :param channel_id: YouTube channel ID. :param backend: Optional backend to use. If not specified, the default one will be used. """ self._get_backend(backend).unsubscribe(channel_id)
# vim:sw=4:ts=4:et: