forked from platypush/platypush
[youtube
] Added youtube.get_feed
action.
This commit is contained in:
parent
b63579b81c
commit
be28965d84
2 changed files with 158 additions and 39 deletions
|
@ -1,9 +1,9 @@
|
||||||
from datetime import datetime
|
from typing import Optional
|
||||||
import urllib.parse
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from platypush.plugins import Plugin, action
|
from platypush.plugins import Plugin, action
|
||||||
|
from platypush.schemas.piped import PipedVideoSchema
|
||||||
|
|
||||||
|
|
||||||
class YoutubePlugin(Plugin):
|
class YoutubePlugin(Plugin):
|
||||||
|
@ -22,13 +22,46 @@ class YoutubePlugin(Plugin):
|
||||||
It thus requires a link to a valid Piped instance.
|
It thus requires a link to a valid Piped instance.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, piped_api_url: str = 'https://pipedapi.kavin.rocks', **kwargs):
|
_timeout = 20
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
piped_api_url: str = 'https://pipedapi.kavin.rocks',
|
||||||
|
auth_token: Optional[str] = None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
:param piped_api_url: Base API URL of the Piped instance (default:
|
:param piped_api_url: Base API URL of the Piped instance (default:
|
||||||
``https://pipedapi.kavin.rocks``).
|
``https://pipedapi.kavin.rocks``).
|
||||||
|
:param 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self._piped_api_url = piped_api_url
|
self._piped_api_url = piped_api_url
|
||||||
|
self._auth_token = auth_token
|
||||||
|
|
||||||
|
def _api_url(self, path: str = '') -> str:
|
||||||
|
return f"{self._piped_api_url}/{path}"
|
||||||
|
|
||||||
|
def _request(self, path: str, auth: bool = True, **kwargs):
|
||||||
|
timeout = kwargs.pop('timeout', self._timeout)
|
||||||
|
if auth:
|
||||||
|
kwargs['params'] = kwargs.get('params', {})
|
||||||
|
kwargs['params']['authToken'] = self._auth_token
|
||||||
|
|
||||||
|
rs = requests.get(self._api_url(path), timeout=timeout, **kwargs)
|
||||||
|
rs.raise_for_status()
|
||||||
|
return rs.json()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def search(self, query: str, **_):
|
def search(self, query: str, **_):
|
||||||
|
@ -36,44 +69,11 @@ class YoutubePlugin(Plugin):
|
||||||
Search for YouTube videos.
|
Search for YouTube videos.
|
||||||
|
|
||||||
:param query: Query string.
|
:param query: Query string.
|
||||||
|
:return: .. schema:: piped.PipedVideoSchema(many=True)
|
||||||
:return: A list of results in the following format:
|
|
||||||
|
|
||||||
.. code-block:: json
|
|
||||||
|
|
||||||
{
|
|
||||||
"url": "https://www.youtube.com/watch?v=..."
|
|
||||||
"title": "Video title",
|
|
||||||
"description": "Video description",
|
|
||||||
"image": "https://i.ytimg.com/vi/.../hqdefault.jpg",
|
|
||||||
"duration": 300
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.logger.info('Searching YouTube for "%s"', query)
|
self.logger.info('Searching YouTube for "%s"', query)
|
||||||
query = urllib.parse.quote(query)
|
rs = self._request('search', auth=False, params={'q': query, 'filter': 'all'})
|
||||||
url = f"{self._piped_api_url}/search?q=" + query + "&filter=all"
|
results = PipedVideoSchema(many=True).dump(rs.get("items", [])) or []
|
||||||
rs = requests.get(url, timeout=20)
|
|
||||||
rs.raise_for_status()
|
|
||||||
results = [
|
|
||||||
{
|
|
||||||
"url": "https://www.youtube.com" + item["url"],
|
|
||||||
"title": item.get("title", '[No title]'),
|
|
||||||
"image": item.get("thumbnail"),
|
|
||||||
"duration": item.get("duration", 0),
|
|
||||||
"description": item.get("shortDescription"),
|
|
||||||
"channel": item.get("uploaderName"),
|
|
||||||
"channel_url": "https://www.youtube.com" + item["uploaderUrl"]
|
|
||||||
if item.get("uploaderUrl")
|
|
||||||
else None,
|
|
||||||
"channel_image": item.get("uploaderAvatar"),
|
|
||||||
"created_at": datetime.fromtimestamp(item["uploaded"] / 1000)
|
|
||||||
if item.get("uploaded")
|
|
||||||
else None,
|
|
||||||
}
|
|
||||||
for item in rs.json().get("items", [])
|
|
||||||
]
|
|
||||||
|
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
'%d YouTube video results for the search query "%s"',
|
'%d YouTube video results for the search query "%s"',
|
||||||
len(results),
|
len(results),
|
||||||
|
@ -82,5 +82,18 @@ class YoutubePlugin(Plugin):
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
@action
|
||||||
|
def get_feed(self):
|
||||||
|
"""
|
||||||
|
Retrieve the YouTube feed.
|
||||||
|
|
||||||
|
Depending on your account settings on the configured Piped instance,
|
||||||
|
this may return either the latest videos uploaded by your subscribed
|
||||||
|
channels, or the trending videos in the configured area.
|
||||||
|
|
||||||
|
:return: .. schema:: piped.PipedVideoSchema(many=True)
|
||||||
|
"""
|
||||||
|
return PipedVideoSchema(many=True).dump(self._request('feed')) or []
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
106
platypush/schemas/piped.py
Normal file
106
platypush/schemas/piped.py
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from marshmallow import EXCLUDE, fields, pre_dump
|
||||||
|
from marshmallow.schema import Schema
|
||||||
|
|
||||||
|
from platypush.schemas import StrippedString
|
||||||
|
|
||||||
|
|
||||||
|
class PipedVideoSchema(Schema):
|
||||||
|
"""
|
||||||
|
Class for video items returned by the Piped API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""
|
||||||
|
Exclude unknown fields.
|
||||||
|
"""
|
||||||
|
|
||||||
|
unknown = EXCLUDE
|
||||||
|
|
||||||
|
url = fields.Url(
|
||||||
|
required=True,
|
||||||
|
metadata={
|
||||||
|
'description': 'Video URL',
|
||||||
|
'example': 'https://youtube.com/watch?v=1234567890',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
title = StrippedString(
|
||||||
|
missing='[No Title]',
|
||||||
|
metadata={
|
||||||
|
'description': 'Video title',
|
||||||
|
'example': 'My Video Title',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
duration = fields.Int(
|
||||||
|
missing=0,
|
||||||
|
metadata={
|
||||||
|
'description': 'Video duration in seconds',
|
||||||
|
'example': 120,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
channel = fields.String(
|
||||||
|
attribute='uploaderName',
|
||||||
|
metadata={
|
||||||
|
'description': 'Channel name',
|
||||||
|
'example': 'My Channel',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
channel_url = fields.Url(
|
||||||
|
attribute='uploaderUrl',
|
||||||
|
metadata={
|
||||||
|
'description': 'Channel URL',
|
||||||
|
'example': 'https://youtube.com/channel/1234567890',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
channel_image = fields.Url(
|
||||||
|
attribute='uploaderAvatar',
|
||||||
|
metadata={
|
||||||
|
'description': 'Channel image URL',
|
||||||
|
'example': 'https://i.ytimg.com/vi/1234567890/hqdefault.jpg',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
created_at = fields.DateTime(
|
||||||
|
attribute='uploaded',
|
||||||
|
metadata={
|
||||||
|
'description': 'Video upload date',
|
||||||
|
'example': '2020-01-01T00:00:00',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@pre_dump
|
||||||
|
def fill_urls(self, data: dict, **_):
|
||||||
|
for attr in ('url', 'uploaderUrl'):
|
||||||
|
if data.get(attr) and not data[attr].startswith('https://'):
|
||||||
|
data[attr] = f'https://youtube.com{data[attr]}'
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
@pre_dump
|
||||||
|
def normalize_timestamps(self, data: dict, **_):
|
||||||
|
if data.get('uploaded') and isinstance(data['uploaded'], int):
|
||||||
|
data['uploaded'] = datetime.fromtimestamp(data["uploaded"] / 1000)
|
||||||
|
|
||||||
|
return data
|
Loading…
Reference in a new issue