forked from platypush/platypush
[media
] Extended current track with ytdl metadata if available.
This commit is contained in:
parent
a939cb648c
commit
1d41df51e7
6 changed files with 142 additions and 16 deletions
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in a new issue