forked from platypush/platypush
[media] Support extended format/metadata for media dirs.
This commit is contained in:
parent
077e12e9a8
commit
213498318f
2 changed files with 141 additions and 20 deletions
|
@ -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
|
||||
``{<name>: <path>}``.
|
||||
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'
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue