[media] Black/LINT for MediaPlugin.

This commit is contained in:
Fabio Manganiello 2023-11-04 00:07:56 +01:00
parent efdb63443d
commit b4bf30945a
Signed by: blacklight
GPG key ID: D90FBA7F76362774

View file

@ -3,13 +3,13 @@ import functools
import os import os
import queue import queue
import re import re
import requests
from abc import ABC, abstractmethod
from typing import Optional, List, Dict, Union
import subprocess import subprocess
import tempfile import tempfile
import threading import threading
from abc import ABC, abstractmethod
from typing import Iterable, Optional, List, Dict, Union
import requests
from platypush.config import Config from platypush.config import Config
from platypush.context import get_plugin, get_backend from platypush.context import get_plugin, get_backend
@ -17,6 +17,10 @@ from platypush.plugins import Plugin, action
class PlayerState(enum.Enum): class PlayerState(enum.Enum):
"""
Models the possible states of a media player
"""
STOP = 'stop' STOP = 'stop'
PLAY = 'play' PLAY = 'play'
PAUSE = 'pause' PAUSE = 'pause'
@ -85,7 +89,6 @@ class MediaPlugin(Plugin, ABC):
'webm', 'webm',
'mkv', 'mkv',
'flv', 'flv',
'flv',
'vob', 'vob',
'ogv', 'ogv',
'ogg', 'ogg',
@ -112,17 +115,13 @@ class MediaPlugin(Plugin, ABC):
'mpeg', 'mpeg',
'mpe', 'mpe',
'mpv', 'mpv',
'mpg',
'mpeg',
'm2v', 'm2v',
'm4v',
'svi', 'svi',
'3gp', '3gp',
'3g2', '3g2',
'mxf', 'mxf',
'roq', 'roq',
'nsv', 'nsv',
'flv',
'f4v', 'f4v',
'f4p', 'f4p',
'f4a', 'f4a',
@ -185,10 +184,10 @@ class MediaPlugin(Plugin, ABC):
if self.__class__.__name__ == 'MediaPlugin': if self.__class__.__name__ == 'MediaPlugin':
# Abstract class, initialize with the default configured player # Abstract class, initialize with the default configured player
for plugin in Config.get_plugins().keys(): for plugin_name in Config.get_plugins().keys():
if plugin in self._supported_media_plugins: if plugin_name in self._supported_media_plugins:
player = plugin player = get_plugin(plugin_name)
if get_plugin(player).is_local(): if player and player.is_local():
# Local players have priority as default if configured # Local players have priority as default if configured
break break
else: else:
@ -201,9 +200,8 @@ class MediaPlugin(Plugin, ABC):
if self.__class__.__name__ == 'MediaPlugin': if self.__class__.__name__ == 'MediaPlugin':
# Populate this plugin with the actions of the configured player # Populate this plugin with the actions of the configured player
plugin = get_plugin(player) for act in player.registered_actions:
for act in plugin.registered_actions: setattr(self, act, getattr(player, act))
setattr(self, act, getattr(plugin, act))
self.registered_actions.add(act) self.registered_actions.add(act)
self._env = env or {} self._env = env or {}
@ -268,7 +266,7 @@ class MediaPlugin(Plugin, ABC):
if not m: if not m:
m = re.match('https://youtu.be/(.*)', resource) m = re.match('https://youtu.be/(.*)', resource)
if m: if m:
resource = 'https://www.youtube.com/watch?v={}'.format(m.group(1)) resource = f'https://www.youtube.com/watch?v={m.group(1)}'
if self.__class__.__name__ == 'MediaChromecastPlugin': if self.__class__.__name__ == 'MediaChromecastPlugin':
# The Chromecast has already its native way to handle YouTube # The Chromecast has already its native way to handle YouTube
@ -277,9 +275,10 @@ class MediaPlugin(Plugin, ABC):
resource = self.get_youtube_video_url(resource) resource = self.get_youtube_video_url(resource)
elif resource.startswith('magnet:?'): elif resource.startswith('magnet:?'):
self.logger.info( self.logger.info(
'Downloading torrent {} to {}'.format(resource, self.download_dir) 'Downloading torrent %s to %s', resource, self.download_dir
) )
torrents = get_plugin(self.torrent_plugin) torrents = get_plugin(self.torrent_plugin)
assert torrents, f'{self.torrent_plugin} plugin not configured'
evt_queue = queue.Queue() evt_queue = queue.Queue()
torrents.download( torrents.download(
@ -290,13 +289,13 @@ class MediaPlugin(Plugin, ABC):
event_hndl=self._torrent_event_handler(evt_queue), event_hndl=self._torrent_event_handler(evt_queue),
) )
resources = [f for f in evt_queue.get()] # noqa: C416 resources = [f for f in evt_queue.get()] # noqa: C416,R1721
if resources: if resources:
self._videos_queue = sorted(resources) self._videos_queue = sorted(resources)
resource = self._videos_queue.pop(0) resource = self._videos_queue.pop(0)
else: else:
raise RuntimeError('No media file found in torrent {}'.format(resource)) raise RuntimeError(f'No media file found in torrent {resource}')
elif re.search(r'^https?://', resource): elif re.search(r'^https?://', resource):
return resource return resource
@ -304,12 +303,12 @@ class MediaPlugin(Plugin, ABC):
return resource return resource
def _stop_torrent(self): def _stop_torrent(self):
# noinspection PyBroadException
try: try:
torrents = get_plugin(self.torrent_plugin) torrents = get_plugin(self.torrent_plugin)
assert torrents, f'{self.torrent_plugin} plugin not configured'
torrents.quit() torrents.quit()
except Exception as e: except Exception as e:
self.logger.warning(f'Could not stop torrent plugin: {str(e)}') self.logger.warning('Could not stop torrent plugin: %s', e)
@action @action
@abstractmethod @abstractmethod
@ -360,6 +359,8 @@ class MediaPlugin(Plugin, ABC):
video = self._videos_queue.pop(0) video = self._videos_queue.pop(0)
return self.play(video) return self.play(video)
return None
@action @action
@abstractmethod @abstractmethod
def toggle_subtitles(self, *args, **kwargs): def toggle_subtitles(self, *args, **kwargs):
@ -413,29 +414,20 @@ class MediaPlugin(Plugin, ABC):
@action @action
def search( def search(
self, self,
query, query: str,
types=None, types: Optional[Iterable[str]] = None,
queue_results=False, queue_results: bool = False,
autoplay=False, autoplay: bool = False,
search_timeout=_default_search_timeout, timeout: float = _default_search_timeout,
): ):
""" """
Perform a video search. Perform a video search.
:param query: Query string, video name or partial name :param query: Query string, video name or partial name
:type query: str
:param types: Video types to search (default: ``["youtube", "file", "torrent"]``) :param types: Video types to search (default: ``["youtube", "file", "torrent"]``)
:type types: list
:param queue_results: Append the results to the current playing queue (default: False) :param queue_results: Append the results to the current playing queue (default: False)
:type queue_results: bool
:param autoplay: Play the first result of the search (default: False) :param autoplay: Play the first result of the search (default: False)
:type autoplay: bool :param timeout: Search timeout (default: 60 seconds)
:param search_timeout: Search timeout (default: 60 seconds)
:type search_timeout: float
""" """
results = {} results = {}
@ -460,32 +452,27 @@ class MediaPlugin(Plugin, ABC):
for media_type in types: for media_type in types:
try: try:
items = results_queues[media_type].get(timeout=search_timeout) items = results_queues[media_type].get(timeout=timeout)
if isinstance(items, Exception): if isinstance(items, Exception):
raise items raise items
results[media_type].extend(items) results[media_type].extend(items)
except queue.Empty: except queue.Empty:
self.logger.warning( self.logger.warning(
'Search for "{}" media type {} timed out'.format(query, media_type) 'Search for "%s" media type %s timed out', query, media_type
) )
except Exception as e: except Exception as e:
self.logger.warning( self.logger.warning(
'Error while searching for "{}", media type {}'.format( 'Error while searching for "%s", media type %s', query, media_type
query, media_type
)
) )
self.logger.exception(e) self.logger.exception(e)
flattened_results = [] results = [
{'type': media_type, **result}
for media_type in self._supported_media_types: for media_type in self._supported_media_types
if media_type in results: for result in results.get(media_type, [])
for result in results[media_type]: if media_type in results
result['type'] = media_type ]
flattened_results += results[media_type]
results = flattened_results
if results: if results:
if queue_results: if queue_results:
@ -507,7 +494,7 @@ class MediaPlugin(Plugin, ABC):
return thread return thread
def _get_search_handler_by_type(self, search_type): def _get_search_handler_by_type(self, search_type: str):
if search_type == 'file': if search_type == 'file':
from .search import LocalMediaSearcher from .search import LocalMediaSearcher
@ -529,33 +516,30 @@ class MediaPlugin(Plugin, ABC):
return JellyfinMediaSearcher(media_plugin=self) return JellyfinMediaSearcher(media_plugin=self)
self.logger.warning('Unsupported search type: {}'.format(search_type)) self.logger.warning('Unsupported search type: %s', search_type)
return None
@classmethod @classmethod
def is_video_file(cls, filename): def is_video_file(cls, filename: str):
return filename.lower().split('.')[-1] in cls.video_extensions return filename.lower().split('.')[-1] in cls.video_extensions
@classmethod @classmethod
def is_audio_file(cls, filename): def is_audio_file(cls, filename: str):
return filename.lower().split('.')[-1] in cls.audio_extensions return filename.lower().split('.')[-1] in cls.audio_extensions
@action @action
def start_streaming(self, media, subtitles=None, download=False): def start_streaming(
self, media: str, subtitles: Optional[str] = None, download: bool = False
):
""" """
Starts streaming local media over the specified HTTP port. Starts streaming local media over the specified HTTP port.
The stream will be available to HTTP clients on The stream will be available to HTTP clients on
`http://{this-ip}:{http_backend_port}/media/<media_id>` `http://{this-ip}:{http_backend_port}/media/<media_id>`
:param media: Media to stream :param media: Media to stream
:type media: str
:param subtitles: Path or URL to the subtitles track to be used :param subtitles: Path or URL to the subtitles track to be used
:type subtitles: str
:param download: Set to True if you prefer to download the file from :param download: Set to True if you prefer to download the file from
the streaming link instead of streaming it the streaming link instead of streaming it
:type download: bool
:return: dict containing the streaming URL.Example: :return: dict containing the streaming URL.Example:
.. code-block:: json .. code-block:: json
@ -570,67 +554,30 @@ class MediaPlugin(Plugin, ABC):
""" """
http = get_backend('http') http = get_backend('http')
if not http: assert http, f'Unable to stream {media}: HTTP backend not configured'
self.logger.warning(
'Unable to stream {}: HTTP backend unavailable'.format(media)
)
return
self.logger.info('Starting streaming {}'.format(media)) self.logger.info('Starting streaming %s', media)
response = requests.put( response = requests.put(
'{url}/media{download}'.format( f'{http.local_base_url}/media' + ('?download' if download else ''),
url=http.local_base_url, download='?download' if download else ''
),
json={'source': media, 'subtitles': subtitles}, json={'source': media, 'subtitles': subtitles},
timeout=300,
) )
if not response.ok: assert response.ok, response.text or response.reason
self.logger.warning(
'Unable to start streaming: {}'.format(response.text or response.reason)
)
return None, (response.text or response.reason)
return response.json() return response.json()
@action @action
def stop_streaming(self, media_id): def stop_streaming(self, media_id: str):
http = get_backend('http') http = get_backend('http')
if not http: assert http, f'Unable to stop streaming {media_id}: HTTP backend not configured'
self.logger.warning(
'Cannot unregister {}: HTTP backend unavailable'.format(media_id)
)
return
response = requests.delete( response = requests.delete(
'{url}/media/{id}'.format(url=http.local_base_url, id=media_id) f'{http.local_base_url}/media/{media_id}', timeout=30
) )
if not response.ok: assert response.ok, response.text or response.reason
self.logger.warning(
'Unable to unregister media_id {}: {}'.format(media_id, response.reason)
)
return
return response.json() return response.json()
@staticmethod
def _youtube_search_api(query):
return [
{
'url': 'https://www.youtube.com/watch?v=' + item['id']['videoId'],
'title': item.get('snippet', {}).get('title', '<No Title>'),
}
for item in get_plugin('google.youtube').search(query=query).output
if item.get('id', {}).get('kind') == 'youtube#video'
]
@staticmethod
def _youtube_search_html_parse(query):
from .search import YoutubeMediaSearcher
# noinspection PyProtectedMember
return YoutubeMediaSearcher()._youtube_search_html_parse(query)
def get_youtube_video_url(self, url, youtube_format: Optional[str] = None): def get_youtube_video_url(self, url, youtube_format: Optional[str] = None):
ytdl_cmd = [ ytdl_cmd = [
'youtube-dl', 'youtube-dl',
@ -639,10 +586,12 @@ class MediaPlugin(Plugin, ABC):
'-g', '-g',
url, url,
] ]
self.logger.info(f'Executing command {" ".join(ytdl_cmd)}')
youtube_dl = subprocess.Popen(ytdl_cmd, stdout=subprocess.PIPE) self.logger.info('Executing command %s', ' '.join(ytdl_cmd))
url = youtube_dl.communicate()[0].decode().strip() with subprocess.Popen(ytdl_cmd, stdout=subprocess.PIPE) as youtube_dl:
youtube_dl.wait() url = youtube_dl.communicate()[0].decode().strip()
youtube_dl.wait()
return url return url
@staticmethod @staticmethod
@ -662,21 +611,30 @@ class MediaPlugin(Plugin, ABC):
if m: if m:
return m.group(1) return m.group(1)
return None
@action @action
def get_youtube_url(self, url, youtube_format: Optional[str] = None): def get_youtube_url(self, url, youtube_format: Optional[str] = None):
youtube_id = self.get_youtube_id(url) youtube_id = self.get_youtube_id(url)
if youtube_id: if youtube_id:
url = 'https://www.youtube.com/watch?v={}'.format(youtube_id) url = f'https://www.youtube.com/watch?v={youtube_id}'
return self.get_youtube_video_url(url, youtube_format=youtube_format) return self.get_youtube_video_url(url, youtube_format=youtube_format)
return None
@action @action
def get_youtube_info(self, url): def get_youtube_info(self, url):
m = re.match('youtube:video:(.*)', url) m = re.match('youtube:video:(.*)', url)
if m: if m:
url = 'https://www.youtube.com/watch?v={}'.format(m.group(1)) url = f'https://www.youtube.com/watch?v={m.group(1)}'
proc = subprocess.Popen(['youtube-dl', '-j', url], stdout=subprocess.PIPE) with subprocess.Popen(
return proc.stdout.read().decode("utf-8", "strict")[:-1] ['youtube-dl', '-j', url], stdout=subprocess.PIPE
) as proc:
if proc.stdout is None:
return None
return proc.stdout.read().decode("utf-8", "strict")[:-1]
@action @action
def get_media_file_duration(self, filename): def get_media_file_duration(self, filename):
@ -687,40 +645,44 @@ class MediaPlugin(Plugin, ABC):
if filename.startswith('file://'): if filename.startswith('file://'):
filename = filename[7:] filename = filename[7:]
result = subprocess.Popen( with subprocess.Popen(
["ffprobe", filename], stdout=subprocess.PIPE, stderr=subprocess.STDOUT ["ffprobe", filename], stdout=subprocess.PIPE, stderr=subprocess.STDOUT
) ) as result:
if not result.stdout:
return 0
return functools.reduce( return functools.reduce(
lambda t, t_i: t + t_i, lambda t, t_i: t + t_i,
[ [
float(t) * pow(60, i) float(t) * pow(60, i)
for (i, t) in enumerate( for (i, t) in enumerate(
re.search( re.search(
r'^Duration:\s*([^,]+)', r'^Duration:\s*([^,]+)',
[ [
x.decode() x.decode()
for x in result.stdout.readlines() for x in result.stdout.readlines()
if "Duration" in x.decode() if "Duration" in x.decode()
] ]
.pop() .pop()
.strip(), .strip(),
)
.group(1)
.split(':')[::-1]
) )
.group(1) ],
.split(':')[::-1] )
)
],
)
@action @action
def download(self, url, filename=None, directory=None): def download(
self, url: str, filename: Optional[str] = None, directory: Optional[str] = None
):
""" """
Download a media URL Download a media URL to a local file on the Platypush host.
:param url: Media URL :param url: Media URL.
:param filename: Media filename (default: URL filename) :param filename: Media filename (default: inferred from the URL basename).
:param directory: Destination directory (default: download_dir) :param directory: Destination directory (default: ``download_dir``).
:return: The absolute path to the downloaded file :return: The absolute path to the downloaded file.
""" """
if not filename: if not filename:
@ -729,10 +691,12 @@ class MediaPlugin(Plugin, ABC):
directory = self.download_dir directory = self.download_dir
path = os.path.join(directory, filename) path = os.path.join(directory, filename)
content = requests.get(url).content
with open(path, 'wb') as f: with requests.get(url, timeout=20, stream=True) as r:
f.write(content) r.raise_for_status()
with open(path, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
return path return path
@ -742,21 +706,21 @@ class MediaPlugin(Plugin, ABC):
@staticmethod @staticmethod
def get_subtitles_file(subtitles): def get_subtitles_file(subtitles):
if not subtitles: if not subtitles:
return return None
if subtitles.startswith('file://'): if subtitles.startswith('file://'):
subtitles = subtitles[len('file://') :] subtitles = subtitles[len('file://') :]
if os.path.isfile(subtitles): if os.path.isfile(subtitles):
return os.path.abspath(subtitles) return os.path.abspath(subtitles)
else:
content = requests.get(subtitles).content
f = tempfile.NamedTemporaryFile(
prefix='media_subs_', suffix='.srt', delete=False
)
with f: content = requests.get(subtitles, timeout=20).content
f.write(content) f = tempfile.NamedTemporaryFile(
return f.name prefix='media_subs_', suffix='.srt', delete=False
)
with f:
f.write(content)
return f.name
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et: