From 4853f51c8b9658a9c6bd683ccd459df3d94f7171 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Tue, 14 Nov 2023 21:47:36 +0100 Subject: [PATCH] [YouTube] Added (read-only) playlists support. --- .../components/panels/Media/MediaImage.vue | 19 ++-- .../panels/Media/Providers/YouTube.vue | 3 + .../Media/Providers/YouTube/Playlist.vue | 74 +++++++++++++++ .../Media/Providers/YouTube/Playlists.vue | 90 +++++++++++++++++++ platypush/plugins/youtube/__init__.py | 39 ++++++-- platypush/schemas/piped.py | 53 +++++++++++ 6 files changed, 268 insertions(+), 10 deletions(-) create mode 100644 platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Playlist.vue create mode 100644 platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Playlists.vue diff --git a/platypush/backend/http/webapp/src/components/panels/Media/MediaImage.vue b/platypush/backend/http/webapp/src/components/panels/Media/MediaImage.vue index d27e3aad..9bfdd80c 100644 --- a/platypush/backend/http/webapp/src/components/panels/Media/MediaImage.vue +++ b/platypush/backend/http/webapp/src/components/panels/Media/MediaImage.vue @@ -1,6 +1,7 @@ @@ -41,7 +45,12 @@ export default { item: { type: Object, default: () => {}, - } + }, + + hasPlay: { + type: Boolean, + default: true, + }, }, data() { @@ -94,7 +103,7 @@ export default { right: 0; } -.duration { +.bottom-overlay { position: absolute; bottom: 0; right: 0; diff --git a/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube.vue b/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube.vue index 806055f4..f94dccac 100644 --- a/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube.vue +++ b/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube.vue @@ -8,6 +8,7 @@
+
@@ -22,6 +23,7 @@ import MediaProvider from "./Mixin"; import Feed from "./YouTube/Feed"; import Index from "./YouTube/Index"; import NoToken from "./YouTube/NoToken"; +import Playlists from "./YouTube/Playlists"; export default { mixins: [MediaProvider], @@ -31,6 +33,7 @@ export default { Loading, MediaNav, NoToken, + Playlists, }, data() { diff --git a/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Playlist.vue b/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Playlist.vue new file mode 100644 index 00000000..64a1ee8c --- /dev/null +++ b/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Playlist.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Playlists.vue b/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Playlists.vue new file mode 100644 index 00000000..53632829 --- /dev/null +++ b/platypush/backend/http/webapp/src/components/panels/Media/Providers/YouTube/Playlists.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/platypush/plugins/youtube/__init__.py b/platypush/plugins/youtube/__init__.py index 41e6e48a..e9ad1704 100644 --- a/platypush/plugins/youtube/__init__.py +++ b/platypush/plugins/youtube/__init__.py @@ -1,9 +1,9 @@ -from typing import Optional +from typing import List, Optional import requests from platypush.plugins import Plugin, action -from platypush.schemas.piped import PipedVideoSchema +from platypush.schemas.piped import PipedPlaylistSchema, PipedVideoSchema class YoutubePlugin(Plugin): @@ -16,7 +16,7 @@ class YoutubePlugin(Plugin): prevent scraping, and it requires the user to tinker with the OAuth layer, app permissions and app validation in order to get it working. - Instead, it relies on a `Piped `_, an open-source alternative YouTube gateway. It thus requires a link to a valid Piped instance. @@ -58,13 +58,15 @@ class YoutubePlugin(Plugin): if auth: kwargs['params'] = kwargs.get('params', {}) kwargs['params']['authToken'] = self._auth_token + kwargs['headers'] = kwargs.get('headers', {}) + kwargs['headers']['Authorization'] = self._auth_token rs = requests.get(self._api_url(path), timeout=timeout, **kwargs) rs.raise_for_status() return rs.json() @action - def search(self, query: str, **_): + def search(self, query: str, **_) -> List[dict]: """ Search for YouTube videos. @@ -83,7 +85,7 @@ class YoutubePlugin(Plugin): return results @action - def get_feed(self): + def get_feed(self) -> List[dict]: """ Retrieve the YouTube feed. @@ -95,5 +97,32 @@ class YoutubePlugin(Plugin): """ return PipedVideoSchema(many=True).dump(self._request('feed')) or [] + @action + def get_playlists(self) -> List[dict]: + """ + Retrieve the playlists saved by the user logged in to the Piped + instance. + + :return: .. schema:: piped.PipedPlaylistSchema(many=True) + """ + return ( + PipedPlaylistSchema(many=True).dump(self._request('user/playlists')) or [] + ) + + @action + def get_playlist(self, id: str) -> List[dict]: # pylint: disable=redefined-builtin + """ + Retrieve the videos in a playlist. + + :param id: Piped playlist ID. + :return: .. schema:: piped.PipedVideoSchema(many=True) + """ + return ( + PipedVideoSchema(many=True).dump( + self._request(f'playlists/{id}').get('relatedStreams', []) + ) + or [] + ) + # vim:sw=4:ts=4:et: diff --git a/platypush/schemas/piped.py b/platypush/schemas/piped.py index 74e34095..96fcf449 100644 --- a/platypush/schemas/piped.py +++ b/platypush/schemas/piped.py @@ -104,3 +104,56 @@ class PipedVideoSchema(Schema): data['uploaded'] = datetime.fromtimestamp(data["uploaded"] / 1000) return data + + +class PipedPlaylistSchema(Schema): + """ + Class for playlist items returned by the Piped API. + """ + + class Meta: + """ + Exclude unknown fields. + """ + + unknown = EXCLUDE + + id = fields.UUID( + required=True, + metadata={ + 'description': 'Playlist ID', + 'example': '12345678-1234-1234-1234-1234567890ab', + }, + ) + + name = StrippedString( + missing='[No Name]', + metadata={ + 'description': 'Playlist name', + 'example': 'My Playlist Name', + }, + ) + + image = fields.Url( + attribute='thumbnail', + metadata={ + 'description': 'Image URL', + 'example': 'https://i.ytimg.com/vi/1234567890/hqdefault.jpg', + }, + ) + + description = StrippedString( + attribute='shortDescription', + metadata={ + 'description': 'Video description', + 'example': 'My video description', + }, + ) + + videos = fields.Int( + missing=0, + metadata={ + 'description': 'Number of videos in the playlist', + 'example': 10, + }, + )