From 1d41df51e7096888bfb7e825b81cdd49bc2bfe98 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Mon, 6 Nov 2023 22:36:25 +0100 Subject: [PATCH] [`media`] Extended current track with ytdl metadata if available. --- platypush/plugins/media/__init__.py | 87 +++++++++++++++++-- platypush/plugins/media/gstreamer/__init__.py | 14 ++- platypush/plugins/media/mplayer/__init__.py | 11 +++ platypush/plugins/media/mpv/__init__.py | 14 ++- platypush/plugins/media/omxplayer/__init__.py | 14 ++- platypush/plugins/media/vlc/__init__.py | 18 +++- 6 files changed, 142 insertions(+), 16 deletions(-) diff --git a/platypush/plugins/media/__init__.py b/platypush/plugins/media/__init__.py index 167aa028..04ff279c 100644 --- a/platypush/plugins/media/__init__.py +++ b/platypush/plugins/media/__init__.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass import enum import functools import inspect @@ -30,6 +31,25 @@ class PlayerState(enum.Enum): IDLE = 'idle' +@dataclass +class MediaResource: + """ + Models a media resource + """ + + resource: str + url: str + title: Optional[str] = None + description: Optional[str] = None + filename: Optional[str] = None + image: Optional[str] = None + duration: Optional[float] = None + channel: Optional[str] = None + channel_url: Optional[str] = None + type: Optional[str] = None + resolution: Optional[str] = None + + class MediaPlugin(Plugin, ABC): """ Generic plugin to interact with a media player. @@ -241,6 +261,7 @@ class MediaPlugin(Plugin, ABC): self._youtube_proc = None self.torrent_plugin = torrent_plugin self.youtube_format = youtube_format + self._latest_resource: Optional[MediaResource] = None @staticmethod def _torrent_event_handler(evt_queue): @@ -272,7 +293,29 @@ class MediaPlugin(Plugin, ABC): for extractor in self.get_extractors() ) - def _get_resource(self, resource): + def _get_youtube_best_thumbnail(self, info: Dict[str, dict]): + thumbnails = info.get('thumbnails', {}) + if not thumbnails: + return None + + # Preferred resolution + for res in ((640, 480), (480, 360), (320, 240)): + thumb = next( + ( + thumb + for thumb in thumbnails + if thumb.get('width') == res[0] and thumb.get('height') == res[1] + ), + None, + ) + + if thumb: + return thumb.get('url') + + # Default fallback (best quality) + return info.get('thumbnail') + + def _get_resource(self, resource: str): """ :param resource: Resource to play/parse. Supported types: @@ -283,8 +326,33 @@ class MediaPlugin(Plugin, ABC): """ - if self._is_youtube_resource(resource): - resource = self._get_youtube_info(resource).get('url') + if resource.startswith('file://'): + resource = resource[len('file://') :] + assert os.path.isfile(resource), f'File {resource} not found' + self._latest_resource = MediaResource( + resource=resource, + url=f'file://{resource}', + title=os.path.basename(resource), + filename=os.path.basename(resource), + ) + elif self._is_youtube_resource(resource): + info = self._get_youtube_info(resource) + url = info.get('url') + if url: + resource = url + self._latest_resource = MediaResource( + resource=resource, + url=resource, + title=info.get('title'), + description=info.get('description'), + filename=info.get('filename'), + image=info.get('thumbnail'), + duration=float(info.get('duration') or 0) or None, + channel=info.get('channel'), + channel_url=info.get('channel_url'), + resolution=info.get('resolution'), + type=info.get('extractor'), + ) elif resource.startswith('magnet:?'): self.logger.info( 'Downloading torrent %s to %s', resource, self.download_dir @@ -308,8 +376,6 @@ class MediaPlugin(Plugin, ABC): resource = self._videos_queue.pop(0) else: raise RuntimeError(f'No media file found in torrent {resource}') - elif re.search(r'^https?://', resource): - return resource assert resource, 'Unable to find any compatible media resource' return resource @@ -539,6 +605,12 @@ class MediaPlugin(Plugin, ABC): def is_audio_file(cls, filename: str): return filename.lower().split('.')[-1] in cls.audio_extensions + def _get_info(self, resource: str): + if self._is_youtube_resource(resource): + return self.get_youtube_info(resource) + + return {'url': resource} + @action def start_streaming( self, media: str, subtitles: Optional[str] = None, download: bool = False @@ -656,10 +728,7 @@ class MediaPlugin(Plugin, ABC): @action def get_info(self, resource: str): - if self._is_youtube_resource(resource): - return self.get_youtube_info(resource) - - return {'url': resource} + return self._get_info(resource) @action def get_media_file_duration(self, filename): diff --git a/platypush/plugins/media/gstreamer/__init__.py b/platypush/plugins/media/gstreamer/__init__.py index eab25552..fbf60e32 100644 --- a/platypush/plugins/media/gstreamer/__init__.py +++ b/platypush/plugins/media/gstreamer/__init__.py @@ -1,3 +1,4 @@ +from dataclasses import asdict import os from typing import Optional @@ -186,7 +187,7 @@ class MediaGstreamerPlugin(MediaPlugin): pos = self._player.get_position() length = self._player.get_duration() - return { + status = { 'duration': length, 'filename': self._resource[7:] if self._resource.startswith('file://') @@ -204,6 +205,17 @@ class MediaGstreamerPlugin(MediaPlugin): 'volume': self._player.get_volume() * 100, } + if self._latest_resource: + status.update( + { + k: v + for k, v in asdict(self._latest_resource).items() + if v is not None + } + ) + + return status + @staticmethod def _gst_to_player_state(state) -> PlayerState: # noinspection PyUnresolvedReferences,PyPackageRequirements diff --git a/platypush/plugins/media/mplayer/__init__.py b/platypush/plugins/media/mplayer/__init__.py index 414ae0cd..2c181333 100644 --- a/platypush/plugins/media/mplayer/__init__.py +++ b/platypush/plugins/media/mplayer/__init__.py @@ -1,3 +1,4 @@ +from dataclasses import asdict import os import re import select @@ -500,6 +501,16 @@ class MediaMplayerPlugin(MediaPlugin): ) + status['path'] status['volume_max'] = 100 + + if self._latest_resource: + status.update( + { + k: v + for k, v in asdict(self._latest_resource).items() + if v is not None + } + ) + return status @action diff --git a/platypush/plugins/media/mpv/__init__.py b/platypush/plugins/media/mpv/__init__.py index 01eae6ea..a83504a9 100644 --- a/platypush/plugins/media/mpv/__init__.py +++ b/platypush/plugins/media/mpv/__init__.py @@ -1,3 +1,4 @@ +from dataclasses import asdict import os import threading @@ -410,7 +411,7 @@ class MediaMpvPlugin(MediaPlugin): if not self._player or not hasattr(self._player, 'pause'): return {'state': PlayerState.STOP.value} - return { + status = { 'audio_channels': getattr(self._player, 'audio_channels', None), 'audio_codec': getattr(self._player, 'audio_codec_name', None), 'delay': getattr(self._player, 'delay', None), @@ -442,6 +443,17 @@ class MediaMpvPlugin(MediaPlugin): 'width': getattr(self._player, 'width', None), } + if self._latest_resource: + status.update( + { + k: v + for k, v in asdict(self._latest_resource).items() + if v is not None + } + ) + + return status + def on_stop(self, callback): self._on_stop_callbacks.append(callback) diff --git a/platypush/plugins/media/omxplayer/__init__.py b/platypush/plugins/media/omxplayer/__init__.py index 37b9ea85..e704fc38 100644 --- a/platypush/plugins/media/omxplayer/__init__.py +++ b/platypush/plugins/media/omxplayer/__init__.py @@ -1,3 +1,4 @@ +from dataclasses import asdict import enum import threading from typing import Collection, Optional @@ -343,7 +344,7 @@ class MediaOmxplayerPlugin(MediaPlugin): elif state == 'paused': state = PlayerState.PAUSE.value - return { + status = { 'duration': self._player.duration(), 'filename': urllib.parse.unquote(self._player.get_source()).split('/')[-1] if self._player.get_source().startswith('file://') @@ -363,6 +364,17 @@ class MediaOmxplayerPlugin(MediaPlugin): 'volume_max': 100, } + if self._latest_resource: + status.update( + { + k: v + for k, v in asdict(self._latest_resource).items() + if v is not None + } + ) + + return status + def add_handler(self, event_type, callback): if event_type not in self._handlers.keys(): raise AttributeError(f'{event_type} is not a valid PlayerEvent type') diff --git a/platypush/plugins/media/vlc/__init__.py b/platypush/plugins/media/vlc/__init__.py index e024523e..0965c9e7 100644 --- a/platypush/plugins/media/vlc/__init__.py +++ b/platypush/plugins/media/vlc/__init__.py @@ -1,3 +1,4 @@ +from dataclasses import asdict import os import threading import urllib.parse @@ -204,10 +205,6 @@ class MediaVlcPlugin(MediaPlugin): self._post_event(MediaPlayRequestEvent, resource=resource) resource = self._get_resource(resource) - - if resource.startswith('file://'): - resource = resource[len('file://') :] - self._filename = resource self._init_vlc(resource) if subtitles and self._player: @@ -411,6 +408,7 @@ class MediaVlcPlugin(MediaPlugin): "filename": "filename or stream URL", "state": "play" # or "stop" or "pause" } + """ import vlc @@ -458,6 +456,18 @@ class MediaVlcPlugin(MediaPlugin): status['volume'] = self._player.audio_get_volume() status['volume_max'] = 100 + if ( + status['state'] in (PlayerState.PLAY.value, PlayerState.PAUSE.value) + and self._latest_resource + ): + status.update( + { + k: v + for k, v in asdict(self._latest_resource).items() + if v is not None + } + ) + return status def on_stop(self, callback):