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 enum
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
|
@ -30,6 +31,25 @@ class PlayerState(enum.Enum):
|
||||||
IDLE = 'idle'
|
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):
|
class MediaPlugin(Plugin, ABC):
|
||||||
"""
|
"""
|
||||||
Generic plugin to interact with a media player.
|
Generic plugin to interact with a media player.
|
||||||
|
@ -241,6 +261,7 @@ class MediaPlugin(Plugin, ABC):
|
||||||
self._youtube_proc = None
|
self._youtube_proc = None
|
||||||
self.torrent_plugin = torrent_plugin
|
self.torrent_plugin = torrent_plugin
|
||||||
self.youtube_format = youtube_format
|
self.youtube_format = youtube_format
|
||||||
|
self._latest_resource: Optional[MediaResource] = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _torrent_event_handler(evt_queue):
|
def _torrent_event_handler(evt_queue):
|
||||||
|
@ -272,7 +293,29 @@ class MediaPlugin(Plugin, ABC):
|
||||||
for extractor in self.get_extractors()
|
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:
|
:param resource: Resource to play/parse. Supported types:
|
||||||
|
|
||||||
|
@ -283,8 +326,33 @@ class MediaPlugin(Plugin, ABC):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self._is_youtube_resource(resource):
|
if resource.startswith('file://'):
|
||||||
resource = self._get_youtube_info(resource).get('url')
|
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:?'):
|
elif resource.startswith('magnet:?'):
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
'Downloading torrent %s to %s', resource, self.download_dir
|
'Downloading torrent %s to %s', resource, self.download_dir
|
||||||
|
@ -308,8 +376,6 @@ class MediaPlugin(Plugin, ABC):
|
||||||
resource = self._videos_queue.pop(0)
|
resource = self._videos_queue.pop(0)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(f'No media file found in torrent {resource}')
|
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'
|
assert resource, 'Unable to find any compatible media resource'
|
||||||
return resource
|
return resource
|
||||||
|
@ -539,6 +605,12 @@ class MediaPlugin(Plugin, ABC):
|
||||||
def is_audio_file(cls, filename: str):
|
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
|
||||||
|
|
||||||
|
def _get_info(self, resource: str):
|
||||||
|
if self._is_youtube_resource(resource):
|
||||||
|
return self.get_youtube_info(resource)
|
||||||
|
|
||||||
|
return {'url': resource}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def start_streaming(
|
def start_streaming(
|
||||||
self, media: str, subtitles: Optional[str] = None, download: bool = False
|
self, media: str, subtitles: Optional[str] = None, download: bool = False
|
||||||
|
@ -656,10 +728,7 @@ class MediaPlugin(Plugin, ABC):
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def get_info(self, resource: str):
|
def get_info(self, resource: str):
|
||||||
if self._is_youtube_resource(resource):
|
return self._get_info(resource)
|
||||||
return self.get_youtube_info(resource)
|
|
||||||
|
|
||||||
return {'url': resource}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def get_media_file_duration(self, filename):
|
def get_media_file_duration(self, filename):
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from dataclasses import asdict
|
||||||
import os
|
import os
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
@ -186,7 +187,7 @@ class MediaGstreamerPlugin(MediaPlugin):
|
||||||
pos = self._player.get_position()
|
pos = self._player.get_position()
|
||||||
length = self._player.get_duration()
|
length = self._player.get_duration()
|
||||||
|
|
||||||
return {
|
status = {
|
||||||
'duration': length,
|
'duration': length,
|
||||||
'filename': self._resource[7:]
|
'filename': self._resource[7:]
|
||||||
if self._resource.startswith('file://')
|
if self._resource.startswith('file://')
|
||||||
|
@ -204,6 +205,17 @@ class MediaGstreamerPlugin(MediaPlugin):
|
||||||
'volume': self._player.get_volume() * 100,
|
'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
|
@staticmethod
|
||||||
def _gst_to_player_state(state) -> PlayerState:
|
def _gst_to_player_state(state) -> PlayerState:
|
||||||
# noinspection PyUnresolvedReferences,PyPackageRequirements
|
# noinspection PyUnresolvedReferences,PyPackageRequirements
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from dataclasses import asdict
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import select
|
import select
|
||||||
|
@ -500,6 +501,16 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
) + status['path']
|
) + status['path']
|
||||||
|
|
||||||
status['volume_max'] = 100
|
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
|
return status
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from dataclasses import asdict
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
@ -410,7 +411,7 @@ class MediaMpvPlugin(MediaPlugin):
|
||||||
if not self._player or not hasattr(self._player, 'pause'):
|
if not self._player or not hasattr(self._player, 'pause'):
|
||||||
return {'state': PlayerState.STOP.value}
|
return {'state': PlayerState.STOP.value}
|
||||||
|
|
||||||
return {
|
status = {
|
||||||
'audio_channels': getattr(self._player, 'audio_channels', None),
|
'audio_channels': getattr(self._player, 'audio_channels', None),
|
||||||
'audio_codec': getattr(self._player, 'audio_codec_name', None),
|
'audio_codec': getattr(self._player, 'audio_codec_name', None),
|
||||||
'delay': getattr(self._player, 'delay', None),
|
'delay': getattr(self._player, 'delay', None),
|
||||||
|
@ -442,6 +443,17 @@ class MediaMpvPlugin(MediaPlugin):
|
||||||
'width': getattr(self._player, 'width', None),
|
'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):
|
def on_stop(self, callback):
|
||||||
self._on_stop_callbacks.append(callback)
|
self._on_stop_callbacks.append(callback)
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from dataclasses import asdict
|
||||||
import enum
|
import enum
|
||||||
import threading
|
import threading
|
||||||
from typing import Collection, Optional
|
from typing import Collection, Optional
|
||||||
|
@ -343,7 +344,7 @@ class MediaOmxplayerPlugin(MediaPlugin):
|
||||||
elif state == 'paused':
|
elif state == 'paused':
|
||||||
state = PlayerState.PAUSE.value
|
state = PlayerState.PAUSE.value
|
||||||
|
|
||||||
return {
|
status = {
|
||||||
'duration': self._player.duration(),
|
'duration': self._player.duration(),
|
||||||
'filename': urllib.parse.unquote(self._player.get_source()).split('/')[-1]
|
'filename': urllib.parse.unquote(self._player.get_source()).split('/')[-1]
|
||||||
if self._player.get_source().startswith('file://')
|
if self._player.get_source().startswith('file://')
|
||||||
|
@ -363,6 +364,17 @@ class MediaOmxplayerPlugin(MediaPlugin):
|
||||||
'volume_max': 100,
|
'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):
|
def add_handler(self, event_type, callback):
|
||||||
if event_type not in self._handlers.keys():
|
if event_type not in self._handlers.keys():
|
||||||
raise AttributeError(f'{event_type} is not a valid PlayerEvent type')
|
raise AttributeError(f'{event_type} is not a valid PlayerEvent type')
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from dataclasses import asdict
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
@ -204,10 +205,6 @@ class MediaVlcPlugin(MediaPlugin):
|
||||||
|
|
||||||
self._post_event(MediaPlayRequestEvent, resource=resource)
|
self._post_event(MediaPlayRequestEvent, resource=resource)
|
||||||
resource = self._get_resource(resource)
|
resource = self._get_resource(resource)
|
||||||
|
|
||||||
if resource.startswith('file://'):
|
|
||||||
resource = resource[len('file://') :]
|
|
||||||
|
|
||||||
self._filename = resource
|
self._filename = resource
|
||||||
self._init_vlc(resource)
|
self._init_vlc(resource)
|
||||||
if subtitles and self._player:
|
if subtitles and self._player:
|
||||||
|
@ -411,6 +408,7 @@ class MediaVlcPlugin(MediaPlugin):
|
||||||
"filename": "filename or stream URL",
|
"filename": "filename or stream URL",
|
||||||
"state": "play" # or "stop" or "pause"
|
"state": "play" # or "stop" or "pause"
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import vlc
|
import vlc
|
||||||
|
|
||||||
|
@ -458,6 +456,18 @@ class MediaVlcPlugin(MediaPlugin):
|
||||||
status['volume'] = self._player.audio_get_volume()
|
status['volume'] = self._player.audio_get_volume()
|
||||||
status['volume_max'] = 100
|
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
|
return status
|
||||||
|
|
||||||
def on_stop(self, callback):
|
def on_stop(self, callback):
|
||||||
|
|
Loading…
Reference in a new issue