From: Marcel van der Veldt Date: Fri, 11 Sep 2020 20:34:13 +0000 (+0200) Subject: fix webplayer anf file provider X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=29ee4a53455f93f34d3aff015e37a45326be77ff;p=music-assistant-server.git fix webplayer anf file provider --- diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..fb7d2820 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +include *.txt +include README.rst +include LICENSE.md +graft music_assistant +recursive-exclude * *.py[co] \ No newline at end of file diff --git a/music_assistant.egg-info/SOURCES.txt b/music_assistant.egg-info/SOURCES.txt index f8adc6fd..09131263 100644 --- a/music_assistant.egg-info/SOURCES.txt +++ b/music_assistant.egg-info/SOURCES.txt @@ -1,6 +1,100 @@ +MANIFEST.in README.md +requirements.txt +requirements_dev.txt +requirements_lint.txt +requirements_test.txt setup.cfg setup.py +.tox/lint/lib/python3.7/site-packages/appdirs-1.4.4.dist-info/LICENSE.txt +.tox/lint/lib/python3.7/site-packages/appdirs-1.4.4.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/astroid-2.3.3.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/attrs-20.2.0.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/black-19.10b0.dist-info/entry_points.txt +.tox/lint/lib/python3.7/site-packages/black-19.10b0.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/blib2to3/Grammar.txt +.tox/lint/lib/python3.7/site-packages/blib2to3/PatternGrammar.txt +.tox/lint/lib/python3.7/site-packages/click-7.1.2.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/coverage-5.2.1.dist-info/LICENSE.txt +.tox/lint/lib/python3.7/site-packages/coverage-5.2.1.dist-info/entry_points.txt +.tox/lint/lib/python3.7/site-packages/coverage-5.2.1.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/flake8-3.7.9.dist-info/entry_points.txt +.tox/lint/lib/python3.7/site-packages/flake8-3.7.9.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/importlib_metadata-1.7.0.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/isort-4.3.21.dist-info/entry_points.txt +.tox/lint/lib/python3.7/site-packages/isort-4.3.21.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/lazy_object_proxy-1.4.3.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/mccabe-0.6.1.dist-info/entry_points.txt +.tox/lint/lib/python3.7/site-packages/mccabe-0.6.1.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/more_itertools-8.5.0.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/mypy-0.770.dist-info/entry_points.txt +.tox/lint/lib/python3.7/site-packages/mypy-0.770.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/mypy_extensions-0.4.3.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/packaging-20.4.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/pathspec-0.8.0.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/pip-20.2.2.dist-info/LICENSE.txt +.tox/lint/lib/python3.7/site-packages/pip-20.2.2.dist-info/entry_points.txt +.tox/lint/lib/python3.7/site-packages/pip-20.2.2.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/pip/_vendor/vendor.txt +.tox/lint/lib/python3.7/site-packages/pluggy-0.13.1.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/py-1.9.0.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/py/_vendored_packages/apipkg-1.4.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/py/_vendored_packages/iniconfig-1.0.0.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/pycodestyle-2.5.0.dist-info/LICENSE.txt +.tox/lint/lib/python3.7/site-packages/pycodestyle-2.5.0.dist-info/entry_points.txt +.tox/lint/lib/python3.7/site-packages/pycodestyle-2.5.0.dist-info/namespace_packages.txt +.tox/lint/lib/python3.7/site-packages/pycodestyle-2.5.0.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/pydocstyle-5.0.2.dist-info/entry_points.txt +.tox/lint/lib/python3.7/site-packages/pydocstyle-5.0.2.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/pydocstyle/data/imperatives.txt +.tox/lint/lib/python3.7/site-packages/pydocstyle/data/imperatives_blacklist.txt +.tox/lint/lib/python3.7/site-packages/pyflakes-2.1.1.dist-info/entry_points.txt +.tox/lint/lib/python3.7/site-packages/pyflakes-2.1.1.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/pylint-2.4.4.dist-info/entry_points.txt +.tox/lint/lib/python3.7/site-packages/pylint-2.4.4.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/pyparsing-2.4.7.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/pytest-5.4.1.dist-info/entry_points.txt +.tox/lint/lib/python3.7/site-packages/pytest-5.4.1.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/pytest_cov-2.8.1.dist-info/entry_points.txt +.tox/lint/lib/python3.7/site-packages/pytest_cov-2.8.1.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/pytest_timeout-1.3.4.dist-info/entry_points.txt +.tox/lint/lib/python3.7/site-packages/pytest_timeout-1.3.4.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/regex-2020.7.14.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/setuptools-49.6.0.dist-info/dependency_links.txt +.tox/lint/lib/python3.7/site-packages/setuptools-49.6.0.dist-info/entry_points.txt +.tox/lint/lib/python3.7/site-packages/setuptools-49.6.0.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/six-1.15.0.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/snowballstemmer-2.0.0.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/toml-0.10.1.dist-info/LICENSE.txt +.tox/lint/lib/python3.7/site-packages/toml-0.10.1.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/typed_ast-1.4.1.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/typing_extensions-3.7.4.3.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/wcwidth-0.2.5.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/wheel-0.35.1.dist-info/LICENSE.txt +.tox/lint/lib/python3.7/site-packages/wheel-0.35.1.dist-info/entry_points.txt +.tox/lint/lib/python3.7/site-packages/wheel-0.35.1.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/wrapt-1.11.2.dist-info/top_level.txt +.tox/lint/lib/python3.7/site-packages/zipp-3.1.0.dist-info/top_level.txt +.tox/mypy/lib/python3.7/site-packages/pip-20.2.2.dist-info/LICENSE.txt +.tox/mypy/lib/python3.7/site-packages/pip-20.2.2.dist-info/entry_points.txt +.tox/mypy/lib/python3.7/site-packages/pip-20.2.2.dist-info/top_level.txt +.tox/mypy/lib/python3.7/site-packages/pip/_vendor/vendor.txt +.tox/mypy/lib/python3.7/site-packages/setuptools-49.6.0.dist-info/dependency_links.txt +.tox/mypy/lib/python3.7/site-packages/setuptools-49.6.0.dist-info/entry_points.txt +.tox/mypy/lib/python3.7/site-packages/setuptools-49.6.0.dist-info/top_level.txt +.tox/mypy/lib/python3.7/site-packages/wheel-0.35.1.dist-info/LICENSE.txt +.tox/mypy/lib/python3.7/site-packages/wheel-0.35.1.dist-info/entry_points.txt +.tox/mypy/lib/python3.7/site-packages/wheel-0.35.1.dist-info/top_level.txt +.tox/py37/lib/python3.7/site-packages/pip-20.2.2.dist-info/LICENSE.txt +.tox/py37/lib/python3.7/site-packages/pip-20.2.2.dist-info/entry_points.txt +.tox/py37/lib/python3.7/site-packages/pip-20.2.2.dist-info/top_level.txt +.tox/py37/lib/python3.7/site-packages/pip/_vendor/vendor.txt +.tox/py37/lib/python3.7/site-packages/setuptools-49.6.0.dist-info/dependency_links.txt +.tox/py37/lib/python3.7/site-packages/setuptools-49.6.0.dist-info/entry_points.txt +.tox/py37/lib/python3.7/site-packages/setuptools-49.6.0.dist-info/top_level.txt +.tox/py37/lib/python3.7/site-packages/wheel-0.35.1.dist-info/LICENSE.txt +.tox/py37/lib/python3.7/site-packages/wheel-0.35.1.dist-info/entry_points.txt +.tox/py37/lib/python3.7/site-packages/wheel-0.35.1.dist-info/top_level.txt music_assistant/__init__.py music_assistant/__main__.py music_assistant/app_vars.py @@ -39,13 +133,223 @@ music_assistant/providers/chromecast/player.py music_assistant/providers/demo/__init__.py music_assistant/providers/demo/demo_musicprovider.py music_assistant/providers/demo/demo_playerprovider.py +music_assistant/providers/file/file.py music_assistant/providers/home_assistant/__init__.py music_assistant/providers/qobuz/__init__.py music_assistant/providers/sonos/__init__.py music_assistant/providers/sonos/sonos.py music_assistant/providers/spotify/__init__.py +music_assistant/providers/spotify/spotty/arm-linux/spotty-hf +music_assistant/providers/spotify/spotty/darwin/spotty +music_assistant/providers/spotify/spotty/windows/spotty.exe +music_assistant/providers/spotify/spotty/x86-linux/spotty +music_assistant/providers/spotify/spotty/x86-linux/spotty-x86_64 music_assistant/providers/squeezebox/__init__.py music_assistant/providers/squeezebox/constants.py music_assistant/providers/squeezebox/discovery.py music_assistant/providers/squeezebox/socket_client.py -music_assistant/providers/tunein/__init__.py \ No newline at end of file +music_assistant/providers/tunein/__init__.py +music_assistant/providers/webplayer/todo.py +venv/lib/python3.7/site-packages/Pillow-7.2.0.dist-info/top_level.txt +venv/lib/python3.7/site-packages/PyChromecast-7.2.1.dist-info/top_level.txt +venv/lib/python3.7/site-packages/PyJWT-1.7.1.dist-info/entry_points.txt +venv/lib/python3.7/site-packages/PyJWT-1.7.1.dist-info/top_level.txt +venv/lib/python3.7/site-packages/SoundFile-0.10.3.post1.dist-info/top_level.txt +venv/lib/python3.7/site-packages/Unidecode-1.1.1.dist-info/LICENSE.txt +venv/lib/python3.7/site-packages/Unidecode-1.1.1.dist-info/entry_points.txt +venv/lib/python3.7/site-packages/Unidecode-1.1.1.dist-info/top_level.txt +venv/lib/python3.7/site-packages/aiodns-2.0.0.dist-info/top_level.txt +venv/lib/python3.7/site-packages/aiohttp-3.6.2.dist-info/LICENSE.txt +venv/lib/python3.7/site-packages/aiohttp-3.6.2.dist-info/top_level.txt +venv/lib/python3.7/site-packages/aiohttp_cors-0.7.0.dist-info/top_level.txt +venv/lib/python3.7/site-packages/aiohttp_jwt-0.6.1.dist-info/LICENSE.txt +venv/lib/python3.7/site-packages/aiohttp_jwt-0.6.1.dist-info/top_level.txt +venv/lib/python3.7/site-packages/appdirs-1.4.4.dist-info/LICENSE.txt +venv/lib/python3.7/site-packages/appdirs-1.4.4.dist-info/top_level.txt +venv/lib/python3.7/site-packages/argparse-1.4.0.dist-info/top_level.txt +venv/lib/python3.7/site-packages/astroid-2.3.3.dist-info/top_level.txt +venv/lib/python3.7/site-packages/async_timeout-3.0.1.dist-info/top_level.txt +venv/lib/python3.7/site-packages/asyncio_throttle-1.0.1.dist-info/top_level.txt +venv/lib/python3.7/site-packages/attrs-20.2.0.dist-info/top_level.txt +venv/lib/python3.7/site-packages/black-19.10b0.dist-info/entry_points.txt +venv/lib/python3.7/site-packages/black-19.10b0.dist-info/top_level.txt +venv/lib/python3.7/site-packages/blib2to3/Grammar.txt +venv/lib/python3.7/site-packages/blib2to3/PatternGrammar.txt +venv/lib/python3.7/site-packages/brotlipy-0.7.0.dist-info/top_level.txt +venv/lib/python3.7/site-packages/casttube-0.2.1.dist-info/top_level.txt +venv/lib/python3.7/site-packages/cchardet-2.1.6.dist-info/top_level.txt +venv/lib/python3.7/site-packages/certifi-2020.6.20.dist-info/top_level.txt +venv/lib/python3.7/site-packages/cffi-1.14.2.dist-info/entry_points.txt +venv/lib/python3.7/site-packages/cffi-1.14.2.dist-info/top_level.txt +venv/lib/python3.7/site-packages/chardet-3.0.4.dist-info/entry_points.txt +venv/lib/python3.7/site-packages/chardet-3.0.4.dist-info/top_level.txt +venv/lib/python3.7/site-packages/click-7.1.2.dist-info/top_level.txt +venv/lib/python3.7/site-packages/coverage-5.2.1.dist-info/LICENSE.txt +venv/lib/python3.7/site-packages/coverage-5.2.1.dist-info/entry_points.txt +venv/lib/python3.7/site-packages/coverage-5.2.1.dist-info/top_level.txt +venv/lib/python3.7/site-packages/cryptography-3.1.dist-info/top_level.txt +venv/lib/python3.7/site-packages/cytoolz-0.10.1-py3.7.egg-info/SOURCES.txt +venv/lib/python3.7/site-packages/cytoolz-0.10.1-py3.7.egg-info/dependency_links.txt +venv/lib/python3.7/site-packages/cytoolz-0.10.1-py3.7.egg-info/installed-files.txt +venv/lib/python3.7/site-packages/cytoolz-0.10.1-py3.7.egg-info/requires.txt +venv/lib/python3.7/site-packages/cytoolz-0.10.1-py3.7.egg-info/top_level.txt +venv/lib/python3.7/site-packages/filelock-3.0.12.dist-info/top_level.txt +venv/lib/python3.7/site-packages/flake8-3.7.9.dist-info/entry_points.txt +venv/lib/python3.7/site-packages/flake8-3.7.9.dist-info/top_level.txt +venv/lib/python3.7/site-packages/future-0.18.2-py3.7.egg-info/SOURCES.txt +venv/lib/python3.7/site-packages/future-0.18.2-py3.7.egg-info/dependency_links.txt +venv/lib/python3.7/site-packages/future-0.18.2-py3.7.egg-info/entry_points.txt +venv/lib/python3.7/site-packages/future-0.18.2-py3.7.egg-info/installed-files.txt +venv/lib/python3.7/site-packages/future-0.18.2-py3.7.egg-info/top_level.txt +venv/lib/python3.7/site-packages/hass_client-0.0.6.dist-info/top_level.txt +venv/lib/python3.7/site-packages/idna-2.10.dist-info/top_level.txt +venv/lib/python3.7/site-packages/ifaddr-0.1.7.dist-info/LICENSE.txt +venv/lib/python3.7/site-packages/ifaddr-0.1.7.dist-info/top_level.txt +venv/lib/python3.7/site-packages/importlib_metadata-1.7.0.dist-info/top_level.txt +venv/lib/python3.7/site-packages/isort-4.3.21.dist-info/entry_points.txt +venv/lib/python3.7/site-packages/isort-4.3.21.dist-info/top_level.txt +venv/lib/python3.7/site-packages/lazy_object_proxy-1.4.3.dist-info/top_level.txt +venv/lib/python3.7/site-packages/mccabe-0.6.1.dist-info/entry_points.txt +venv/lib/python3.7/site-packages/mccabe-0.6.1.dist-info/top_level.txt +venv/lib/python3.7/site-packages/memory_tempfile-2.2.3.dist-info/LICENSE.txt +venv/lib/python3.7/site-packages/more_itertools-8.5.0.dist-info/top_level.txt +venv/lib/python3.7/site-packages/multidict-4.7.6.dist-info/top_level.txt +venv/lib/python3.7/site-packages/music_assistant-1.0.0-py3.7.egg/EGG-INFO/SOURCES.txt +venv/lib/python3.7/site-packages/music_assistant-1.0.0-py3.7.egg/EGG-INFO/dependency_links.txt +venv/lib/python3.7/site-packages/music_assistant-1.0.0-py3.7.egg/EGG-INFO/entry_points.txt +venv/lib/python3.7/site-packages/music_assistant-1.0.0-py3.7.egg/EGG-INFO/requires.txt +venv/lib/python3.7/site-packages/music_assistant-1.0.0-py3.7.egg/EGG-INFO/top_level.txt +venv/lib/python3.7/site-packages/mypy-0.770.dist-info/entry_points.txt +venv/lib/python3.7/site-packages/mypy-0.770.dist-info/top_level.txt +venv/lib/python3.7/site-packages/mypy_extensions-0.4.3.dist-info/top_level.txt +venv/lib/python3.7/site-packages/numpy/LICENSE.txt +venv/lib/python3.7/site-packages/numpy-1.19.2.dist-info/LICENSE.txt +venv/lib/python3.7/site-packages/numpy-1.19.2.dist-info/LICENSES_bundled.txt +venv/lib/python3.7/site-packages/numpy-1.19.2.dist-info/entry_points.txt +venv/lib/python3.7/site-packages/numpy-1.19.2.dist-info/top_level.txt +venv/lib/python3.7/site-packages/numpy/core/include/numpy/multiarray_api.txt +venv/lib/python3.7/site-packages/numpy/core/include/numpy/ufunc_api.txt +venv/lib/python3.7/site-packages/packaging-20.4.dist-info/top_level.txt +venv/lib/python3.7/site-packages/passlib-1.7.2.dist-info/top_level.txt +venv/lib/python3.7/site-packages/passlib/_data/wordsets/bip39.txt +venv/lib/python3.7/site-packages/passlib/_data/wordsets/eff_long.txt +venv/lib/python3.7/site-packages/passlib/_data/wordsets/eff_prefixed.txt +venv/lib/python3.7/site-packages/passlib/_data/wordsets/eff_short.txt +venv/lib/python3.7/site-packages/pathspec-0.8.0.dist-info/top_level.txt +venv/lib/python3.7/site-packages/pip-19.2.3.dist-info/LICENSE.txt +venv/lib/python3.7/site-packages/pip-19.2.3.dist-info/entry_points.txt +venv/lib/python3.7/site-packages/pip-19.2.3.dist-info/top_level.txt +venv/lib/python3.7/site-packages/pluggy-0.13.1.dist-info/top_level.txt +venv/lib/python3.7/site-packages/protobuf-3.13.0.dist-info/namespace_packages.txt +venv/lib/python3.7/site-packages/protobuf-3.13.0.dist-info/top_level.txt +venv/lib/python3.7/site-packages/py-1.9.0.dist-info/top_level.txt +venv/lib/python3.7/site-packages/py/_vendored_packages/apipkg-1.4.dist-info/top_level.txt +venv/lib/python3.7/site-packages/py/_vendored_packages/iniconfig-1.0.0.dist-info/top_level.txt +venv/lib/python3.7/site-packages/pycares-3.1.1.dist-info/top_level.txt +venv/lib/python3.7/site-packages/pycodestyle-2.5.0.dist-info/LICENSE.txt +venv/lib/python3.7/site-packages/pycodestyle-2.5.0.dist-info/entry_points.txt +venv/lib/python3.7/site-packages/pycodestyle-2.5.0.dist-info/namespace_packages.txt +venv/lib/python3.7/site-packages/pycodestyle-2.5.0.dist-info/top_level.txt +venv/lib/python3.7/site-packages/pycparser-2.20.dist-info/top_level.txt +venv/lib/python3.7/site-packages/pydocstyle-5.0.2.dist-info/entry_points.txt +venv/lib/python3.7/site-packages/pydocstyle-5.0.2.dist-info/top_level.txt +venv/lib/python3.7/site-packages/pydocstyle/data/imperatives.txt +venv/lib/python3.7/site-packages/pydocstyle/data/imperatives_blacklist.txt +venv/lib/python3.7/site-packages/pyflakes-2.1.1.dist-info/entry_points.txt +venv/lib/python3.7/site-packages/pyflakes-2.1.1.dist-info/top_level.txt +venv/lib/python3.7/site-packages/pylint-2.4.4.dist-info/entry_points.txt +venv/lib/python3.7/site-packages/pylint-2.4.4.dist-info/top_level.txt +venv/lib/python3.7/site-packages/pyloudnorm-0.1.0.dist-info/top_level.txt +venv/lib/python3.7/site-packages/pyparsing-2.4.7.dist-info/top_level.txt +venv/lib/python3.7/site-packages/pytaglib-1.4.6-py3.7.egg-info/SOURCES.txt +venv/lib/python3.7/site-packages/pytaglib-1.4.6-py3.7.egg-info/dependency_links.txt +venv/lib/python3.7/site-packages/pytaglib-1.4.6-py3.7.egg-info/entry_points.txt +venv/lib/python3.7/site-packages/pytaglib-1.4.6-py3.7.egg-info/installed-files.txt +venv/lib/python3.7/site-packages/pytaglib-1.4.6-py3.7.egg-info/top_level.txt +venv/lib/python3.7/site-packages/pytest-5.4.1.dist-info/entry_points.txt +venv/lib/python3.7/site-packages/pytest-5.4.1.dist-info/top_level.txt +venv/lib/python3.7/site-packages/pytest_cov-2.8.1.dist-info/entry_points.txt +venv/lib/python3.7/site-packages/pytest_cov-2.8.1.dist-info/top_level.txt +venv/lib/python3.7/site-packages/pytest_timeout-1.3.4.dist-info/entry_points.txt +venv/lib/python3.7/site-packages/pytest_timeout-1.3.4.dist-info/top_level.txt +venv/lib/python3.7/site-packages/python_slugify-4.0.1.dist-info/entry_points.txt +venv/lib/python3.7/site-packages/python_slugify-4.0.1.dist-info/top_level.txt +venv/lib/python3.7/site-packages/python_vlc-3.0.11115.dist-info/top_level.txt +venv/lib/python3.7/site-packages/regex-2020.7.14.dist-info/top_level.txt +venv/lib/python3.7/site-packages/requests-2.24.0.dist-info/LICENSE.txt +venv/lib/python3.7/site-packages/requests-2.24.0.dist-info/top_level.txt +venv/lib/python3.7/site-packages/scipy/HACKING.rst.txt +venv/lib/python3.7/site-packages/scipy/INSTALL.rst.txt +venv/lib/python3.7/site-packages/scipy/LICENSE.txt +venv/lib/python3.7/site-packages/scipy/LICENSES_bundled.txt +venv/lib/python3.7/site-packages/scipy/THANKS.txt +venv/lib/python3.7/site-packages/scipy/mypy_requirements.txt +venv/lib/python3.7/site-packages/scipy-1.5.2.dist-info/LICENSE.txt +venv/lib/python3.7/site-packages/scipy-1.5.2.dist-info/LICENSES_bundled.txt +venv/lib/python3.7/site-packages/scipy-1.5.2.dist-info/top_level.txt +venv/lib/python3.7/site-packages/scipy/io/matlab/tests/data/japanese_utf8.txt +venv/lib/python3.7/site-packages/scipy/ndimage/tests/data/README.txt +venv/lib/python3.7/site-packages/scipy/ndimage/tests/data/label_inputs.txt +venv/lib/python3.7/site-packages/scipy/ndimage/tests/data/label_results.txt +venv/lib/python3.7/site-packages/scipy/ndimage/tests/data/label_strels.txt +venv/lib/python3.7/site-packages/scipy/sparse/linalg/dsolve/SuperLU/License.txt +venv/lib/python3.7/site-packages/scipy/spatial/qhull_src/COPYING.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/cdist-X1.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/cdist-X2.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/iris.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/pdist-boolean-inp.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/pdist-chebyshev-ml-iris.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/pdist-chebyshev-ml.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/pdist-cityblock-ml-iris.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/pdist-cityblock-ml.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/pdist-correlation-ml-iris.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/pdist-correlation-ml.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/pdist-cosine-ml-iris.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/pdist-cosine-ml.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/pdist-double-inp.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/pdist-euclidean-ml-iris.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/pdist-euclidean-ml.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/pdist-hamming-ml.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/pdist-jaccard-ml.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/pdist-jensenshannon-ml-iris.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/pdist-jensenshannon-ml.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/pdist-minkowski-3.2-ml-iris.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/pdist-minkowski-3.2-ml.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/pdist-minkowski-5.8-ml-iris.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/pdist-seuclidean-ml-iris.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/pdist-seuclidean-ml.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/pdist-spearman-ml.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/random-bool-data.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/random-double-data.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/random-int-data.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/random-uint-data.txt +venv/lib/python3.7/site-packages/scipy/spatial/tests/data/selfdual-4d-polytope.txt +venv/lib/python3.7/site-packages/setuptools-41.2.0.dist-info/dependency_links.txt +venv/lib/python3.7/site-packages/setuptools-41.2.0.dist-info/entry_points.txt +venv/lib/python3.7/site-packages/setuptools-41.2.0.dist-info/top_level.txt +venv/lib/python3.7/site-packages/six-1.15.0.dist-info/top_level.txt +venv/lib/python3.7/site-packages/snowballstemmer-2.0.0.dist-info/top_level.txt +venv/lib/python3.7/site-packages/soco-0.19.dist-info/top_level.txt +venv/lib/python3.7/site-packages/text_unidecode-1.3.dist-info/LICENSE.txt +venv/lib/python3.7/site-packages/text_unidecode-1.3.dist-info/top_level.txt +venv/lib/python3.7/site-packages/toml-0.10.1.dist-info/LICENSE.txt +venv/lib/python3.7/site-packages/toml-0.10.1.dist-info/top_level.txt +venv/lib/python3.7/site-packages/toolz-0.10.0-py3.7.egg-info/SOURCES.txt +venv/lib/python3.7/site-packages/toolz-0.10.0-py3.7.egg-info/dependency_links.txt +venv/lib/python3.7/site-packages/toolz-0.10.0-py3.7.egg-info/installed-files.txt +venv/lib/python3.7/site-packages/toolz-0.10.0-py3.7.egg-info/top_level.txt +venv/lib/python3.7/site-packages/tox-3.14.6.dist-info/entry_points.txt +venv/lib/python3.7/site-packages/tox-3.14.6.dist-info/top_level.txt +venv/lib/python3.7/site-packages/typed_ast-1.4.1.dist-info/top_level.txt +venv/lib/python3.7/site-packages/typing_extensions-3.7.4.3.dist-info/top_level.txt +venv/lib/python3.7/site-packages/urllib3-1.25.10.dist-info/LICENSE.txt +venv/lib/python3.7/site-packages/urllib3-1.25.10.dist-info/top_level.txt +venv/lib/python3.7/site-packages/uvloop-0.14.0.dist-info/top_level.txt +venv/lib/python3.7/site-packages/virtualenv-20.0.31.dist-info/entry_points.txt +venv/lib/python3.7/site-packages/virtualenv-20.0.31.dist-info/top_level.txt +venv/lib/python3.7/site-packages/wcwidth-0.2.5.dist-info/top_level.txt +venv/lib/python3.7/site-packages/wrapt-1.11.2.dist-info/top_level.txt +venv/lib/python3.7/site-packages/xmltodict-0.12.0.dist-info/top_level.txt +venv/lib/python3.7/site-packages/yarl-1.5.1.dist-info/top_level.txt +venv/lib/python3.7/site-packages/zeroconf-0.28.4.dist-info/top_level.txt +venv/lib/python3.7/site-packages/zipp-3.1.0.dist-info/top_level.txt \ No newline at end of file diff --git a/music_assistant/models/media_types.py b/music_assistant/models/media_types.py index de0e8f79..a05afb2b 100755 --- a/music_assistant/models/media_types.py +++ b/music_assistant/models/media_types.py @@ -88,7 +88,7 @@ class MediaItem(object): name: str = "" metadata: dict = field(default_factory=dict) tags: List[str] = field(default_factory=list) - external_ids: List[ExternalId] = field(default_factory=dict) + external_ids: dict = field(default_factory=dict) provider_ids: List[MediaItemProviderId] = field(default_factory=list) in_library: List[str] = field(default_factory=list) is_lazy: bool = False diff --git a/music_assistant/providers/file/__init__.py b/music_assistant/providers/file/__init__.py new file mode 100644 index 00000000..32279e50 --- /dev/null +++ b/music_assistant/providers/file/__init__.py @@ -0,0 +1,411 @@ +"""Filesystem musicprovider support for MusicAssistant.""" +import base64 +import logging +import os +from typing import List, Optional + +import taglib +from music_assistant.models.config_entry import ConfigEntry, ConfigEntryType +from music_assistant.models.media_types import ( + Album, + Artist, + MediaItemProviderId, + MediaType, + Playlist, + SearchResult, + Track, + TrackQuality, +) +from music_assistant.models.musicprovider import MusicProvider +from music_assistant.models.streamdetails import ContentType, StreamDetails, StreamType +from music_assistant.utils import parse_title_and_version + +PROV_ID = "file" +PROV_NAME = "Local files and playlists" + +LOGGER = logging.getLogger(PROV_ID) + +CONF_MUSIC_DIR = "music_dir" +CONF_PLAYLISTS_DIR = "playlists_dir" + +CONFIG_ENTRIES = [ + ConfigEntry( + entry_key=CONF_MUSIC_DIR, + entry_type=ConfigEntryType.STRING, + description_key="file_prov_music_path", + ), + ConfigEntry( + entry_key=CONF_PLAYLISTS_DIR, + entry_type=ConfigEntryType.STRING, + description_key="file_prov_playlists_path", + ), +] + + +async def async_setup(mass): + """Perform async setup of this Plugin/Provider.""" + prov = FileProvider() + await mass.async_register_provider(prov) + + +class FileProvider(MusicProvider): + """ + Very basic implementation of a musicprovider for local files. + + Assumes files are stored on disk in format // + Reads ID3 tags from file and falls back to parsing filename + Supports m3u files only for playlists + Supports having URI's from streaming providers within m3u playlist + Should be compatible with LMS + """ + + _music_dir = None + _playlists_dir = None + + @property + def id(self) -> str: + """Return provider ID for this provider.""" + return PROV_ID + + @property + def name(self) -> str: + """Return provider Name for this provider.""" + return PROV_NAME + + @property + def config_entries(self) -> List[ConfigEntry]: + """Return Config Entries for this provider.""" + return CONFIG_ENTRIES + + @property + def supported_mediatypes(self) -> List[MediaType]: + """Return MediaTypes the provider supports.""" + return [MediaType.Album, MediaType.Artist, MediaType.Playlist, MediaType.Track] + + async def async_on_start(self) -> bool: + """Handle initialization of the provider based on config.""" + conf = self.mass.config.get_provider_config(self.id) + if not os.path.isdir(conf[CONF_MUSIC_DIR]): + raise FileNotFoundError(f"Directory {conf[CONF_MUSIC_DIR]} does not exist") + self._music_dir = conf["music_dir"] + if os.path.isdir(conf[CONF_PLAYLISTS_DIR]): + self._playlists_dir = conf[CONF_PLAYLISTS_DIR] + else: + self._playlists_dir = None + + async def async_on_stop(self): + """Handle correct close/cleanup of the provider on exit.""" + # nothing to be done + + async def async_search( + self, search_query: str, media_types=Optional[List[MediaType]], limit: int = 5 + ) -> SearchResult: + """ + Perform search on musicprovider. + + :param search_query: Search query. + :param media_types: A list of media_types to include. All types if None. + :param limit: Number of items to return in the search (per type). + """ + result = SearchResult() + # TODO ! + return result + + async def async_get_library_artists(self) -> List[Artist]: + """Retrieve all library artists.""" + if not os.path.isdir(self._music_dir): + LOGGER.error("music path does not exist: %s" % self._music_dir) + return + yield + for dirname in os.listdir(self._music_dir): + dirpath = os.path.join(self._music_dir, dirname) + if os.path.isdir(dirpath) and not dirpath.startswith("."): + artist = await self.get_artist(dirpath) + if artist: + yield artist + + async def async_get_library_albums(self) -> List[Album]: + """Get album folders recursively.""" + async for artist in self.get_library_artists(): + async for album in self.get_artist_albums(artist.item_id): + yield album + + async def async_get_library_tracks(self) -> List[Track]: + """Get all tracks recursively.""" + # TODO: support disk subfolders + async for album in self.get_library_albums(): + async for track in self.get_album_tracks(album.item_id): + yield track + + async def async_get_library_playlists(self) -> List[Playlist]: + """Retrieve playlists from disk.""" + if not self._playlists_dir: + return + yield + for filename in os.listdir(self._playlists_dir): + filepath = os.path.join(self._playlists_dir, filename) + if ( + os.path.isfile(filepath) + and not filename.startswith(".") + and filename.lower().endswith(".m3u") + ): + playlist = await self.get_playlist(filepath) + if playlist: + yield playlist + + async def async_get_artist(self, prov_item_id: str) -> Artist: + """Get full artist details by id.""" + if os.sep not in prov_item_id: + itempath = base64.b64decode(prov_item_id).decode("utf-8") + else: + itempath = prov_item_id + prov_item_id = base64.b64encode(itempath.encode("utf-8")).decode("utf-8") + if not os.path.isdir(itempath): + LOGGER.error("Artist path does not exist: %s" % itempath) + return None + name = itempath.split(os.sep)[-1] + artist = Artist() + artist.item_id = prov_item_id + artist.provider = PROV_ID + artist.name = name + artist.provider_ids.append( + MediaItemProviderId(provider=PROV_ID, item_id=artist.item_id) + ) + return artist + + async def async_get_album(self, prov_item_id: str) -> Album: + """Get full album details by id.""" + if os.sep not in prov_item_id: + itempath = base64.b64decode(prov_item_id).decode("utf-8") + else: + itempath = prov_item_id + prov_item_id = base64.b64encode(itempath.encode("utf-8")).decode("utf-8") + if not os.path.isdir(itempath): + LOGGER.error("album path does not exist: %s" % itempath) + return None + name = itempath.split(os.sep)[-1] + artistpath = itempath.rsplit(os.sep, 1)[0] + album = Album() + album.item_id = prov_item_id + album.provider = self.prov_id + album.name, album.version = parse_title_and_version(name) + album.artist = await self.get_artist(artistpath) + if not album.artist: + raise Exception("No album artist ! %s" % artistpath) + album.provider_ids.append( + MediaItemProviderId(provider=PROV_ID, item_id=prov_item_id) + ) + return album + + async def async_get_track(self, prov_item_id: str) -> Track: + """Get full track details by id.""" + if os.sep not in prov_item_id: + itempath = base64.b64decode(prov_item_id).decode("utf-8") + else: + itempath = prov_item_id + if not os.path.isfile(itempath): + LOGGER.error("track path does not exist: %s", itempath) + return None + return await self.__parse_track(itempath) + + async def async_get_playlist(self, prov_item_id: str) -> Playlist: + """Get full playlist details by id.""" + if os.sep not in prov_item_id: + itempath = base64.b64decode(prov_item_id).decode("utf-8") + else: + itempath = prov_item_id + prov_item_id = base64.b64encode(itempath.encode("utf-8")).decode("utf-8") + if not os.path.isfile(itempath): + LOGGER.error("playlist path does not exist: %s" % itempath) + return None + playlist = Playlist() + playlist.item_id = prov_item_id + playlist.provider = self.prov_id + playlist.name = itempath.split(os.sep)[-1].replace(".m3u", "") + playlist.is_editable = True + playlist.provider_ids.append( + MediaItemProviderId(provider=PROV_ID, item_id=prov_item_id) + ) + playlist.owner = "disk" + playlist.checksum = os.path.getmtime(itempath) + return playlist + + async def async_get_album_tracks(self, prov_album_id) -> List[Track]: + """Get album tracks for given album id.""" + if os.sep not in prov_album_id: + albumpath = base64.b64decode(prov_album_id).decode("utf-8") + else: + albumpath = prov_album_id + if not os.path.isdir(albumpath): + LOGGER.error("album path does not exist: %s" % albumpath) + return + album = await self.get_album(albumpath) + for filename in os.listdir(albumpath): + filepath = os.path.join(albumpath, filename) + if os.path.isfile(filepath) and not filepath.startswith("."): + track = await self.__parse_track(filepath) + if track: + track.album = album + yield track + + async def async_get_playlist_tracks( + self, prov_playlist_id: str, limit: int = 50, offset: int = 0 + ) -> List[Track]: + """Get playlist tracks for given playlist id.""" + if os.sep not in prov_playlist_id: + itempath = base64.b64decode(prov_playlist_id).decode("utf-8") + else: + itempath = prov_playlist_id + if not os.path.isfile(itempath): + LOGGER.error("playlist path does not exist: %s" % itempath) + return + counter = 0 + index = 0 + with open(itempath) as f: + for line in f.readlines(): + line = line.strip() + if line and not line.startswith("#"): + counter += 1 + if counter > offset: + track = await self.__parse_track_from_uri(line) + if track: + yield track + index += 1 + if limit and index == limit: + break + + async def async_get_artist_albums(self, prov_artist_id: str) -> List[Album]: + """Get a list of albums for the given artist.""" + if os.sep not in prov_artist_id: + artistpath = base64.b64decode(prov_artist_id).decode("utf-8") + else: + artistpath = prov_artist_id + if not os.path.isdir(artistpath): + LOGGER.error("artist path does not exist: %s" % artistpath) + return + for dirname in os.listdir(artistpath): + dirpath = os.path.join(artistpath, dirname) + if os.path.isdir(dirpath) and not dirpath.startswith("."): + album = await self.get_album(dirpath) + if album: + yield album + + async def async_get_artist_toptracks(self, prov_artist_id: str) -> List[Track]: + """Get a list of random tracks as we have no clue about preference.""" + async for album in self.get_artist_albums(prov_artist_id): + async for track in self.get_album_tracks(album.item_id): + yield track + + async def async_get_stream_details(self, track_id): + """Return the content details for the given track when it will be streamed.""" + if os.sep not in track_id: + track_id = base64.b64decode(track_id).decode("utf-8") + if not os.path.isfile(track_id): + return None + # TODO: retrieve sanple rate and bitdepth + return StreamDetails( + type=StreamType.FILE, + provider=PROV_ID, + item_id=track_id, + content_type=ContentType(track_id.split(".")[-1]), + path=track_id, + sample_rate=44100, + bit_depth=16, + ) + + async def __async_parse_track(self, filename): + """Try to parse a track from a filename with taglib.""" + track = Track() + # pylint: disable=broad-except + try: + song = taglib.File(filename) + except Exception: + return None # not a media file ? + prov_item_id = base64.b64encode(filename.encode("utf-8")).decode("utf-8") + track.duration = song.length + track.item_id = prov_item_id + track.provider = self.prov_id + name = song.tags["TITLE"][0] + track.name, track.version = parse_title_and_version(name) + albumpath = filename.rsplit(os.sep, 1)[0] + track.album = await self.get_album(albumpath) + artists = [] + for artist_str in song.tags["ARTIST"]: + local_artist_path = os.path.join(self._music_dir, artist_str) + if os.path.isfile(local_artist_path): + artist = await self.get_artist(local_artist_path) + else: + artist = Artist() + artist.name = artist_str + fake_artistpath = os.path.join(self._music_dir, artist_str) + artist.item_id = fake_artistpath # temporary id + artist.provider_ids.append( + MediaItemProviderId( + provider=PROV_ID, + item_id=base64.b64encode( + fake_artistpath.encode("utf-8") + ).decode("utf-8"), + ) + ) + artists.append(artist) + track.artists = artists + if "GENRE" in song.tags: + track.tags = song.tags["GENRE"] + if "ISRC" in song.tags: + track.external_ids["isrc"] = song.tags["ISRC"][0] + if "DISCNUMBER" in song.tags: + track.disc_number = int(song.tags["DISCNUMBER"][0]) + if "TRACKNUMBER" in song.tags: + track.track_number = int(song.tags["TRACKNUMBER"][0]) + quality_details = "" + if filename.endswith(".flac"): + # TODO: get bit depth + quality = TrackQuality.FLAC_LOSSLESS + if song.sampleRate > 192000: + quality = TrackQuality.FLAC_LOSSLESS_HI_RES_4 + elif song.sampleRate > 96000: + quality = TrackQuality.FLAC_LOSSLESS_HI_RES_3 + elif song.sampleRate > 48000: + quality = TrackQuality.FLAC_LOSSLESS_HI_RES_2 + quality_details = "%s Khz" % (song.sampleRate / 1000) + elif filename.endswith(".ogg"): + quality = TrackQuality.LOSSY_OGG + quality_details = "%s kbps" % (song.bitrate) + elif filename.endswith(".m4a"): + quality = TrackQuality.LOSSY_AAC + quality_details = "%s kbps" % (song.bitrate) + else: + quality = TrackQuality.LOSSY_MP3 + quality_details = "%s kbps" % (song.bitrate) + track.provider_ids.append( + MediaItemProviderId( + provider=PROV_ID, + item_id=prov_item_id, + quality=quality, + details=quality_details, + ) + ) + return track + + async def __async_parse_track_from_uri(self, uri): + """Try to parse a track from an uri found in playlist.""" + if "://" in uri: + # track is uri from external provider? + prov_id = uri.split("://")[0] + prov_item_id = uri.split("/")[-1].split(".")[0].split(":")[-1] + try: + return await self.mass.music_manager.async_get_track( + prov_item_id, prov_id, lazy=False + ) + except Exception as exc: + LOGGER.warning("Could not parse uri %s to track: %s" % (uri, str(exc))) + return None + # try to treat uri as filename + # TODO: filename could be related to musicdir or full path + track = await self.async_get_track(uri) + if track: + return track + track = await self.async_get_track(os.path.join(self._music_dir, uri)) + if track: + return track + return None diff --git a/music_assistant/providers/file/file.py b/music_assistant/providers/file/file.py deleted file mode 100644 index 7aa7f227..00000000 --- a/music_assistant/providers/file/file.py +++ /dev/null @@ -1,347 +0,0 @@ -"""Filesystem musicprovider support for MusicAssistant.""" -# pylint: skip-file -# flake8: noqa -import base64 -import os -from typing import List - -import taglib -from music_assistant.constants import CONF_ENABLED -from music_assistant.models.media_types import ( - Album, - Artist, - MediaType, - Playlist, - Track, - TrackQuality, -) -from music_assistant.models.musicprovider import MusicProvider -from music_assistant.utils import LOGGER, parse_title_and_version - -PROV_NAME = "Local files and playlists" -PROV_CLASS = "FileProvider" - -CONFIG_ENTRIES = [ - (CONF_ENABLED, False, CONF_ENABLED), - ("music_dir", "", "file_prov_music_path"), - ("playlists_dir", "", "file_prov_playlists_path"), -] - - -class FileProvider(MusicProvider): - """ - Very basic implementation of a musicprovider for local files - Assumes files are stored on disk in format // - Reads ID3 tags from file and falls back to parsing filename - Supports m3u files only for playlists - Supports having URI's from streaming providers within m3u playlist - Should be compatible with LMS - """ - - _music_dir = None - _playlists_dir = None - - async def async_setup(self, conf): - """setup the provider, return True if succesfull""" - if not os.path.isdir(conf["music_dir"]): - raise FileNotFoundError(f"Directory {conf['music_dir']} does not exist") - self._music_dir = conf["music_dir"] - if os.path.isdir(conf["playlists_dir"]): - self._playlists_dir = conf["playlists_dir"] - else: - self._playlists_dir = None - - async def async_search(self, searchstring, media_types=List[MediaType], limit=5): - """perform search on the provider""" - result = {"artists": [], "albums": [], "tracks": [], "playlists": []} - return result - - async def async_get_library_artists(self) -> List[Artist]: - """get artist folders in music directory""" - if not os.path.isdir(self._music_dir): - LOGGER.error("music path does not exist: %s" % self._music_dir) - return - yield - for dirname in os.listdir(self._music_dir): - dirpath = os.path.join(self._music_dir, dirname) - if os.path.isdir(dirpath) and not dirpath.startswith("."): - artist = await self.get_artist(dirpath) - if artist: - yield artist - - async def async_get_library_albums(self) -> List[Album]: - """get album folders recursively""" - async for artist in self.get_library_artists(): - async for album in self.get_artist_albums(artist.item_id): - yield album - - async def async_get_library_tracks(self) -> List[Track]: - """get all tracks recursively""" - # TODO: support disk subfolders - async for album in self.get_library_albums(): - async for track in self.get_album_tracks(album.item_id): - yield track - - async def async_get_library_playlists(self) -> List[Playlist]: - """retrieve playlists from disk""" - if not self._playlists_dir: - return - yield - for filename in os.listdir(self._playlists_dir): - filepath = os.path.join(self._playlists_dir, filename) - if ( - os.path.isfile(filepath) - and not filename.startswith(".") - and filename.lower().endswith(".m3u") - ): - playlist = await self.get_playlist(filepath) - if playlist: - yield playlist - - async def async_get_artist(self, prov_item_id) -> Artist: - """get full artist details by id""" - if not os.sep in prov_item_id: - itempath = base64.b64decode(prov_item_id).decode("utf-8") - else: - itempath = prov_item_id - prov_item_id = base64.b64encode(itempath.encode("utf-8")).decode("utf-8") - if not os.path.isdir(itempath): - LOGGER.error("artist path does not exist: %s" % itempath) - return None - name = itempath.split(os.sep)[-1] - artist = Artist() - artist.item_id = prov_item_id - artist.provider = self.prov_id - artist.name = name - artist.ids.append({"provider": self.prov_id, "item_id": artist.item_id}) - return artist - - async def async_get_album(self, prov_item_id) -> Album: - """get full album details by id""" - if not os.sep in prov_item_id: - itempath = base64.b64decode(prov_item_id).decode("utf-8") - else: - itempath = prov_item_id - prov_item_id = base64.b64encode(itempath.encode("utf-8")).decode("utf-8") - if not os.path.isdir(itempath): - LOGGER.error("album path does not exist: %s" % itempath) - return None - name = itempath.split(os.sep)[-1] - artistpath = itempath.rsplit(os.sep, 1)[0] - album = Album() - album.item_id = prov_item_id - album.provider = self.prov_id - album.name, album.version = parse_title_and_version(name) - album.artist = await self.get_artist(artistpath) - if not album.artist: - raise Exception("No album artist ! %s" % artistpath) - album.ids.append({"provider": self.prov_id, "item_id": prov_item_id}) - return album - - async def async_get_track(self, prov_item_id) -> Track: - """get full track details by id""" - if not os.sep in prov_item_id: - itempath = base64.b64decode(prov_item_id).decode("utf-8") - else: - itempath = prov_item_id - if not os.path.isfile(itempath): - LOGGER.error("track path does not exist: %s" % itempath) - return None - return await self.__parse_track(itempath) - - async def async_get_playlist(self, prov_item_id) -> Playlist: - """get full playlist details by id""" - if not os.sep in prov_item_id: - itempath = base64.b64decode(prov_item_id).decode("utf-8") - else: - itempath = prov_item_id - prov_item_id = base64.b64encode(itempath.encode("utf-8")).decode("utf-8") - if not os.path.isfile(itempath): - LOGGER.error("playlist path does not exist: %s" % itempath) - return None - playlist = Playlist() - playlist.item_id = prov_item_id - playlist.provider = self.prov_id - playlist.name = itempath.split(os.sep)[-1].replace(".m3u", "") - playlist.is_editable = True - playlist.ids.append({"provider": self.prov_id, "item_id": prov_item_id}) - playlist.owner = "disk" - playlist.checksum = os.path.getmtime(itempath) - return playlist - - async def async_get_album_tracks(self, prov_album_id) -> List[Track]: - """get album tracks for given album id""" - if not os.sep in prov_album_id: - albumpath = base64.b64decode(prov_album_id).decode("utf-8") - else: - albumpath = prov_album_id - if not os.path.isdir(albumpath): - LOGGER.error("album path does not exist: %s" % albumpath) - return - album = await self.get_album(albumpath) - for filename in os.listdir(albumpath): - filepath = os.path.join(albumpath, filename) - if os.path.isfile(filepath) and not filepath.startswith("."): - track = await self.__parse_track(filepath) - if track: - track.album = album - yield track - - async def async_get_playlist_tracks( - self, prov_playlist_id, limit=50, offset=0 - ) -> List[Track]: - """get playlist tracks for given playlist id""" - if not os.sep in prov_playlist_id: - itempath = base64.b64decode(prov_playlist_id).decode("utf-8") - else: - itempath = prov_playlist_id - if not os.path.isfile(itempath): - LOGGER.error("playlist path does not exist: %s" % itempath) - return - counter = 0 - index = 0 - with open(itempath) as f: - for line in f.readlines(): - line = line.strip() - if line and not line.startswith("#"): - counter += 1 - if counter > offset: - track = await self.__parse_track_from_uri(line) - if track: - yield track - index += 1 - if limit and index == limit: - break - - async def async_get_artist_albums(self, prov_artist_id) -> List[Album]: - """get a list of albums for the given artist""" - if not os.sep in prov_artist_id: - artistpath = base64.b64decode(prov_artist_id).decode("utf-8") - else: - artistpath = prov_artist_id - if not os.path.isdir(artistpath): - LOGGER.error("artist path does not exist: %s" % artistpath) - return - for dirname in os.listdir(artistpath): - dirpath = os.path.join(artistpath, dirname) - if os.path.isdir(dirpath) and not dirpath.startswith("."): - album = await self.get_album(dirpath) - if album: - yield album - - async def async_get_artist_toptracks(self, prov_artist_id) -> List[Track]: - """get a list of random tracks as we have no clue about preference""" - async for album in self.get_artist_albums(prov_artist_id): - async for track in self.get_album_tracks(album.item_id): - yield track - - async def async_get_stream_details(self, track_id): - """return the content details for the given track when it will be streamed""" - if not os.sep in track_id: - track_id = base64.b64decode(track_id).decode("utf-8") - if not os.path.isfile(track_id): - return None - # TODO: retrieve sanple rate and bitdepth - return { - "type": "file", - "path": track_id, - "content_type": track_id.split(".")[-1], - "sample_rate": 44100, - "bit_depth": 16, - } - - async def __async_parse_track(self, filename): - """try to parse a track from a filename with taglib""" - track = Track() - try: - song = taglib.File(filename) - except: - return None # not a media file ? - prov_item_id = base64.b64encode(filename.encode("utf-8")).decode("utf-8") - track.duration = song.length - track.item_id = prov_item_id - track.provider = self.prov_id - name = song.tags["TITLE"][0] - track.name, track.version = parse_title_and_version(name) - albumpath = filename.rsplit(os.sep, 1)[0] - track.album = await self.get_album(albumpath) - artists = [] - for artist_str in song.tags["ARTIST"]: - local_artist_path = os.path.join(self._music_dir, artist_str) - if os.path.isfile(local_artist_path): - artist = await self.get_artist(local_artist_path) - else: - artist = Artist() - artist.name = artist_str - fake_artistpath = os.path.join(self._music_dir, artist_str) - artist.item_id = fake_artistpath # temporary id - artist.ids.append( - { - "provider": self.prov_id, - "item_id": base64.b64encode( - fake_artistpath.encode("utf-8") - ).decode("utf-8"), - } - ) - artists.append(artist) - track.artists = artists - if "GENRE" in song.tags: - track.tags = song.tags["GENRE"] - if "ISRC" in song.tags: - track.external_ids["isrc"] = song.tags["ISRC"][0] - if "DISCNUMBER" in song.tags: - track.disc_number = int(song.tags["DISCNUMBER"][0]) - if "TRACKNUMBER" in song.tags: - track.track_number = int(song.tags["TRACKNUMBER"][0]) - quality_details = "" - if filename.endswith(".flac"): - # TODO: get bit depth - quality = TrackQuality.FLAC_LOSSLESS - if song.sampleRate > 192000: - quality = TrackQuality.FLAC_LOSSLESS_HI_RES_4 - elif song.sampleRate > 96000: - quality = TrackQuality.FLAC_LOSSLESS_HI_RES_3 - elif song.sampleRate > 48000: - quality = TrackQuality.FLAC_LOSSLESS_HI_RES_2 - quality_details = "%s Khz" % (song.sampleRate / 1000) - elif filename.endswith(".ogg"): - quality = TrackQuality.LOSSY_OGG - quality_details = "%s kbps" % (song.bitrate) - elif filename.endswith(".m4a"): - quality = TrackQuality.LOSSY_AAC - quality_details = "%s kbps" % (song.bitrate) - else: - quality = TrackQuality.LOSSY_MP3 - quality_details = "%s kbps" % (song.bitrate) - track.ids.append( - { - "provider": self.prov_id, - "item_id": prov_item_id, - "quality": quality, - "details": quality_details, - } - ) - return track - - async def __async_parse_track_from_uri(self, uri): - """try to parse a track from an uri found in playlist""" - if "://" in uri: - # track is uri from external provider? - prov_id = uri.split("://")[0] - prov_item_id = uri.split("/")[-1].split(".")[0].split(":")[-1] - try: - return await self.mass.music_manager.providers[prov_id].track( - prov_item_id, lazy=False - ) - except Exception as exc: - LOGGER.warning("Could not parse uri %s to track: %s" % (uri, str(exc))) - return None - # try to treat uri as filename - # TODO: filename could be related to musicdir or full path - track = await self.get_track(uri) - if track: - return track - track = await self.get_track(os.path.join(self._music_dir, uri)) - if track: - return track - return None diff --git a/music_assistant/providers/webplayer/__init__.py b/music_assistant/providers/webplayer/__init__.py new file mode 100644 index 00000000..112942a9 --- /dev/null +++ b/music_assistant/providers/webplayer/__init__.py @@ -0,0 +1,156 @@ +"""Webplayer support.""" +import logging +import time +from typing import List + +from music_assistant.models.config_entry import ConfigEntry +from music_assistant.models.player import Player, PlayerState +from music_assistant.models.playerprovider import PlayerProvider +from music_assistant.utils import run_periodic + +PROV_ID = "webplayer" +PROV_NAME = "WebPlayer" +LOGGER = logging.getLogger(PROV_ID) + +CONFIG_ENTRIES = [] + +EVENT_WEBPLAYER_CMD = "webplayer command" +EVENT_WEBPLAYER_STATE = "webplayer state" +EVENT_WEBPLAYER_REGISTER = "webplayer register" + + +async def async_setup(mass): + """Perform async setup of this Plugin/Provider.""" + prov = WebPlayerProvider() + await mass.async_register_provider(prov) + + +class WebPlayerProvider(PlayerProvider): + """ + Implementation of a player using pure HTML/javascript. + + Used in the front-end. + Communication is handled through the websocket connection + and our internal event bus. + """ + + _players = {} + + ### Provider specific implementation ##### + + @property + def id(self) -> str: + """Return provider ID for this provider.""" + return PROV_ID + + @property + def name(self) -> str: + """Return provider Name for this provider.""" + return PROV_NAME + + @property + def config_entries(self) -> List[ConfigEntry]: + """Return Config Entries for this provider.""" + return CONFIG_ENTRIES + + async def async_on_start(self) -> bool: + """Handle initialization of the provider based on config.""" + self.mass.add_event_listener( + self.async_handle_mass_event, + [EVENT_WEBPLAYER_STATE, EVENT_WEBPLAYER_REGISTER], + ) + self.mass.add_job(self.async_check_players()) + + async def async_handle_mass_event(self, msg, msg_details): + """Handle received event for the webplayer component.""" + if msg == EVENT_WEBPLAYER_REGISTER: + # register new player + player_id = msg_details["player_id"] + player = Player( + player_id=player_id, + provider_id=PROV_ID, + name=msg_details["name"], + powered=True, + ) + await self.mass.player_manager.async_add_player(player) + + elif msg == EVENT_WEBPLAYER_STATE: + await self.__handle_player_state(msg_details) + + @run_periodic(30) + async def async_check_players(self): + """Invalidate players that did not send a heartbeat message in a while.""" + cur_time = time.time() + offline_players = [] + for player in self._players.values(): + if cur_time - player._last_message > 30: + offline_players.append(player.player_id) + for player_id in offline_players: + await self.mass.player_manager.async_remove_player(player_id) + self._players.pop(player_id, None) + + async def async_cmd_stop(self, player_id: str): + """Send stop command to player.""" + data = {"player_id": player_id, "cmd": "stop"} + self.mass.signal_event(EVENT_WEBPLAYER_CMD, data) + + async def async_cmd_play(self, player_id: str): + """Send play command to player.""" + data = {"player_id": player_id, "cmd": "play"} + self.mass.signal_event(EVENT_WEBPLAYER_CMD, data) + + async def async_cmd_pause(self, player_id: str): + """Send pause command to player.""" + data = {"player_id": player_id, "cmd": "pause"} + self.mass.signal_event(EVENT_WEBPLAYER_CMD, data) + + async def async_cmd_power_on(self, player_id: str): + """Send power ON command to player.""" + self._players[player_id].powered = True # not supported on webplayer + data = {"player_id": player_id, "cmd": "stop"} + self.mass.signal_event(EVENT_WEBPLAYER_CMD, data) + + async def async_cmd_power_off(self, player_id: str): + """Send power OFF command to player.""" + await self.async_cmd_stop(player_id) + self._players[player_id].powered = False + + async def async_cmd_volume_set(self, volume_level, player_id: str): + """Send new volume level command to player.""" + data = { + "player_id": player_id, + "cmd": "volume_set", + "volume_level": volume_level, + } + self.mass.signal_event(EVENT_WEBPLAYER_CMD, data) + + async def async_cmd_volume_mute(self, player_id: str, is_muted=False): + """Send mute command to player.""" + data = {"player_id": player_id, "cmd": "volume_mute", "is_muted": is_muted} + self.mass.signal_event(EVENT_WEBPLAYER_CMD, data) + + async def async_cmd_play_uri(self, player_id: str, uri: str): + """Play single uri on player.""" + data = {"player_id": player_id, "cmd": "play_uri", "uri": uri} + self.mass.signal_event(EVENT_WEBPLAYER_CMD, data) + + async def __async_handle_player_state(self, data): + """Handle state event from player.""" + player_id = data["player_id"] + player = self._players[player_id] + if "volume_level" in data: + player.volume_level = data["volume_level"] + if "muted" in data: + player.muted = data["muted"] + if "state" in data: + player.state = PlayerState(data["state"]) + if "cur_time" in data: + player.elapsed_time = data["elapsed_time"] + if "current_uri" in data: + player.current_uri = data["current_uri"] + if "powered" in data: + player.powered = data["powered"] + if "name" in data: + player.name = data["name"] + player._last_message = time.time() + self.mass.add_job(self.mass.player_manager.async_update_player(player)) diff --git a/music_assistant/providers/webplayer/todo.py b/music_assistant/providers/webplayer/todo.py deleted file mode 100644 index fcacca28..00000000 --- a/music_assistant/providers/webplayer/todo.py +++ /dev/null @@ -1,153 +0,0 @@ -# pylint: skip-file -# flake8: noqa -import asyncio -import decimal -import os -import random -import socket -import struct -import sys -import time -from collections import OrderedDict -from typing import List - -from music_assistant.constants import CONF_ENABLED -from music_assistant.models.player import Player, PlayerState -from music_assistant.models.player_queue import QueueItem -from music_assistant.models.playerprovider import PlayerProvider -from music_assistant.utils import ( - LOGGER, - get_hostname, - get_ip, - run_periodic, - try_parse_int, -) - -PROV_ID = "webplayer" -PROV_NAME = "WebPlayer" -PROV_CLASS = "WebPlayerProvider" - -CONFIG_ENTRIES = [(CONF_ENABLED, True, CONF_ENABLED)] - -EVENT_WEBPLAYER_CMD = "webplayer command" -EVENT_WEBPLAYER_STATE = "webplayer state" -EVENT_WEBPLAYER_REGISTER = "webplayer register" - - -class WebPlayerProvider(PlayerProvider): - """ - Implementation of a player using pure HTML/javascript - used in the front-end. - Communication is handled through the websocket connection - and our internal event bus - """ - - ### Provider specific implementation ##### - - async def async_setup(self, conf): - """async initialize of module""" - await self.mass.add_event_listener( - self.handle_mass_event, EVENT_WEBPLAYER_STATE - ) - await self.mass.add_event_listener( - self.handle_mass_event, EVENT_WEBPLAYER_REGISTER - ) - self.mass.add_job(self.check_players()) - - async def async_handle_mass_event(self, msg, msg_details): - """received event for the webplayer component""" - if msg == EVENT_WEBPLAYER_REGISTER: - # register new player - player_id = msg_details["player_id"] - player = WebPlayer(self.mass, player_id, self.prov_id) - player.supports_crossfade = False - player.supports_gapless = False - player.supports_queue = False - player.name = msg_details["name"] - await self.add_player(player) - elif msg == EVENT_WEBPLAYER_STATE: - player_id = msg_details["player_id"] - player = await self.get_player(player_id) - if player: - await player.handle_state(msg_details) - - @run_periodic(30) - async def async_check_players(self): - """invalidate players that did not send a heartbeat message in a while""" - cur_time = time.time() - offline_players = [] - for player in self.players: - if cur_time - player._last_message > 30: - offline_players.append(player.player_id) - for player_id in offline_players: - await self.remove_player(player_id) - - -class WebPlayer(Player): - """Web player object""" - - def __init__(self, mass, player_id, prov_id): - self._last_message = time.time() - super().__init__(mass, player_id, prov_id) - - async def async_cmd_stop(self): - """Send stop command to player.""" - data = {"player_id": self.player_id, "cmd": "stop"} - self.mass.signal_event(EVENT_WEBPLAYER_CMD, data) - - async def async_cmd_play(self): - """Send play command to player.""" - data = {"player_id": self.player_id, "cmd": "play"} - self.mass.signal_event(EVENT_WEBPLAYER_CMD, data) - - async def async_cmd_pause(self): - """Send pause command to player.""" - data = {"player_id": self.player_id, "cmd": "pause"} - self.mass.signal_event(EVENT_WEBPLAYER_CMD, data) - - async def async_cmd_power_on(self): - """Send power ON command to player.""" - self.powered = True # not supported on webplayer - data = {"player_id": self.player_id, "cmd": "stop"} - self.mass.signal_event(EVENT_WEBPLAYER_CMD, data) - - async def async_cmd_power_off(self): - """Send power OFF command to player.""" - self.powered = False - - async def async_cmd_volume_set(self, volume_level): - """Send new volume level command to player.""" - data = { - "player_id": self.player_id, - "cmd": "volume_set", - "volume_level": volume_level, - } - self.mass.signal_event(EVENT_WEBPLAYER_CMD, data) - - async def async_cmd_volume_mute(self, is_muted=False): - """Send mute command to player.""" - data = {"player_id": self.player_id, "cmd": "volume_mute", "is_muted": is_muted} - self.mass.signal_event(EVENT_WEBPLAYER_CMD, data) - - async def async_cmd_play_uri(self, uri: str): - """Play single uri on player.""" - data = {"player_id": self.player_id, "cmd": "play_uri", "uri": uri} - self.mass.signal_event(EVENT_WEBPLAYER_CMD, data) - - async def async_handle_state(self, data): - """handle state event from player.""" - if "volume_level" in data: - self.volume_level = data["volume_level"] - if "muted" in data: - self.muted = data["muted"] - if "state" in data: - self.state = PlayerState(data["state"]) - if "cur_time" in data: - self.cur_time = data["cur_time"] - if "current_uri" in data: - self.current_uri = data["current_uri"] - if "powered" in data: - self.powered = data["powered"] - if "name" in data: - self.name = data["name"] - self._last_message = time.time() diff --git a/pylintrc b/pylintrc index f68343bf..c3e21eae 100644 --- a/pylintrc +++ b/pylintrc @@ -6,21 +6,47 @@ jobs=2 persistent=no [BASIC] -good-names=id,i,j,k,ex,Run,_,fp +good-names=id,i,j,k,ex,Run,_,fp,T,ev [MESSAGES CONTROL] # Reasons disabled: +# format - handled by black # locally-disabled - it spams too much +# duplicate-code - unavoidable +# cyclic-import - doesn't test if both import on load +# abstract-class-little-used - prevents from setting right foundation +# unused-argument - generic callbacks and setup methods create a lot of warnings # too-many-* - are not enforced for the sake of readability # too-few-* - same as too-many-* -# import-outside-toplevel - TODO +# abstract-method - with intro of async there are always methods missing +# inconsistent-return-statements - doesn't handle raise +# too-many-ancestors - it's too strict. +# wrong-import-order - isort guards this disable= - bad-continuation, - fixme, - import-outside-toplevel, + format, + abstract-class-little-used, + abstract-method, + cyclic-import, + duplicate-code, + inconsistent-return-statements, locally-disabled, + not-context-manager, too-few-public-methods, + too-many-ancestors, + too-many-arguments, + too-many-branches, + too-many-instance-attributes, + too-many-lines, + too-many-locals, too-many-public-methods, + too-many-return-statements, + too-many-statements, + too-many-boolean-expressions, + unused-argument, + wrong-import-order +# enable useless-suppression temporarily every now and then to clean them up +enable= + use-symbolic-message-instead [REPORTS] score=no diff --git a/tox.ini b/tox.ini index fcf6776f..60d1182c 100644 --- a/tox.ini +++ b/tox.ini @@ -12,17 +12,17 @@ basepython = python3 ignore_errors = True commands = black --check ./ - flake8 music_assistant test - pylint music_assistant test - pydocstyle music_assistant test + flake8 music_assistant tests + pylint music_assistant tests + pydocstyle music_assistant tests deps = -rrequirements_lint.txt -rrequirements_test.txt -# [testenv:mypy] -# basepython = python3 -# ignore_errors = True -# commands = -# mypy music_assistant -# deps = -# -rrequirements_lint.txt +[testenv:mypy] +basepython = python3 +ignore_errors = True +commands = + mypy music_assistant +deps = + -rrequirements_lint.txt \ No newline at end of file