fix webplayer anf file provider
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Fri, 11 Sep 2020 20:34:13 +0000 (22:34 +0200)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Fri, 11 Sep 2020 20:34:13 +0000 (22:34 +0200)
MANIFEST.in [new file with mode: 0644]
music_assistant.egg-info/SOURCES.txt
music_assistant/models/media_types.py
music_assistant/providers/file/__init__.py [new file with mode: 0644]
music_assistant/providers/file/file.py [deleted file]
music_assistant/providers/webplayer/__init__.py [new file with mode: 0644]
music_assistant/providers/webplayer/todo.py [deleted file]
pylintrc
tox.ini

diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644 (file)
index 0000000..fb7d282
--- /dev/null
@@ -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
index f8adc6fd26f251878f2167144936595367083de1..0913126308befc61d8fa0d9f3ddfd5cd8aa16e52 100644 (file)
@@ -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
index de0e8f794f6b5274dc41b272dd46b86590e256af..a05afb2bf40deb072d9be07902f95a7d757dd810 100755 (executable)
@@ -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 (file)
index 0000000..32279e5
--- /dev/null
@@ -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 <artist>/<album>/<track.ext>
+    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 (file)
index 7aa7f22..0000000
+++ /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 <artist>/<album>/<track.ext>
-        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 (file)
index 0000000..112942a
--- /dev/null
@@ -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 (file)
index fcacca2..0000000
+++ /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()
index f68343bf038a1869aaf61894d0702eb14cd3c47c..c3e21eaea54fa2ced4f843b5b7e58f3379beacf0 100644 (file)
--- 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 fcf6776f76ca7dec4ff55d4b6d29ca17e1f08cf6..60d1182c191e28aa351a49b350ccb3d732ce1b7a 100644 (file)
--- 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