From 213498318f7d2494502721b5d90569730def3457 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Mon, 19 Aug 2024 23:56:05 +0200 Subject: [PATCH] [media] Support extended format/metadata for media dirs. --- platypush/plugins/media/__init__.py | 112 +++++++++++++++++++++++----- platypush/plugins/media/_model.py | 49 ++++++++++++ 2 files changed, 141 insertions(+), 20 deletions(-) diff --git a/platypush/plugins/media/__init__.py b/platypush/plugins/media/__init__.py index aad13205fe..ecea79b4b0 100644 --- a/platypush/plugins/media/__init__.py +++ b/platypush/plugins/media/__init__.py @@ -13,6 +13,7 @@ from typing import ( Type, Union, ) +from urllib.parse import urlparse import requests @@ -27,12 +28,14 @@ from platypush.utils import ( ) from ._constants import audio_extensions, video_extensions -from ._model import DownloadState, PlayerState +from ._model import DownloadState, MediaDirectory, PlayerState from ._resource import MediaResource from ._resource.downloaders import DownloadThread, MediaResourceDownloader, downloaders from ._resource.parsers import MediaResourceParser, parsers from ._search import MediaSearcher, searchers +_MediaDirs = Union[str, Iterable[Union[str, dict]], Dict[str, Union[str, dict]]] + class MediaPlugin(RunnablePlugin, ABC): """ @@ -65,7 +68,7 @@ class MediaPlugin(RunnablePlugin, ABC): def __init__( self, - media_dirs: Optional[Union[str, Iterable[str], Dict[str, str]]] = None, + media_dirs: Optional[_MediaDirs] = None, download_dir: Optional[str] = None, env: Optional[Dict[str, str]] = None, volume: Optional[Union[float, int]] = None, @@ -82,8 +85,51 @@ class MediaPlugin(RunnablePlugin, ABC): """ :param media_dirs: Directories that will be scanned for media files when a search is performed (default: only ``download_dir``). You can - specify it either as a list of string or a map in the format - ``{: }``. + specify it either: + + - As a list of strings: + + .. code-block:: yaml + + media_dirs: + - /mnt/hd/media/movies + - /mnt/hd/media/music + - /mnt/hd/media/series + + - As a dictionary where the key is the name of the media display + name and the value is the path: + + .. code-block:: yaml + + media_dirs: + Movies: /mnt/hd/media/movies + Music: /mnt/hd/media/music + Series: /mnt/hd/media/series + + - As a dictionary where the key is the name of the media display + name and the value is a dictionary with the path and additional + display information: + + media_dirs: + Movies: + path: /mnt/hd/media/movies + icon: + url: https://example.com/icon.png + # FontAwesome icon classes are supported + class: fa fa-film + + Music: + path: /mnt/hd/media/music + icon: + url: https://example.com/icon.png + class: fa fa-music + + Series: + path: /mnt/hd/media/series + icon: + url: https://example.com/icon.png + class: fa fa-tv + :param download_dir: Directory where external resources/torrents will be downloaded (default: ~/Downloads) :param env: Environment variables key-values to pass to the @@ -150,10 +196,6 @@ class MediaPlugin(RunnablePlugin, ABC): if not player: raise AttributeError('No media plugin configured') - self.media_dirs = self._parse_media_dirs( - media_dirs or player_config.get('media_dirs', []) - ) - if self.__class__.__name__ == 'MediaPlugin': # Populate this plugin with the actions of the configured player for act in player.registered_actions: @@ -193,6 +235,10 @@ class MediaPlugin(RunnablePlugin, ABC): self.ytdl_args = ytdl_args or [] self._latest_resource: Optional[MediaResource] = None + self.media_dirs = self._parse_media_dirs( + media_dirs or player_config.get('media_dirs', []) + ) + self._parsers: Dict[Type[MediaResourceParser], MediaResourceParser] = { parser: parser(self) for parser in parsers } @@ -202,14 +248,16 @@ class MediaPlugin(RunnablePlugin, ABC): ] = {downloader: downloader(self) for downloader in downloaders} self._searchers: Dict[Type[MediaSearcher], MediaSearcher] = { - searcher: searcher(dirs=self.media_dirs.values(), media_plugin=self) + searcher: searcher( + dirs=[d.path for d in self.media_dirs.values()], media_plugin=self + ) for searcher in searchers } @staticmethod def _parse_media_dirs( - media_dirs: Optional[Union[str, Iterable[str], Dict[str, str]]] - ) -> Dict[str, str]: + media_dirs: Optional[_MediaDirs], + ) -> Dict[str, MediaDirectory]: dirs = {} if media_dirs: @@ -224,11 +272,38 @@ class MediaPlugin(RunnablePlugin, ABC): ret = {} for k, v in dirs.items(): - v = os.path.abspath(os.path.expanduser(v)) - if os.path.isdir(v): - ret[k] = v + assert isinstance(k, str), f'Invalid media_dirs key format: {k}' + if isinstance(v, str): + v = {'path': v} - return ret + assert isinstance(v, dict), f'Invalid media_dirs format: {v}' + path = v.get('path') + assert path, f'Missing path in media_dirs entry {k}' + path = os.path.abspath(os.path.expanduser(path)) + assert os.path.isdir(path), f'Invalid path in media_dirs entry {k}' + + icon = v.get('icon', {}) + if isinstance(icon, str): + # Fill up the URL field if it's a URL, otherwise assume that + # it's a FontAwesome icon class + icon = {'url': icon} if urlparse(icon).scheme else {'class': icon} + + ret[k] = MediaDirectory.build( + name=k, + path=path, + icon_class=icon.get('class'), + icon_url=icon.get('url'), + ) + + # Add the downloads directory if it's missing + if not any(d.path == get_default_downloads_dir() for d in ret.values()): + ret['Downloads'] = MediaDirectory.build( + name='Downloads', + path=get_default_downloads_dir(), + icon_class='fas fa-download', + ) + + return {k: ret[k] for k in sorted(ret.keys())} def _get_resource( self, @@ -713,14 +788,11 @@ class MediaPlugin(RunnablePlugin, ABC): ) @action - def get_media_dirs(self) -> Dict[str, str]: + def get_media_dirs(self) -> Dict[str, dict]: """ :return: List of configured media directories. """ - return { - 'Downloads': self.download_dir, - **self.media_dirs, - } + return {dir.name: dir.to_dict() for dir in self.media_dirs.values()} def _get_downloads(self, url: Optional[str] = None, path: Optional[str] = None): assert url or path, 'URL or path must be specified' diff --git a/platypush/plugins/media/_model.py b/platypush/plugins/media/_model.py index 04b3e14fea..5bf0da731f 100644 --- a/platypush/plugins/media/_model.py +++ b/platypush/plugins/media/_model.py @@ -1,4 +1,6 @@ import enum +from dataclasses import dataclass, field +from typing import Optional class PlayerState(enum.Enum): @@ -26,4 +28,51 @@ class DownloadState(enum.Enum): ERROR = 'error' +@dataclass +class MediaDirectoryIcon: + """ + Dataclass that represents a media directory icon. + """ + + class_: str = 'fas fa-folder' + url: Optional[str] = None + + def to_dict(self) -> dict: + """ + Convert the MediaDirectoryIcon instance to a dictionary. + """ + return {'class': self.class_, 'url': self.url} + + +@dataclass +class MediaDirectory: + """ + Dataclass that represents a media directory. + """ + + name: str + path: str + icon: MediaDirectoryIcon = field(default_factory=MediaDirectoryIcon) + + @classmethod + def build( + cls, + name: str, + path: str, + icon_class: Optional[str] = None, + icon_url: Optional[str] = None, + ) -> 'MediaDirectory': + """ + Create a MediaDirectory instance from a dictionary. + """ + icon_class = icon_class or 'fas fa-folder' + return cls(name, path, MediaDirectoryIcon(icon_class, icon_url)) + + def to_dict(self) -> dict: + """ + Convert the MediaDirectory instance to a dictionary. + """ + return {'name': self.name, 'path': self.path, 'icon': self.icon.to_dict()} + + # vim:sw=4:ts=4:et: