[media] Extended current track with ytdl metadata if available.

This commit is contained in:
Fabio Manganiello 2023-11-06 22:36:25 +01:00
parent a939cb648c
commit 1d41df51e7
Signed by: blacklight
GPG key ID: D90FBA7F76362774
6 changed files with 142 additions and 16 deletions

View file

@ -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):

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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')

View file

@ -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):