[media] Allow media_dirs to be either a list or a dict.

This allows the user to have some user-friendly names for their
collections on the UI, such as `Movies` instead of
`/mnt/hd/media/movies`.
This commit is contained in:
Fabio Manganiello 2024-08-19 22:45:23 +02:00
parent 897e8a9ff7
commit 077e12e9a8
Signed by untrusted user: blacklight
GPG key ID: D90FBA7F76362774

View file

@ -7,7 +7,6 @@ from abc import ABC, abstractmethod
from typing import ( from typing import (
Dict, Dict,
Iterable, Iterable,
List,
Optional, Optional,
Sequence, Sequence,
Tuple, Tuple,
@ -66,7 +65,7 @@ class MediaPlugin(RunnablePlugin, ABC):
def __init__( def __init__(
self, self,
media_dirs: Optional[List[str]] = None, media_dirs: Optional[Union[str, Iterable[str], Dict[str, str]]] = None,
download_dir: Optional[str] = None, download_dir: Optional[str] = None,
env: Optional[Dict[str, str]] = None, env: Optional[Dict[str, str]] = None,
volume: Optional[Union[float, int]] = None, volume: Optional[Union[float, int]] = None,
@ -82,21 +81,21 @@ class MediaPlugin(RunnablePlugin, ABC):
): ):
""" """
:param media_dirs: Directories that will be scanned for media files when :param media_dirs: Directories that will be scanned for media files when
a search is performed (default: none) 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>}``.
:param download_dir: Directory where external resources/torrents will be :param download_dir: Directory where external resources/torrents will be
downloaded (default: ~/Downloads) downloaded (default: ~/Downloads)
:param env: Environment variables key-values to pass to the :param env: Environment variables key-values to pass to the
player executable (e.g. DISPLAY, XDG_VTNR, PULSE_SINK etc.) player executable (e.g. DISPLAY, XDG_VTNR, PULSE_SINK etc.)
:param volume: Default volume for the player (default: None, maximum volume). :param volume: Default volume for the player (default: None, maximum volume).
:param torrent_plugin: Optional plugin to be used for torrent download.
Possible values:
:param torrent_plugin: Optional plugin to be used for torrent download. Possible values: - ``torrent`` - native ``libtorrent``-based plugin (default,
recommended)
- ``torrent`` - native ``libtorrent``-based plugin (default, recommended) - ``rtorrent`` - torrent support over rtorrent RPC/XML interface
- ``rtorrent`` - torrent support over rtorrent RPC/XML interface - ``webtorrent`` - torrent support over webtorrent (unstable)
- ``webtorrent`` - torrent support over webtorrent (unstable)
:param youtube_format: Select the preferred video/audio format for :param youtube_format: Select the preferred video/audio format for
YouTube videos - and any media supported by youtube-dl or the YouTube videos - and any media supported by youtube-dl or the
@ -105,25 +104,20 @@ class MediaPlugin(RunnablePlugin, ABC):
info on supported formats. Example: info on supported formats. Example:
``bestvideo[height<=?1080][ext=mp4]+bestaudio`` - select the best ``bestvideo[height<=?1080][ext=mp4]+bestaudio`` - select the best
mp4 video with a resolution <= 1080p, and the best audio format. mp4 video with a resolution <= 1080p, and the best audio format.
:param youtube_audio_format: Select the preferred audio format for :param youtube_audio_format: Select the preferred audio format for
YouTube videos downloaded only for audio. Default: ``bestaudio``. YouTube videos downloaded only for audio. Default: ``bestaudio``.
:param youtube_dl: Path to the ``youtube-dl`` executable, used to :param youtube_dl: Path to the ``youtube-dl`` executable, used to
extract information from YouTube videos and other media platforms. extract information from YouTube videos and other media platforms.
Default: ``yt-dlp``. The default has changed from ``youtube-dl`` to Default: ``yt-dlp``. The default has changed from ``youtube-dl`` to
the ``yt-dlp`` fork because the former is badly maintained and its the ``yt-dlp`` fork because the former is badly maintained and its
latest release was pushed in 2021. latest release was pushed in 2021.
:param merge_output_format: If media download requires ``youtube_dl``, :param merge_output_format: If media download requires ``youtube_dl``,
and the upstream media contains both audio and video to be merged, and the upstream media contains both audio and video to be merged,
this can be used to specify the format of the output container - this can be used to specify the format of the output container -
e.g. ``mp4``, ``mkv``, ``avi``, ``flv``. Default: ``mp4``. e.g. ``mp4``, ``mkv``, ``avi``, ``flv``. Default: ``mp4``.
:param cache_dir: Directory where the media cache will be stored. If not :param cache_dir: Directory where the media cache will be stored. If not
specified, the cache will be stored in the default cache directory specified, the cache will be stored in the default cache directory
(usually ``~/.cache/platypush/media/<media_plugin>``). (usually ``~/.cache/platypush/media/<media_plugin>``).
:param cache_streams: If set to True, streams transcoded via yt-dlp or :param cache_streams: If set to True, streams transcoded via yt-dlp or
ffmpeg will be cached in ``cache_dir`` directory. If not set ffmpeg will be cached in ``cache_dir`` directory. If not set
(default), then streams will be played directly via memory pipe. (default), then streams will be played directly via memory pipe.
@ -132,16 +126,12 @@ class MediaPlugin(RunnablePlugin, ABC):
may be delayed. If set to False, the media will start playing as may be delayed. If set to False, the media will start playing as
soon as the stream is ready, but the quality may be lower, soon as the stream is ready, but the quality may be lower,
especially at the beginning, and seeking may not be supported. especially at the beginning, and seeking may not be supported.
:param ytdl_args: Additional arguments to pass to the youtube-dl :param ytdl_args: Additional arguments to pass to the youtube-dl
executable. Default: None. executable. Default: None.
""" """
super().__init__(**kwargs) super().__init__(**kwargs)
if media_dirs is None:
media_dirs = []
player = None player = None
player_config = {} player_config = {}
self._download_threads: Dict[Tuple[str, str], DownloadThread] = {} self._download_threads: Dict[Tuple[str, str], DownloadThread] = {}
@ -160,7 +150,9 @@ class MediaPlugin(RunnablePlugin, ABC):
if not player: if not player:
raise AttributeError('No media plugin configured') raise AttributeError('No media plugin configured')
media_dirs = media_dirs or player_config.get('media_dirs', []) self.media_dirs = self._parse_media_dirs(
media_dirs or player_config.get('media_dirs', [])
)
if self.__class__.__name__ == 'MediaPlugin': if self.__class__.__name__ == 'MediaPlugin':
# Populate this plugin with the actions of the configured player # Populate this plugin with the actions of the configured player
@ -170,13 +162,6 @@ class MediaPlugin(RunnablePlugin, ABC):
self._env = env or {} self._env = env or {}
self.cache_streams = cache_streams self.cache_streams = cache_streams
self.media_dirs = set(
filter(
os.path.isdir,
[os.path.abspath(os.path.expanduser(d)) for d in media_dirs],
)
)
self.download_dir = os.path.abspath( self.download_dir = os.path.abspath(
os.path.expanduser( os.path.expanduser(
download_dir download_dir
@ -198,7 +183,6 @@ class MediaPlugin(RunnablePlugin, ABC):
pathlib.Path(self.cache_dir).mkdir(parents=True, exist_ok=True) pathlib.Path(self.cache_dir).mkdir(parents=True, exist_ok=True)
pathlib.Path(self.download_dir).mkdir(parents=True, exist_ok=True) pathlib.Path(self.download_dir).mkdir(parents=True, exist_ok=True)
self._ytdl = youtube_dl self._ytdl = youtube_dl
self.media_dirs.add(self.download_dir)
self.volume = volume self.volume = volume
self._videos_queue = [] self._videos_queue = []
self._youtube_proc = None self._youtube_proc = None
@ -218,10 +202,34 @@ class MediaPlugin(RunnablePlugin, ABC):
] = {downloader: downloader(self) for downloader in downloaders} ] = {downloader: downloader(self) for downloader in downloaders}
self._searchers: Dict[Type[MediaSearcher], MediaSearcher] = { self._searchers: Dict[Type[MediaSearcher], MediaSearcher] = {
searcher: searcher(dirs=self.media_dirs, media_plugin=self) searcher: searcher(dirs=self.media_dirs.values(), media_plugin=self)
for searcher in searchers for searcher in searchers
} }
@staticmethod
def _parse_media_dirs(
media_dirs: Optional[Union[str, Iterable[str], Dict[str, str]]]
) -> Dict[str, str]:
dirs = {}
if media_dirs:
if isinstance(media_dirs, str):
dirs = [media_dirs]
if isinstance(media_dirs, (list, tuple, set)):
dirs = {d: d for d in media_dirs}
if isinstance(media_dirs, dict):
dirs = media_dirs
assert isinstance(dirs, dict), f'Invalid media_dirs format: {media_dirs}'
ret = {}
for k, v in dirs.items():
v = os.path.abspath(os.path.expanduser(v))
if os.path.isdir(v):
ret[k] = v
return ret
def _get_resource( def _get_resource(
self, self,
resource: str, resource: str,
@ -704,6 +712,16 @@ class MediaPlugin(RunnablePlugin, ABC):
many=True, many=True,
) )
@action
def get_media_dirs(self) -> Dict[str, str]:
"""
:return: List of configured media directories.
"""
return {
'Downloads': self.download_dir,
**self.media_dirs,
}
def _get_downloads(self, url: Optional[str] = None, path: Optional[str] = None): def _get_downloads(self, url: Optional[str] = None, path: Optional[str] = None):
assert url or path, 'URL or path must be specified' assert url or path, 'URL or path must be specified'
threads = [] threads = []