Compare commits
70 commits
hhm0-to_br
...
master
Author | SHA1 | Date | |
---|---|---|---|
ebb2dab571 | |||
62429f22bd | |||
![]() |
4bc641e95a | ||
![]() |
5ce65f0a08 | ||
![]() |
b5e0de1e4d | ||
![]() |
e8493923c1 | ||
![]() |
32f74f28e9 | ||
![]() |
cf588481c6 | ||
![]() |
876bec6711 | ||
![]() |
7064dd9e50 | ||
![]() |
81a62cdca4 | ||
![]() |
1bc9e35d83 | ||
![]() |
6857653cdb | ||
![]() |
511a101c2c | ||
![]() |
f7090127fe | ||
![]() |
31023236ae | ||
![]() |
eff25672d9 | ||
![]() |
584209c134 | ||
![]() |
ee3e36408d | ||
![]() |
713845090c | ||
![]() |
89093e7064 | ||
![]() |
77f86bd2ed | ||
![]() |
bd2306a5fb | ||
![]() |
f421bd2a90 | ||
![]() |
128ba8173f | ||
![]() |
73b5f9bcc5 | ||
![]() |
a7fdc9f436 | ||
![]() |
cd22b5f694 | ||
![]() |
1b04266d92 | ||
![]() |
b067352a00 | ||
![]() |
008527f115 | ||
![]() |
35340c5c69 | ||
![]() |
e27563edaf | ||
![]() |
a2b22cb793 | ||
![]() |
521cdf8002 | ||
![]() |
8d7cd3ff01 | ||
![]() |
70b8245fd0 | ||
![]() |
f95af9c977 | ||
![]() |
3f8a3d709e | ||
![]() |
da58978427 | ||
![]() |
8f5d3e0216 | ||
![]() |
a7eef84b2a | ||
![]() |
ff66d4fa40 | ||
![]() |
f70ec43cfb | ||
![]() |
a7039ba53c | ||
![]() |
7592036c7f | ||
![]() |
364352a765 | ||
![]() |
a2f23c2095 | ||
![]() |
de69905c54 | ||
![]() |
6a87ebe8e2 | ||
![]() |
fb64508673 | ||
![]() |
f8e1311866 | ||
![]() |
8226a35bc2 | ||
![]() |
f777b01f72 | ||
![]() |
aec8b02484 | ||
![]() |
1f19ffcb75 | ||
![]() |
64938a8c7e | ||
![]() |
634efc4de4 | ||
![]() |
5bfe185ef0 | ||
![]() |
127cd030d8 | ||
![]() |
9ac788b69b | ||
![]() |
7f9c685d1d | ||
![]() |
fe444091a6 | ||
![]() |
ad1d862d21 | ||
![]() |
f43711a346 | ||
![]() |
9e8116713c | ||
![]() |
8ab48138c3 | ||
![]() |
76ae3d3ec5 | ||
![]() |
e004816794 | ||
![]() |
314002237d |
21 changed files with 1129 additions and 340 deletions
51
.circleci/config.yml
Normal file
51
.circleci/config.yml
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
version: 2.1
|
||||||
|
|
||||||
|
orbs:
|
||||||
|
codecov: codecov/codecov@1.0.5
|
||||||
|
|
||||||
|
workflows:
|
||||||
|
version: 2
|
||||||
|
test:
|
||||||
|
jobs:
|
||||||
|
- py38
|
||||||
|
- py37
|
||||||
|
- black
|
||||||
|
- check-manifest
|
||||||
|
- flake8
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
py38: &test-template
|
||||||
|
docker:
|
||||||
|
- image: mopidy/ci-python:3.8
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- restore_cache:
|
||||||
|
name: Restoring tox cache
|
||||||
|
key: tox-v1-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.cfg" }}
|
||||||
|
- run:
|
||||||
|
name: Run tests
|
||||||
|
command: |
|
||||||
|
tox -e $CIRCLE_JOB -- \
|
||||||
|
--junit-xml=test-results/pytest/results.xml \
|
||||||
|
--cov-report=xml
|
||||||
|
- save_cache:
|
||||||
|
name: Saving tox cache
|
||||||
|
key: tox-v1-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.cfg" }}
|
||||||
|
paths:
|
||||||
|
- ./.tox
|
||||||
|
- ~/.cache/pip
|
||||||
|
- codecov/upload:
|
||||||
|
file: coverage.xml
|
||||||
|
- store_test_results:
|
||||||
|
path: test-results
|
||||||
|
|
||||||
|
py37:
|
||||||
|
<<: *test-template
|
||||||
|
docker:
|
||||||
|
- image: mopidy/ci-python:3.7
|
||||||
|
|
||||||
|
black: *test-template
|
||||||
|
|
||||||
|
check-manifest: *test-template
|
||||||
|
|
||||||
|
flake8: *test-template
|
13
.gitignore
vendored
13
.gitignore
vendored
|
@ -1,6 +1,9 @@
|
||||||
build/
|
|
||||||
dist/
|
|
||||||
*.conf
|
|
||||||
venv/
|
|
||||||
*.egg-info
|
|
||||||
*.pyc
|
*.pyc
|
||||||
|
/.coverage
|
||||||
|
/.mypy_cache/
|
||||||
|
/.pytest_cache/
|
||||||
|
/.tox/
|
||||||
|
/*.egg-info
|
||||||
|
/build/
|
||||||
|
/dist/
|
||||||
|
/MANIFEST
|
||||||
|
|
111
CHANGELOG.rst
Normal file
111
CHANGELOG.rst
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
*********
|
||||||
|
Changelog
|
||||||
|
*********
|
||||||
|
|
||||||
|
|
||||||
|
v1.0.0 (2020-03-13)
|
||||||
|
===================
|
||||||
|
|
||||||
|
- Require Mopidy 3.0 or newer.
|
||||||
|
|
||||||
|
- Update extension to match the Mopidy extension cookiecutter.
|
||||||
|
|
||||||
|
|
||||||
|
v0.4.1 (2020-02-01)
|
||||||
|
===================
|
||||||
|
|
||||||
|
- Require Python 3.7 or newer.
|
||||||
|
|
||||||
|
- Require py-sonic 0.7.7 or newer.
|
||||||
|
|
||||||
|
|
||||||
|
v0.4.0 (2017-08-14)
|
||||||
|
===================
|
||||||
|
|
||||||
|
- Use Mopidy extension name as Subsonic API app name.
|
||||||
|
|
||||||
|
|
||||||
|
v0.3.4 (2017-06-12)
|
||||||
|
===================
|
||||||
|
|
||||||
|
- Playlist improvements.
|
||||||
|
|
||||||
|
|
||||||
|
v0.3.3 (2017-05-15)
|
||||||
|
===================
|
||||||
|
|
||||||
|
- Add API version setting.
|
||||||
|
|
||||||
|
|
||||||
|
v0.3.2 (2017-05-04)
|
||||||
|
===================
|
||||||
|
|
||||||
|
- Fix playlist track listing.
|
||||||
|
|
||||||
|
|
||||||
|
v0.3.1 (2017-03-23)
|
||||||
|
===================
|
||||||
|
|
||||||
|
- Fix URL encoding bug.
|
||||||
|
|
||||||
|
|
||||||
|
v0.3.0 (2017-03-22)
|
||||||
|
===================
|
||||||
|
|
||||||
|
- Add support for browsing.
|
||||||
|
|
||||||
|
|
||||||
|
v0.2.7 (2017-03-14)
|
||||||
|
===================
|
||||||
|
|
||||||
|
- Improved sorting of results.
|
||||||
|
|
||||||
|
|
||||||
|
v0.2.6 (2017-03-04)
|
||||||
|
===================
|
||||||
|
|
||||||
|
- Require py-sonic 0.6.1 to support legacy auth.
|
||||||
|
|
||||||
|
|
||||||
|
v0.2.5 (2017-02-27)
|
||||||
|
===================
|
||||||
|
|
||||||
|
- Fix legacy auth support.
|
||||||
|
|
||||||
|
|
||||||
|
v0.2.4 (2017-02-23)
|
||||||
|
===================
|
||||||
|
|
||||||
|
- Document current features/restrictions.
|
||||||
|
|
||||||
|
- Fix bug.
|
||||||
|
|
||||||
|
|
||||||
|
v0.2.3 (2016-11-03)
|
||||||
|
===================
|
||||||
|
|
||||||
|
- Add more debug logging.
|
||||||
|
|
||||||
|
|
||||||
|
v0.2.2 (2016-11-02)
|
||||||
|
===================
|
||||||
|
|
||||||
|
- Improved error handling.
|
||||||
|
|
||||||
|
|
||||||
|
v0.2.1 (2016-09-22)
|
||||||
|
===================
|
||||||
|
|
||||||
|
- Improved search.
|
||||||
|
|
||||||
|
|
||||||
|
v0.2.0 (2016-09-22)
|
||||||
|
===================
|
||||||
|
|
||||||
|
- Add basic naive search.
|
||||||
|
|
||||||
|
|
||||||
|
v0.1.1 (2016-09-20)
|
||||||
|
===================
|
||||||
|
|
||||||
|
- Initial release.
|
15
MANIFEST.in
15
MANIFEST.in
|
@ -1,4 +1,15 @@
|
||||||
|
include *.py
|
||||||
|
include *.rst
|
||||||
|
include .mailmap
|
||||||
include LICENSE
|
include LICENSE
|
||||||
include MANIFEST.in
|
include MANIFEST.in
|
||||||
include README.md
|
include pyproject.toml
|
||||||
include mopidy_subidy/ext.conf
|
include tox.ini
|
||||||
|
|
||||||
|
recursive-include .circleci *
|
||||||
|
recursive-include .github *
|
||||||
|
|
||||||
|
include mopidy_*/ext.conf
|
||||||
|
|
||||||
|
recursive-include tests *.py
|
||||||
|
recursive-include tests/data *
|
||||||
|
|
35
PKGBUILD
Normal file
35
PKGBUILD
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# Maintainer: Matthew Gamble <git@matthewgamble.net>
|
||||||
|
# Contributor: Frederick Gnodtke <fgnodtke at cronosx dot de>
|
||||||
|
|
||||||
|
pkgname=mopidy-subidy
|
||||||
|
pkgver=1.3.0
|
||||||
|
pkgrel=2
|
||||||
|
pkgdesc="Mopidy extension for playing music from Subsonic servers"
|
||||||
|
arch=("any")
|
||||||
|
url="https://git.hannover.ccc.de/lubiana/mopidy-subidy/releases"
|
||||||
|
license=('BSD')
|
||||||
|
depends=(
|
||||||
|
"mopidy"
|
||||||
|
"python"
|
||||||
|
"python-setuptools"
|
||||||
|
"python-pykka"
|
||||||
|
"python-pysonic"
|
||||||
|
)
|
||||||
|
source=("https://git.hannover.ccc.de/lubiana/mopidy-subidy/archive/1.0.0.tar.gz")
|
||||||
|
sha256sums=("ed78ce86da58fb42f6ddf9a8de72169d23521125b269b51054d69375b57c5b73")
|
||||||
|
|
||||||
|
build() {
|
||||||
|
cd "mopidy-subidy"
|
||||||
|
|
||||||
|
python setup.py build
|
||||||
|
}
|
||||||
|
|
||||||
|
package() {
|
||||||
|
cd "mopidy-subidy"
|
||||||
|
|
||||||
|
PYTHONHASHSEED=0 python setup.py install --root="${pkgdir}" --optimize=1 --skip-build
|
||||||
|
|
||||||
|
install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/mopidy-subidy/LICENSE"
|
||||||
|
install -Dm644 README.rst "${pkgdir}/usr/share/doc/mopidy-subidy/README.rst"
|
||||||
|
install -Dm644 CHANGELOG.rst "${pkgdir}/usr/share/doc/mopidy-subidy/CHANGELOG.rst"
|
||||||
|
}
|
39
README.md
39
README.md
|
@ -1,39 +0,0 @@
|
||||||
# Mopidy Subidy
|
|
||||||
|
|
||||||
A subsonic backend for mopidy using [py-sub](https://github.com/crustymonkey/py-sonic).
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Add a section similiar to the following to your mopidy configuration:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[subidy]
|
|
||||||
enabled=True
|
|
||||||
url=https://path.to/your/subsonic/server
|
|
||||||
username=subsonic_username
|
|
||||||
password=your_secret_password
|
|
||||||
legacy_auth=(optional; setting to yes may solve some connection errors)
|
|
||||||
```
|
|
||||||
|
|
||||||
## State of this plugin
|
|
||||||
|
|
||||||
Plugin is developed against mopidy version 2.0.1.
|
|
||||||
|
|
||||||
The following things are supported:
|
|
||||||
|
|
||||||
* Browsing all artists/albums/tracks
|
|
||||||
* Searching for any terms
|
|
||||||
* Browsing playlists
|
|
||||||
* Searching explicitly for one of: artists, albums, tracks
|
|
||||||
|
|
||||||
The following things are **not** supported:
|
|
||||||
|
|
||||||
* Creating, editing and deleting playlists
|
|
||||||
* Subsonics smart playlists
|
|
||||||
* Searching for a combination of filters (artist and album, artist and track, etc.)
|
|
||||||
|
|
||||||
## Contributors
|
|
||||||
|
|
||||||
The following people contributed to this project:
|
|
||||||
- Frederick Gnodtke
|
|
||||||
- hhm0
|
|
81
README.rst
Normal file
81
README.rst
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
*************
|
||||||
|
Mopidy-Subidy
|
||||||
|
*************
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/v/Mopidy-Subidy
|
||||||
|
:target: https://pypi.org/project/Mopidy-Subidy/
|
||||||
|
:alt: Latest PyPI version
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/circleci/build/gh/Prior99/mopidy-subidy
|
||||||
|
:target: https://circleci.com/gh/Prior99/mopidy-subidy
|
||||||
|
:alt: CircleCI build status
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/codecov/c/gh/Prior99/mopidy-subidy
|
||||||
|
:target: https://codecov.io/gh/Prior99/mopidy-subidy
|
||||||
|
:alt: Test coverage
|
||||||
|
|
||||||
|
**This library is actively looking for maintainers to help out as I do not have the time or need to maintain this anymore. Please contact me if you feel that you could maintain this.**
|
||||||
|
|
||||||
|
A Subsonic backend for Mopidy using `py-sonic
|
||||||
|
<https://github.com/crustymonkey/py-sonic>`_.
|
||||||
|
|
||||||
|
|
||||||
|
Installation
|
||||||
|
============
|
||||||
|
|
||||||
|
Install the latest release from PyPI by running::
|
||||||
|
|
||||||
|
python3 -m pip install Mopidy-Subidy
|
||||||
|
|
||||||
|
Install the development version directly from this repo by running::
|
||||||
|
|
||||||
|
python3 -m pip install https://github.com/Prior99/mopidy-subidy/archive/master.zip
|
||||||
|
|
||||||
|
See https://mopidy.com/ext/subidy/ for alternative installation methods.
|
||||||
|
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
=============
|
||||||
|
|
||||||
|
Before starting Mopidy, you must add configuration for Mopidy-Subidy to your
|
||||||
|
Mopidy configuration file::
|
||||||
|
|
||||||
|
[subidy]
|
||||||
|
url=https://path.to/your/subsonic/server
|
||||||
|
username=subsonic_username
|
||||||
|
password=your_secret_password
|
||||||
|
|
||||||
|
In addition, the following optional configuration values are supported:
|
||||||
|
|
||||||
|
- ``enabled`` -- Defaults to ``true``. Set to ``false`` to disable the
|
||||||
|
extension.
|
||||||
|
|
||||||
|
- ``legacy_auth`` -- Defaults to ``false``. Setting to ``true`` may solve some
|
||||||
|
connection errors.
|
||||||
|
|
||||||
|
- ``api_version`` -- Defaults to ``1.14.0``, which is the version used by
|
||||||
|
Subsonic 6.2.
|
||||||
|
|
||||||
|
|
||||||
|
State of this plugin
|
||||||
|
====================
|
||||||
|
|
||||||
|
The following things are supported:
|
||||||
|
|
||||||
|
- Browsing all artists/albums/tracks
|
||||||
|
- Searching for any terms
|
||||||
|
- Browsing, creating, editing and deleting playlists
|
||||||
|
- Searching explicitly for one of: artists, albums, tracks
|
||||||
|
|
||||||
|
The following things are **not** supported:
|
||||||
|
|
||||||
|
- Subsonic's smart playlists
|
||||||
|
- Searching for a combination of filters (artist and album, artist and track, etc.)
|
||||||
|
|
||||||
|
|
||||||
|
Credits
|
||||||
|
=======
|
||||||
|
|
||||||
|
- Original author: `Frederick Gnodtke <https://github.com/Prior99>`__
|
||||||
|
- Current maintainer: `Frederick Gnodtke <https://github.com/Prior99>`__
|
||||||
|
- `Contributors <https://github.com/Prior99/mopidy-subidy/graphs/contributors>`_
|
|
@ -1,30 +1,31 @@
|
||||||
from __future__ import unicode_literals
|
import pathlib
|
||||||
|
|
||||||
import os
|
import pkg_resources
|
||||||
|
|
||||||
from mopidy import ext, config
|
from mopidy import config, ext
|
||||||
|
|
||||||
__version__ = '0.2.1'
|
__version__ = pkg_resources.get_distribution("Mopidy-Subidy").version
|
||||||
|
|
||||||
|
|
||||||
class SubidyExtension(ext.Extension):
|
class SubidyExtension(ext.Extension):
|
||||||
|
|
||||||
dist_name = 'Mopidy-Subidy'
|
dist_name = "Mopidy-Subidy"
|
||||||
ext_name = 'subidy'
|
ext_name = "subidy"
|
||||||
version = __version__
|
version = __version__
|
||||||
|
|
||||||
def get_default_config(self):
|
def get_default_config(self):
|
||||||
conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf')
|
return config.read(pathlib.Path(__file__).parent / "ext.conf")
|
||||||
return config.read(conf_file)
|
|
||||||
|
|
||||||
def get_config_schema(self):
|
def get_config_schema(self):
|
||||||
schema = super(SubidyExtension, self).get_config_schema()
|
schema = super().get_config_schema()
|
||||||
schema['url'] = config.String()
|
schema["url"] = config.String()
|
||||||
schema['username'] = config.String()
|
schema["username"] = config.String()
|
||||||
schema['password'] = config.Secret()
|
schema["password"] = config.Secret()
|
||||||
schema['legacy_auth'] = config.Boolean(optional=True)
|
schema["legacy_auth"] = config.Boolean(optional=True)
|
||||||
|
schema["api_version"] = config.String(optional=True)
|
||||||
return schema
|
return schema
|
||||||
|
|
||||||
def setup(self, registry):
|
def setup(self, registry):
|
||||||
from .backend import SubidyBackend
|
from .backend import SubidyBackend
|
||||||
registry.add('backend', SubidyBackend)
|
|
||||||
|
registry.add("backend", SubidyBackend)
|
||||||
|
|
|
@ -1,17 +1,25 @@
|
||||||
from mopidy_subidy import library, playback, playlists, subsonic_api
|
|
||||||
from mopidy import backend
|
|
||||||
import pykka
|
import pykka
|
||||||
|
|
||||||
|
import mopidy_subidy
|
||||||
|
from mopidy import backend
|
||||||
|
from mopidy_subidy import library, playback, playlists, subsonic_api
|
||||||
|
|
||||||
|
|
||||||
class SubidyBackend(pykka.ThreadingActor, backend.Backend):
|
class SubidyBackend(pykka.ThreadingActor, backend.Backend):
|
||||||
def __init__(self, config, audio):
|
def __init__(self, config, audio):
|
||||||
super(SubidyBackend, self).__init__()
|
super().__init__()
|
||||||
subidy_config = config['subidy']
|
subidy_config = config["subidy"]
|
||||||
self.subsonic_api = subsonic_api.SubsonicApi(
|
self.subsonic_api = subsonic_api.SubsonicApi(
|
||||||
url=subidy_config['url'],
|
url=subidy_config["url"],
|
||||||
username=subidy_config['username'],
|
username=subidy_config["username"],
|
||||||
password=subidy_config['password'],
|
password=subidy_config["password"],
|
||||||
legacy_auth=subidy_config['legacy_auth'])
|
app_name=mopidy_subidy.SubidyExtension.dist_name,
|
||||||
|
legacy_auth=subidy_config["legacy_auth"],
|
||||||
|
api_version=subidy_config["api_version"],
|
||||||
|
)
|
||||||
self.library = library.SubidyLibraryProvider(backend=self)
|
self.library = library.SubidyLibraryProvider(backend=self)
|
||||||
self.playback = playback.SubidyPlaybackProvider(audio=audio, backend=self)
|
self.playback = playback.SubidyPlaybackProvider(
|
||||||
|
audio=audio, backend=self
|
||||||
|
)
|
||||||
self.playlists = playlists.SubidyPlaylistsProvider(backend=self)
|
self.playlists = playlists.SubidyPlaylistsProvider(backend=self)
|
||||||
self.uri_schemes = ['subidy']
|
self.uri_schemes = ["subidy"]
|
||||||
|
|
|
@ -4,3 +4,4 @@ url =
|
||||||
username =
|
username =
|
||||||
password =
|
password =
|
||||||
legacy_auth = no
|
legacy_auth = no
|
||||||
|
api_version = 1.14.0
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
from mopidy import backend, models
|
import logging
|
||||||
|
|
||||||
|
from mopidy import backend
|
||||||
from mopidy.models import Ref, SearchResult
|
from mopidy.models import Ref, SearchResult
|
||||||
from mopidy_subidy import uri
|
from mopidy_subidy import uri
|
||||||
|
|
||||||
import logging
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class SubidyLibraryProvider(backend.LibraryProvider):
|
class SubidyLibraryProvider(backend.LibraryProvider):
|
||||||
def __create_vdirs():
|
def __create_vdirs():
|
||||||
vdir_templates = [
|
vdir_templates = [
|
||||||
|
@ -12,6 +14,7 @@ class SubidyLibraryProvider(backend.LibraryProvider):
|
||||||
dict(id="artists", name="Artists"),
|
dict(id="artists", name="Artists"),
|
||||||
dict(id="albums", name="Albums"),
|
dict(id="albums", name="Albums"),
|
||||||
dict(id="rootdirs", name="Directories"),
|
dict(id="rootdirs", name="Directories"),
|
||||||
|
dict(id="random", name="Random"),
|
||||||
]
|
]
|
||||||
# Create a dict with the keys being the `id`s in `vdir_templates`
|
# Create a dict with the keys being the `id`s in `vdir_templates`
|
||||||
# and the values being objects containing the vdir `id`,
|
# and the values being objects containing the vdir `id`,
|
||||||
|
@ -20,7 +23,7 @@ class SubidyLibraryProvider(backend.LibraryProvider):
|
||||||
for template in vdir_templates:
|
for template in vdir_templates:
|
||||||
vdir = template.copy()
|
vdir = template.copy()
|
||||||
vdir.update(uri=uri.get_vdir_uri(vdir["id"]))
|
vdir.update(uri=uri.get_vdir_uri(vdir["id"]))
|
||||||
vdirs[template['id']] = vdir
|
vdirs[template["id"]] = vdir
|
||||||
return vdirs
|
return vdirs
|
||||||
|
|
||||||
_vdirs = __create_vdirs()
|
_vdirs = __create_vdirs()
|
||||||
|
@ -28,16 +31,14 @@ class SubidyLibraryProvider(backend.LibraryProvider):
|
||||||
def __raw_vdir_to_ref(vdir):
|
def __raw_vdir_to_ref(vdir):
|
||||||
if vdir is None:
|
if vdir is None:
|
||||||
return None
|
return None
|
||||||
return Ref.directory(
|
return Ref.directory(name=vdir["name"], uri=vdir["uri"])
|
||||||
name=vdir['name'],
|
|
||||||
uri=vdir['uri'])
|
|
||||||
|
|
||||||
root_directory = __raw_vdir_to_ref(_vdirs['root'])
|
root_directory = __raw_vdir_to_ref(_vdirs["root"])
|
||||||
|
|
||||||
_raw_vdir_to_ref = staticmethod(__raw_vdir_to_ref)
|
_raw_vdir_to_ref = staticmethod(__raw_vdir_to_ref)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(SubidyLibraryProvider, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.subsonic_api = self.backend.subsonic_api
|
self.subsonic_api = self.backend.subsonic_api
|
||||||
|
|
||||||
def browse_songs(self, album_id):
|
def browse_songs(self, album_id):
|
||||||
|
@ -52,23 +53,46 @@ class SubidyLibraryProvider(backend.LibraryProvider):
|
||||||
def browse_rootdirs(self):
|
def browse_rootdirs(self):
|
||||||
return self.subsonic_api.get_rootdirs_as_refs()
|
return self.subsonic_api.get_rootdirs_as_refs()
|
||||||
|
|
||||||
|
def browse_random_songs(self):
|
||||||
|
return self.subsonic_api.get_random_songs_as_refs()
|
||||||
|
|
||||||
def browse_diritems(self, directory_id):
|
def browse_diritems(self, directory_id):
|
||||||
return self.subsonic_api.get_diritems_as_refs(directory_id)
|
return self.subsonic_api.get_diritems_as_refs(directory_id)
|
||||||
|
|
||||||
def lookup_song(self, song_id):
|
def lookup_song(self, song_id):
|
||||||
return self.subsonic_api.get_song_by_id(song_id)
|
song = self.subsonic_api.get_song_by_id(song_id)
|
||||||
|
if song is None:
|
||||||
|
return []
|
||||||
|
else:
|
||||||
|
return [song]
|
||||||
|
|
||||||
def lookup_album(self, album_id):
|
def lookup_album(self, album_id):
|
||||||
return self.subsonic_api.get_album_by_id(album_id)
|
return self.subsonic_api.get_songs_as_tracks(album_id)
|
||||||
|
|
||||||
def lookup_artist(self, artist_id):
|
def lookup_artist(self, artist_id):
|
||||||
return self.subsonic_api.get_artist_by_id(artist_id)
|
return list(
|
||||||
|
self.subsonic_api.get_artist_as_songs_as_tracks_iter(artist_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
def lookup_directory(self, directory_id):
|
||||||
|
return list(
|
||||||
|
self.subsonic_api.get_recursive_dir_as_songs_as_tracks_iter(
|
||||||
|
directory_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def lookup_playlist(self, playlist_id):
|
||||||
|
return self.subsonic_api.get_playlist_as_playlist(playlist_id).tracks
|
||||||
|
|
||||||
def browse(self, browse_uri):
|
def browse(self, browse_uri):
|
||||||
if browse_uri == uri.get_vdir_uri('root'):
|
if browse_uri == uri.get_vdir_uri("root"):
|
||||||
root_vdir_names = ["rootdirs", "artists", "albums"]
|
root_vdir_names = ["rootdirs", "artists", "albums", "random"]
|
||||||
root_vdirs = [self._vdirs[vdir_name] for vdir_name in root_vdir_names]
|
root_vdirs = [
|
||||||
sorted_root_vdirs = sorted(root_vdirs, key=lambda vdir: vdir["name"])
|
self._vdirs[vdir_name] for vdir_name in root_vdir_names
|
||||||
|
]
|
||||||
|
sorted_root_vdirs = sorted(
|
||||||
|
root_vdirs, key=lambda vdir: vdir["name"]
|
||||||
|
)
|
||||||
return [self._raw_vdir_to_ref(vdir) for vdir in sorted_root_vdirs]
|
return [self._raw_vdir_to_ref(vdir) for vdir in sorted_root_vdirs]
|
||||||
elif browse_uri == uri.get_vdir_uri("rootdirs"):
|
elif browse_uri == uri.get_vdir_uri("rootdirs"):
|
||||||
return self.browse_rootdirs()
|
return self.browse_rootdirs()
|
||||||
|
@ -76,6 +100,9 @@ class SubidyLibraryProvider(backend.LibraryProvider):
|
||||||
return self.browse_artists()
|
return self.browse_artists()
|
||||||
elif browse_uri == uri.get_vdir_uri("albums"):
|
elif browse_uri == uri.get_vdir_uri("albums"):
|
||||||
return self.browse_albums()
|
return self.browse_albums()
|
||||||
|
elif browse_uri == uri.get_vdir_uri("random"):
|
||||||
|
return self.browse_random_songs()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
uri_type = uri.get_type(browse_uri)
|
uri_type = uri.get_type(browse_uri)
|
||||||
if uri_type == uri.DIRECTORY:
|
if uri_type == uri.DIRECTORY:
|
||||||
|
@ -93,68 +120,91 @@ class SubidyLibraryProvider(backend.LibraryProvider):
|
||||||
return self.lookup_artist(uri.get_artist_id(lookup_uri))
|
return self.lookup_artist(uri.get_artist_id(lookup_uri))
|
||||||
if type == uri.ALBUM:
|
if type == uri.ALBUM:
|
||||||
return self.lookup_album(uri.get_album_id(lookup_uri))
|
return self.lookup_album(uri.get_album_id(lookup_uri))
|
||||||
|
if type == uri.DIRECTORY:
|
||||||
|
return self.lookup_directory(uri.get_directory_id(lookup_uri))
|
||||||
if type == uri.SONG:
|
if type == uri.SONG:
|
||||||
return self.lookup_song(uri.get_song_id(lookup_uri))
|
return self.lookup_song(uri.get_song_id(lookup_uri))
|
||||||
|
if type == uri.PLAYLIST:
|
||||||
|
return self.lookup_playlist(uri.get_playlist_id(lookup_uri))
|
||||||
|
|
||||||
def lookup(self, uri=None, uris=None):
|
def lookup(self, uri=None, uris=None):
|
||||||
if uris is not None:
|
if uris is not None:
|
||||||
return [self.lookup_one(uri) for uri in uris]
|
return {uri: self.lookup_one(uri) for uri in uris}
|
||||||
if uri is not None:
|
if uri is not None:
|
||||||
return [self.lookup_one(uri)]
|
return self.lookup_one(uri)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def refresh(self, uri):
|
def refresh(self, uri):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def search_uri(self, query):
|
def search_by_artist_album_and_track(
|
||||||
type = uri.get_type(lookup_uri)
|
self, artist_name, album_name, track_name
|
||||||
if type == uri.ARTIST:
|
):
|
||||||
artist = self.lookup_artist(uri.get_artist_id(lookup_uri))
|
|
||||||
if artist is not None:
|
|
||||||
return SearchResult(artists=[artist])
|
|
||||||
elif type == uri.ALBUM:
|
|
||||||
album = self.lookup_album(uri.get_album_id(lookup_uri))
|
|
||||||
if album is not None:
|
|
||||||
return SearchResult(albums=[album])
|
|
||||||
elif type == uri.SONG:
|
|
||||||
song = self.lookup_song(uri.get_song_id(lookup_uri))
|
|
||||||
if song is not None:
|
|
||||||
return SearchResult(tracks=[song])
|
|
||||||
return None
|
|
||||||
|
|
||||||
def search_by_artist_album_and_track(self, artist_name, album_name, track_name):
|
|
||||||
tracks = self.search_by_artist_and_album(artist_name, album_name)
|
tracks = self.search_by_artist_and_album(artist_name, album_name)
|
||||||
track = next(item for item in tracks.tracks if track_name in item.name)
|
track = next(item for item in tracks.tracks if track_name in item.name)
|
||||||
return SearchResult(tracks=[track])
|
return SearchResult(tracks=[track])
|
||||||
|
|
||||||
def search_by_artist_and_album(self, artist_name, album_name):
|
def search_by_artist_and_album(self, artist_name, album_name):
|
||||||
artists = self.subsonic_api.get_raw_artists()
|
artists = self.subsonic_api.find_raw(artist_name).get("artist")
|
||||||
artist = next(item for item in artists if artist_name in item.get('name'))
|
if artists is None:
|
||||||
albums = self.subsonic_api.get_raw_albums(artist.get('id'))
|
return None
|
||||||
album = next(item for item in albums if album_name in item.get('title'))
|
tracks = []
|
||||||
return SearchResult(tracks=self.subsonic_api.get_songs_as_tracks(album.get('id')))
|
for artist in artists:
|
||||||
|
for album in self.subsonic_api.get_raw_albums(artist.get("id")):
|
||||||
|
if album_name in album.get("name"):
|
||||||
|
tracks.extend(
|
||||||
|
self.subsonic_api.get_songs_as_tracks(album.get("id"))
|
||||||
|
)
|
||||||
|
return SearchResult(tracks=tracks)
|
||||||
|
|
||||||
|
def search_by_artist(self, artist_name, exact):
|
||||||
|
result = self.subsonic_api.find_raw(artist_name)
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
tracks = []
|
||||||
|
for artist in result.get("artist"):
|
||||||
|
if exact:
|
||||||
|
if not artist.get("name") == artist_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
tracks.extend(
|
||||||
|
self.subsonic_api.get_artist_as_songs_as_tracks_iter(
|
||||||
|
artist.get("id")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return SearchResult(uri=uri.get_search_uri(artist_name), tracks=tracks)
|
||||||
|
|
||||||
def get_distinct(self, field, query):
|
def get_distinct(self, field, query):
|
||||||
search_result = self.search(query)
|
search_result = self.search(query)
|
||||||
if not search_result:
|
if not search_result:
|
||||||
return []
|
return []
|
||||||
if field == 'track' or field == 'title':
|
if field == "track" or field == "title":
|
||||||
return [track.name for track in (search_result.tracks or [])]
|
return [track.name for track in (search_result.tracks or [])]
|
||||||
if field == 'album':
|
if field == "album":
|
||||||
return [album.name for album in (search_result.albums or [])]
|
return [album.name for album in (search_result.albums or [])]
|
||||||
if field == 'artist':
|
if field == "artist":
|
||||||
if not search_result.artists:
|
if not search_result.artists:
|
||||||
return [artist.name for artist in self.browse_artists()]
|
return [artist.name for artist in self.browse_artists()]
|
||||||
return [artist.name for artist in search_result.artists]
|
return [artist.name for artist in search_result.artists]
|
||||||
|
|
||||||
def search(self, query=None, uris=None, exact=False):
|
def search(self, query=None, uris=None, exact=False):
|
||||||
if 'artist' in query and 'album' in query and 'track_name' in query:
|
if "artist" in query and "album" in query and "track_name" in query:
|
||||||
return self.search_by_artist_album_and_track(query.get('artist')[0], query.get('album')[0], query.get('track_name')[0])
|
return self.search_by_artist_album_and_track(
|
||||||
if 'artist' in query and 'album' in query:
|
query.get("artist")[0],
|
||||||
return self.search_by_artist_and_album(query.get('artist')[0], query.get('album')[0])
|
query.get("album")[0],
|
||||||
if 'artist' in query:
|
query.get("track_name")[0],
|
||||||
return self.subsonic_api.find_as_search_result(query.get('artist')[0])
|
)
|
||||||
if 'any' in query:
|
if "artist" in query and "album" in query:
|
||||||
return self.subsonic_api.find_as_search_result(query.get('any')[0])
|
return self.search_by_artist_and_album(
|
||||||
|
query.get("artist")[0], query.get("album")[0]
|
||||||
|
)
|
||||||
|
if "artist" in query:
|
||||||
|
return self.search_by_artist(query.get("artist")[0], exact)
|
||||||
|
if "comment" in query:
|
||||||
|
if query.get("comment")[0] == "random":
|
||||||
|
return SearchResult(
|
||||||
|
tracks=self.subsonic_api.get_random_songs_as_tracks()
|
||||||
|
)
|
||||||
|
if "any" in query:
|
||||||
|
return self.subsonic_api.find_as_search_result(query.get("any")[0])
|
||||||
return SearchResult(artists=self.subsonic_api.get_artists_as_artists())
|
return SearchResult(artists=self.subsonic_api.get_artists_as_artists())
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
from mopidy import backend
|
from mopidy import backend
|
||||||
from mopidy_subidy import uri
|
from mopidy_subidy import uri
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class SubidyPlaybackProvider(backend.PlaybackProvider):
|
class SubidyPlaybackProvider(backend.PlaybackProvider):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(SubidyPlaybackProvider, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.subsonic_api = self.backend.subsonic_api
|
self.subsonic_api = self.backend.subsonic_api
|
||||||
|
|
||||||
def translate_uri(self, translate_uri):
|
def translate_uri(self, translate_uri):
|
||||||
|
@ -14,3 +16,6 @@ class SubidyPlaybackProvider(backend.PlaybackProvider):
|
||||||
censored_url = self.subsonic_api.get_censored_song_stream_uri(song_id)
|
censored_url = self.subsonic_api.get_censored_song_stream_uri(song_id)
|
||||||
logger.debug("Loading song from subsonic with url: '%s'" % censored_url)
|
logger.debug("Loading song from subsonic with url: '%s'" % censored_url)
|
||||||
return self.subsonic_api.get_song_stream_uri(song_id)
|
return self.subsonic_api.get_song_stream_uri(song_id)
|
||||||
|
|
||||||
|
def should_download(self, uri):
|
||||||
|
return True
|
||||||
|
|
|
@ -1,35 +1,57 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
from mopidy import backend
|
from mopidy import backend
|
||||||
from mopidy_subidy import uri
|
from mopidy_subidy import uri
|
||||||
|
|
||||||
import logging
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class SubidyPlaylistsProvider(backend.PlaylistsProvider):
|
class SubidyPlaylistsProvider(backend.PlaylistsProvider):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(SubidyPlaylistsProvider, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.subsonic_api = self.backend.subsonic_api
|
self.subsonic_api = self.backend.subsonic_api
|
||||||
self.playlists = []
|
self.playlists = []
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
def as_list(self):
|
def as_list(self):
|
||||||
return self.playlists
|
return self.subsonic_api.get_playlists_as_refs()
|
||||||
|
|
||||||
def create(self, name):
|
def create(self, name):
|
||||||
pass
|
result = self.subsonic_api.create_playlist_raw(name)
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
playlist = result.get("playlist")
|
||||||
|
if playlist is None:
|
||||||
|
for pl in self.subsonic_api.get_playlists_as_playlists():
|
||||||
|
if pl.name == name:
|
||||||
|
playlist = pl
|
||||||
|
return playlist
|
||||||
|
else:
|
||||||
|
return self.subsonic_api.raw_playlist_to_playlist(playlist)
|
||||||
|
|
||||||
def delete(self, uri):
|
def delete(self, playlist_uri):
|
||||||
pass
|
playlist_id = uri.get_playlist_id(playlist_uri)
|
||||||
|
self.subsonic_api.delete_playlist_raw(playlist_id)
|
||||||
|
|
||||||
def get_items(self, items_uri):
|
def get_items(self, items_uri):
|
||||||
#logger.info('ITEMS %s: %s' % (lookup_uri, self.subsonic_api.get_playlist_songs_as_refs(uri.get_playlist_id(items_uri))))
|
return self.subsonic_api.get_playlist_as_songs_as_refs(
|
||||||
return self.subsonic_api.get_playlist_songs_as_refs(uri.get_playlist_id(items_uri))
|
uri.get_playlist_id(items_uri)
|
||||||
|
)
|
||||||
|
|
||||||
def lookup(self, lookup_uri):
|
def lookup(self, lookup_uri):
|
||||||
#logger.info('LOOKUP PLAYLIST %s: %s' % (lookup_uri, self.subsonic_api.get_playlist_as_playlist(uri.get_playlist_id(lookup_uri))))
|
return self.subsonic_api.get_playlist_as_playlist(
|
||||||
return self.subsonic_api.get_playlist_as_playlist(uri.get_playlist_id(lookup_uri))
|
uri.get_playlist_id(lookup_uri)
|
||||||
|
)
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
self.playlists = self.subsonic_api.get_playlists_as_refs()
|
pass
|
||||||
|
|
||||||
def save(self, playlist):
|
def save(self, playlist):
|
||||||
pass
|
playlist_id = uri.get_playlist_id(playlist.uri)
|
||||||
|
track_ids = []
|
||||||
|
for trk in playlist.tracks:
|
||||||
|
track_ids.append(uri.get_song_id(trk.uri))
|
||||||
|
result = self.subsonic_api.save_playlist_raw(playlist_id, track_ids)
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return playlist
|
||||||
|
|
|
@ -1,25 +1,28 @@
|
||||||
from urlparse import urlparse
|
|
||||||
import libsonic
|
|
||||||
import logging
|
import logging
|
||||||
import itertools
|
|
||||||
from mopidy.models import Track, Album, Artist, Playlist, Ref, SearchResult
|
|
||||||
import re
|
import re
|
||||||
|
from urllib.parse import urlencode, urlparse
|
||||||
|
|
||||||
|
import libsonic
|
||||||
|
from mopidy.models import Album, Artist, Playlist, Ref, SearchResult, Track
|
||||||
from mopidy_subidy import uri
|
from mopidy_subidy import uri
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
RESPONSE_OK = u'ok'
|
RESPONSE_OK = "ok"
|
||||||
UNKNOWN_SONG = u'Unknown Song'
|
UNKNOWN_SONG = "Unknown Song"
|
||||||
UNKNOWN_ALBUM = u'Unknown Album'
|
UNKNOWN_ALBUM = "Unknown Album"
|
||||||
UNKNOWN_ARTIST = u'Unknown Artist'
|
UNKNOWN_ARTIST = "Unknown Artist"
|
||||||
MAX_SEARCH_RESULTS = 100
|
MAX_SEARCH_RESULTS = 100
|
||||||
MAX_LIST_RESULTS = 500
|
MAX_LIST_RESULTS = 500
|
||||||
|
|
||||||
ref_sort_key = lambda ref: ref.name
|
|
||||||
|
def ref_sort_key(ref):
|
||||||
|
return ref.name
|
||||||
|
|
||||||
|
|
||||||
def string_nums_nocase_sort_key(s):
|
def string_nums_nocase_sort_key(s):
|
||||||
segments = []
|
segments = []
|
||||||
for substr in re.split(r'(\d+)', s):
|
for substr in re.split(r"(\d+)", s):
|
||||||
if substr.isdigit():
|
if substr.isdigit():
|
||||||
seg = int(substr)
|
seg = int(substr)
|
||||||
else:
|
else:
|
||||||
|
@ -27,248 +30,517 @@ def string_nums_nocase_sort_key(s):
|
||||||
segments.append(seg)
|
segments.append(seg)
|
||||||
return segments
|
return segments
|
||||||
|
|
||||||
|
|
||||||
def diritem_sort_key(item):
|
def diritem_sort_key(item):
|
||||||
isdir = item['isDir']
|
isdir = item["isDir"]
|
||||||
if isdir:
|
if isdir:
|
||||||
key = string_nums_nocase_sort_key(item['title'])
|
key = string_nums_nocase_sort_key(item["title"])
|
||||||
else:
|
else:
|
||||||
key = int(item.get('track', 1))
|
key = int(item.get("track", 1))
|
||||||
return (isdir, key)
|
return (isdir, key)
|
||||||
|
|
||||||
class SubsonicApi():
|
|
||||||
def __init__(self, url, username, password, legacy_auth):
|
class SubsonicApi:
|
||||||
|
def __init__(
|
||||||
|
self, url, username, password, app_name, legacy_auth, api_version
|
||||||
|
):
|
||||||
parsed = urlparse(url)
|
parsed = urlparse(url)
|
||||||
self.port = parsed.port if parsed.port else \
|
self.port = (
|
||||||
443 if parsed.scheme == 'https' else 80
|
parsed.port
|
||||||
base_url = parsed.scheme + '://' + parsed.hostname
|
if parsed.port
|
||||||
|
else 443
|
||||||
|
if parsed.scheme == "https"
|
||||||
|
else 80
|
||||||
|
)
|
||||||
|
base_url = parsed.scheme + "://" + parsed.hostname
|
||||||
self.connection = libsonic.Connection(
|
self.connection = libsonic.Connection(
|
||||||
base_url,
|
base_url,
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
self.port,
|
self.port,
|
||||||
parsed.path + '/rest',
|
parsed.path + "/rest",
|
||||||
legacyAuth=legacy_auth)
|
appName=app_name,
|
||||||
self.url = url + '/rest'
|
legacyAuth=legacy_auth,
|
||||||
|
apiVersion=api_version,
|
||||||
|
)
|
||||||
|
self.url = url + "/rest"
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
logger.info('Connecting to subsonic server on url %s as user %s' % (url, username))
|
logger.info(
|
||||||
|
f"Connecting to subsonic server on url {url} as user {username}, "
|
||||||
|
f"API version {api_version}"
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
self.connection.ping()
|
self.connection.ping()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error('Unabled to reach subsonic server: %s' % e)
|
logger.error("Unable to reach subsonic server: %s" % e)
|
||||||
exit()
|
exit()
|
||||||
|
|
||||||
|
def get_subsonic_uri(self, view_name, params, censor=False):
|
||||||
|
di_params = {}
|
||||||
|
di_params.update(params)
|
||||||
|
di_params.update(c=self.connection.appName)
|
||||||
|
di_params.update(v=self.connection.apiVersion)
|
||||||
|
if censor:
|
||||||
|
di_params.update(u="*****", p="*****")
|
||||||
|
else:
|
||||||
|
di_params.update(u=self.username, p=self.password)
|
||||||
|
return "{}/{}.view?{}".format(self.url, view_name, urlencode(di_params))
|
||||||
|
|
||||||
def get_song_stream_uri(self, song_id):
|
def get_song_stream_uri(self, song_id):
|
||||||
template = '%s/stream.view?id=%s&u=%s&p=%s&c=mopidy&v=1.14'
|
return self.get_subsonic_uri("stream", dict(id=song_id))
|
||||||
return template % (self.url, song_id, self.username, self.password)
|
|
||||||
|
|
||||||
def get_censored_song_stream_uri(self, song_id):
|
def get_censored_song_stream_uri(self, song_id):
|
||||||
template = '%s/stream.view?id=%s&u=******&p=******&c=mopidy&v=1.14'
|
return self.get_subsonic_uri("stream", dict(id=song_id), True)
|
||||||
return template % (self.url, song_id)
|
|
||||||
|
|
||||||
def find_raw(self, query, exclude_artists=False, exclude_albums=False, exclude_songs=False):
|
def find_raw(
|
||||||
|
self,
|
||||||
|
query,
|
||||||
|
exclude_artists=False,
|
||||||
|
exclude_albums=False,
|
||||||
|
exclude_songs=False,
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
response = self.connection.search2(
|
response = self.connection.search3(
|
||||||
query.encode('utf-8'),
|
query.encode("utf-8"),
|
||||||
MAX_SEARCH_RESULTS if not exclude_artists else 0, 0,
|
MAX_SEARCH_RESULTS if not exclude_artists else 0,
|
||||||
MAX_SEARCH_RESULTS if not exclude_albums else 0, 0,
|
0,
|
||||||
MAX_SEARCH_RESULTS if not exclude_songs else 0, 0)
|
MAX_SEARCH_RESULTS if not exclude_albums else 0,
|
||||||
except Exception as e:
|
0,
|
||||||
logger.warning('Connecting to subsonic failed when searching.')
|
MAX_SEARCH_RESULTS if not exclude_songs else 0,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.warning("Connecting to subsonic failed when searching.")
|
||||||
return None
|
return None
|
||||||
if response.get('status') != RESPONSE_OK:
|
if response.get("status") != RESPONSE_OK:
|
||||||
logger.warning('Got non-okay status code from subsonic: %s' % response.get('status'))
|
logger.warning(
|
||||||
|
"Got non-okay status code from subsonic: %s"
|
||||||
|
% response.get("status")
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
return response.get('searchResult2')
|
return response.get("searchResult3")
|
||||||
|
|
||||||
def find_as_search_result(self, query, exclude_artists=False, exclude_albums=False, exclude_songs=False):
|
def find_as_search_result(
|
||||||
|
self,
|
||||||
|
query,
|
||||||
|
exclude_artists=False,
|
||||||
|
exclude_albums=False,
|
||||||
|
exclude_songs=False,
|
||||||
|
):
|
||||||
result = self.find_raw(query)
|
result = self.find_raw(query)
|
||||||
if result is None:
|
if result is None:
|
||||||
return None
|
return None
|
||||||
return SearchResult(
|
return SearchResult(
|
||||||
uri=uri.get_search_uri(query),
|
uri=uri.get_search_uri(query),
|
||||||
artists=[self.raw_artist_to_artist(artist) for artist in result.get('artist') or []],
|
artists=[
|
||||||
albums=[self.raw_album_to_album(album) for album in result.get('album') or []],
|
self.raw_artist_to_artist(artist)
|
||||||
tracks=[self.raw_song_to_track(song) for song in result.get('song') or []])
|
for artist in result.get("artist") or []
|
||||||
|
],
|
||||||
|
albums=[
|
||||||
|
self.raw_album_to_album(album)
|
||||||
|
for album in result.get("album") or []
|
||||||
|
],
|
||||||
|
tracks=[
|
||||||
|
self.raw_song_to_track(song)
|
||||||
|
for song in result.get("song") or []
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_playlist_raw(self, name):
|
||||||
|
try:
|
||||||
|
response = self.connection.createPlaylist(name=name)
|
||||||
|
except Exception:
|
||||||
|
logger.warning(
|
||||||
|
"Connecting to subsonic failed when creating playlist."
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
if response.get("status") != RESPONSE_OK:
|
||||||
|
logger.warning(
|
||||||
|
"Got non-okay status code from subsonic: %s"
|
||||||
|
% response.get("status")
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
return response
|
||||||
|
|
||||||
|
def delete_playlist_raw(self, playlist_id):
|
||||||
|
try:
|
||||||
|
response = self.connection.deletePlaylist(playlist_id)
|
||||||
|
except Exception:
|
||||||
|
logger.warning(
|
||||||
|
"Connecting to subsonic failed when deleting playlist."
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
if response.get("status") != RESPONSE_OK:
|
||||||
|
logger.warning(
|
||||||
|
"Got non-okay status code from subsonic: %s"
|
||||||
|
% response.get("status")
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
return response
|
||||||
|
|
||||||
|
def save_playlist_raw(self, playlist_id, song_ids):
|
||||||
|
try:
|
||||||
|
response = self.connection.createPlaylist(
|
||||||
|
playlist_id, songIds=song_ids
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.warning(
|
||||||
|
"Connecting to subsonic failed when creating playlist."
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
if response.get("status") != RESPONSE_OK:
|
||||||
|
logger.warning(
|
||||||
|
"Got non-okay status code from subsonic: %s"
|
||||||
|
% response.get("status")
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
return response
|
||||||
|
|
||||||
def get_raw_artists(self):
|
def get_raw_artists(self):
|
||||||
try:
|
try:
|
||||||
response = self.connection.getArtists()
|
response = self.connection.getArtists()
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.warning('Connecting to subsonic failed when loading list of artists.')
|
logger.warning(
|
||||||
|
"Connecting to subsonic failed when loading list of artists."
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
if response.get('status') != RESPONSE_OK:
|
if response.get("status") != RESPONSE_OK:
|
||||||
logger.warning('Got non-okay status code from subsonic: %s' % response.get('status'))
|
logger.warning(
|
||||||
|
"Got non-okay status code from subsonic: %s"
|
||||||
|
% response.get("status")
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
letters = response.get('artists').get('index')
|
letters = response.get("artists").get("index")
|
||||||
if letters is not None:
|
if letters is not None:
|
||||||
artists = [artist for letter in letters for artist in letter.get('artist') or []]
|
artists = [
|
||||||
|
artist
|
||||||
|
for letter in letters
|
||||||
|
for artist in letter.get("artist") or []
|
||||||
|
]
|
||||||
return artists
|
return artists
|
||||||
logger.warning('Subsonic does not seem to have any artists in it\'s library.')
|
logger.warning(
|
||||||
|
"Subsonic does not seem to have any artists in it's library."
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_raw_rootdirs(self):
|
def get_raw_rootdirs(self):
|
||||||
try:
|
try:
|
||||||
response = self.connection.getIndexes()
|
response = self.connection.getIndexes()
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.warning('Connecting to subsonic failed when loading list of rootdirs.')
|
logger.warning(
|
||||||
|
"Connecting to subsonic failed when loading list of rootdirs."
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
if response.get('status') != RESPONSE_OK:
|
if response.get("status") != RESPONSE_OK:
|
||||||
logger.warning('Got non-okay status code from subsonic: %s' % response.get('status'))
|
logger.warning(
|
||||||
|
"Got non-okay status code from subsonic: %s"
|
||||||
|
% response.get("status")
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
letters = response.get('indexes').get('index')
|
letters = response.get("indexes").get("index")
|
||||||
if letters is not None:
|
if letters is not None:
|
||||||
artists = [artist for letter in letters for artist in letter.get('artist') or []]
|
artists = [
|
||||||
|
artist
|
||||||
|
for letter in letters
|
||||||
|
for artist in letter.get("artist") or []
|
||||||
|
]
|
||||||
return artists
|
return artists
|
||||||
logger.warning('Subsonic does not seem to have any rootdirs in its library.')
|
logger.warning(
|
||||||
|
"Subsonic does not seem to have any rootdirs in its library."
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_song_by_id(self, song_id):
|
def get_song_by_id(self, song_id):
|
||||||
try:
|
try:
|
||||||
response = self.connection.getSong(song_id)
|
response = self.connection.getSong(song_id)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.warning('Connecting to subsonic failed when loading song by id.')
|
logger.warning(
|
||||||
|
"Connecting to subsonic failed when loading song by id."
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
if response.get('status') != RESPONSE_OK:
|
if response.get("status") != RESPONSE_OK:
|
||||||
logger.warning('Got non-okay status code from subsonic: %s' % response.get('status'))
|
logger.warning(
|
||||||
|
"Got non-okay status code from subsonic: %s"
|
||||||
|
% response.get("status")
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
return self.raw_song_to_track(response.get('song')) if response.get('song') is not None else None
|
return (
|
||||||
|
self.raw_song_to_track(response.get("song"))
|
||||||
|
if response.get("song") is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
def get_album_by_id(self, album_id):
|
def get_album_by_id(self, album_id):
|
||||||
try:
|
try:
|
||||||
response = self.connection.getAlbum(album_id)
|
response = self.connection.getAlbum(album_id)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.warning('Connecting to subsonic failed when loading album by id.')
|
logger.warning(
|
||||||
|
"Connecting to subsonic failed when loading album by id."
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
if response.get('status') != RESPONSE_OK:
|
if response.get("status") != RESPONSE_OK:
|
||||||
logger.warning('Got non-okay status code from subsonic: %s' % response.get('status'))
|
logger.warning(
|
||||||
|
"Got non-okay status code from subsonic: %s"
|
||||||
|
% response.get("status")
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
return self.raw_album_to_album(response.get('album')) if response.get('album') is not None else None
|
return (
|
||||||
|
self.raw_album_to_album(response.get("album"))
|
||||||
|
if response.get("album") is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
def get_artist_by_id(self, artist_id):
|
def get_artist_by_id(self, artist_id):
|
||||||
try:
|
try:
|
||||||
response = self.connection.getArtist(artist_id)
|
response = self.connection.getArtist(artist_id)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.warning('Connecting to subsonic failed when loading artist by id.')
|
logger.warning(
|
||||||
|
"Connecting to subsonic failed when loading artist by id."
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
if response.get('status') != RESPONSE_OK:
|
if response.get("status") != RESPONSE_OK:
|
||||||
logger.warning('Got non-okay status code from subsonic: %s' % response.get('status'))
|
logger.warning(
|
||||||
|
"Got non-okay status code from subsonic: %s"
|
||||||
|
% response.get("status")
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
return self.raw_artist_to_artist(response.get('artist')) if response.get('artist') is not None else None
|
return (
|
||||||
|
self.raw_artist_to_artist(response.get("artist"))
|
||||||
|
if response.get("artist") is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
def get_raw_playlists(self):
|
def get_raw_playlists(self):
|
||||||
try:
|
try:
|
||||||
response = self.connection.getPlaylists()
|
response = self.connection.getPlaylists()
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.warning('Connecting to subsonic failed when loading list of playlists.')
|
logger.warning(
|
||||||
|
"Connecting to subsonic failed when loading list of playlists."
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
if response.get('status') != RESPONSE_OK:
|
if response.get("status") != RESPONSE_OK:
|
||||||
logger.warning('Got non-okay status code from subsonic: %s' % response.get('status'))
|
logger.warning(
|
||||||
|
"Got non-okay status code from subsonic: %s"
|
||||||
|
% response.get("status")
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
playlists = response.get('playlists').get('playlist')
|
playlists = response.get("playlists").get("playlist")
|
||||||
if playlists is None:
|
if playlists is None:
|
||||||
logger.warning('Subsonic does not seem to have any playlists in it\'s library.')
|
logger.warning(
|
||||||
|
"Subsonic does not seem to have any playlists in it's library."
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
return playlists
|
return playlists
|
||||||
|
|
||||||
def get_raw_playlist(self, playlist_id):
|
def get_raw_playlist(self, playlist_id):
|
||||||
try:
|
try:
|
||||||
response = self.connection.getPlaylist(playlist_id)
|
response = self.connection.getPlaylist(playlist_id)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.warning('Connecting to subsonic failed when loading playlist.')
|
logger.warning(
|
||||||
|
"Connecting to subsonic failed when loading playlist."
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
if response.get('status') != RESPONSE_OK:
|
if response.get("status") != RESPONSE_OK:
|
||||||
logger.warning('Got non-okay status code from subsonic: %s' % response.get('status'))
|
logger.warning(
|
||||||
|
"Got non-okay status code from subsonic: %s"
|
||||||
|
% response.get("status")
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
return response.get('playlist')
|
return response.get("playlist")
|
||||||
|
|
||||||
def get_raw_dir(self, parent_id):
|
def get_raw_dir(self, parent_id):
|
||||||
try:
|
try:
|
||||||
response = self.connection.getMusicDirectory(parent_id)
|
response = self.connection.getMusicDirectory(parent_id)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.warning('Connecting to subsonic failed when listing content of music directory.')
|
logger.warning(
|
||||||
|
"Connecting to subsonic failed when listing content of music directory."
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
if response.get('status') != RESPONSE_OK:
|
if response.get("status") != RESPONSE_OK:
|
||||||
logger.warning('Got non-okay status code from subsonic: %s' % response.get('status'))
|
logger.warning(
|
||||||
|
"Got non-okay status code from subsonic: %s"
|
||||||
|
% response.get("status")
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
directory = response.get('directory')
|
directory = response.get("directory")
|
||||||
if directory is not None:
|
if directory is not None:
|
||||||
diritems = directory.get('child')
|
diritems = directory.get("child")
|
||||||
return sorted(diritems, key=diritem_sort_key)
|
return sorted(diritems, key=diritem_sort_key)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_raw_albums(self, artist_id):
|
def get_raw_albums(self, artist_id):
|
||||||
try:
|
try:
|
||||||
response = self.connection.getArtist(artist_id)
|
response = self.connection.getArtist(artist_id)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.warning('Connecting to subsonic failed when loading list of albums.')
|
logger.warning(
|
||||||
|
"Connecting to subsonic failed when loading list of albums."
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
if response.get('status') != RESPONSE_OK:
|
if response.get("status") != RESPONSE_OK:
|
||||||
logger.warning('Got non-okay status code from subsonic: %s' % response.get('status'))
|
logger.warning(
|
||||||
|
"Got non-okay status code from subsonic: %s"
|
||||||
|
% response.get("status")
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
albums = response.get('artist').get('album')
|
albums = response.get("artist").get("album")
|
||||||
if albums is not None:
|
if albums is not None:
|
||||||
return sorted(albums, key=lambda album: string_nums_nocase_sort_key(album['name']))
|
return sorted(
|
||||||
|
albums,
|
||||||
|
key=lambda album: string_nums_nocase_sort_key(album["name"]),
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_raw_songs(self, album_id):
|
def get_raw_songs(self, album_id):
|
||||||
try:
|
try:
|
||||||
response = self.connection.getAlbum(album_id)
|
response = self.connection.getAlbum(album_id)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.warning('Connecting to subsonic failed when loading list of songs in album.')
|
logger.warning(
|
||||||
|
"Connecting to subsonic failed when loading list of songs in album."
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
if response.get('status') != RESPONSE_OK:
|
if response.get("status") != RESPONSE_OK:
|
||||||
logger.warning('Got non-okay status code from subsonic: %s' % response.get('status'))
|
logger.warning(
|
||||||
|
"Got non-okay status code from subsonic: %s"
|
||||||
|
% response.get("status")
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
songs = response.get('album').get('song')
|
songs = response.get("album").get("song")
|
||||||
if songs is not None:
|
if songs is not None:
|
||||||
return songs
|
return songs
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_raw_album_list(self, ltype, size=MAX_LIST_RESULTS):
|
def get_raw_random_song(self, size=MAX_LIST_RESULTS):
|
||||||
try:
|
try:
|
||||||
response = self.connection.getAlbumList2(ltype=ltype, size=size)
|
response = self.connection.getRandomSongs(size)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.warning('Connecting to subsonic failed when loading album list.')
|
logger.warning(
|
||||||
|
"Connecting to subsonic failed when loading ramdom song list."
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
if response.get('status') != RESPONSE_OK:
|
if response.get("status") != RESPONSE_OK:
|
||||||
logger.warning('Got non-okay status code from subsonic: %s' % response.get('status'))
|
logger.warning(
|
||||||
|
"Got non-okay status code from subsonic: %s"
|
||||||
|
% response.get("status")
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
albums = response.get('albumList2').get('album')
|
songs = response.get("randomSongs").get("song")
|
||||||
|
if songs is not None:
|
||||||
|
return songs
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_more_albums(self, ltype, size=MAX_LIST_RESULTS, offset=0):
|
||||||
|
try:
|
||||||
|
response = self.connection.getAlbumList2(
|
||||||
|
ltype=ltype, size=size, offset=offset
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.warning(
|
||||||
|
"Connecting to subsonic failed when loading album list."
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
if response.get("status") != RESPONSE_OK:
|
||||||
|
logger.warning(
|
||||||
|
"Got non-okay status code from subsonic: %s"
|
||||||
|
% response.get("status")
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
albums = response.get("albumList2").get("album")
|
||||||
if albums is not None:
|
if albums is not None:
|
||||||
return albums
|
return albums
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def get_raw_album_list(self, ltype, size=MAX_LIST_RESULTS):
|
||||||
|
"""
|
||||||
|
Subsonic servers don't offer any way to retrieve the total number
|
||||||
|
of albums to get, and the spec states that the max number returned
|
||||||
|
for `getAlbumList2` is 500. To get all the albums, we make a
|
||||||
|
`getAlbumList2` request each time the response contains 500 albums. If
|
||||||
|
it does not, we assume we have all the albums and return them.
|
||||||
|
"""
|
||||||
|
offset = 0
|
||||||
|
total = []
|
||||||
|
albums = self.get_more_albums(ltype, size, offset)
|
||||||
|
total = albums
|
||||||
|
while len(albums) == size:
|
||||||
|
offset = offset + size
|
||||||
|
albums = self.get_more_albums(ltype, size, offset)
|
||||||
|
total = total + albums
|
||||||
|
return total
|
||||||
|
|
||||||
def get_albums_as_refs(self, artist_id=None):
|
def get_albums_as_refs(self, artist_id=None):
|
||||||
albums = (self.get_raw_album_list('alphabeticalByName') if artist_id is None else self.get_raw_albums(artist_id))
|
albums = (
|
||||||
|
self.get_raw_album_list("alphabeticalByName")
|
||||||
|
if artist_id is None
|
||||||
|
else self.get_raw_albums(artist_id)
|
||||||
|
)
|
||||||
return [self.raw_album_to_ref(album) for album in albums]
|
return [self.raw_album_to_ref(album) for album in albums]
|
||||||
|
|
||||||
def get_albums_as_albums(self, artist_id):
|
def get_albums_as_albums(self, artist_id):
|
||||||
return [self.raw_album_to_album(album) for album in self.get_raw_albums(artist_id)]
|
return [
|
||||||
|
self.raw_album_to_album(album)
|
||||||
|
for album in self.get_raw_albums(artist_id)
|
||||||
|
]
|
||||||
|
|
||||||
def get_songs_as_refs(self, album_id):
|
def get_songs_as_refs(self, album_id):
|
||||||
return [self.raw_song_to_ref(song) for song in self.get_raw_songs(album_id)]
|
return [
|
||||||
|
self.raw_song_to_ref(song) for song in self.get_raw_songs(album_id)
|
||||||
|
]
|
||||||
|
|
||||||
def get_songs_as_tracks(self, album_id):
|
def get_songs_as_tracks(self, album_id):
|
||||||
return [self.raw_song_to_track(song) for song in self.get_raw_songs(album_id)]
|
return [
|
||||||
|
self.raw_song_to_track(song)
|
||||||
|
for song in self.get_raw_songs(album_id)
|
||||||
|
]
|
||||||
|
|
||||||
def get_artists_as_refs(self):
|
def get_artists_as_refs(self):
|
||||||
return [self.raw_artist_to_ref(artist) for artist in self.get_raw_artists()]
|
return [
|
||||||
|
self.raw_artist_to_ref(artist) for artist in self.get_raw_artists()
|
||||||
|
]
|
||||||
|
|
||||||
def get_rootdirs_as_refs(self):
|
def get_rootdirs_as_refs(self):
|
||||||
return [self.raw_directory_to_ref(rootdir) for rootdir in self.get_raw_rootdirs()]
|
return [
|
||||||
|
self.raw_directory_to_ref(rootdir)
|
||||||
|
for rootdir in self.get_raw_rootdirs()
|
||||||
|
]
|
||||||
|
|
||||||
def get_diritems_as_refs(self, directory_id):
|
def get_diritems_as_refs(self, directory_id):
|
||||||
return [(self.raw_directory_to_ref(diritem) if diritem.get('isDir') else self.raw_song_to_ref(diritem)) for diritem in self.get_raw_dir(directory_id)]
|
return [
|
||||||
|
(
|
||||||
|
self.raw_directory_to_ref(diritem)
|
||||||
|
if diritem.get("isDir")
|
||||||
|
else self.raw_song_to_ref(diritem)
|
||||||
|
)
|
||||||
|
for diritem in self.get_raw_dir(directory_id)
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_random_songs_as_refs(self):
|
||||||
|
return [
|
||||||
|
self.raw_song_to_ref(song) for song in self.get_raw_random_song(75)
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_random_songs_as_tracks(self):
|
||||||
|
return [
|
||||||
|
self.raw_song_to_track(song) for song in self.get_raw_random_song()
|
||||||
|
]
|
||||||
|
|
||||||
def get_artists_as_artists(self):
|
def get_artists_as_artists(self):
|
||||||
return [self.raw_artist_to_artist(artist) for artist in self.get_raw_artists()]
|
return [
|
||||||
|
self.raw_artist_to_artist(artist)
|
||||||
|
for artist in self.get_raw_artists()
|
||||||
|
]
|
||||||
|
|
||||||
def get_playlists_as_refs(self):
|
def get_playlists_as_refs(self):
|
||||||
return [self.raw_playlist_to_ref(playlist) for playlist in self.get_raw_playlists()]
|
return [
|
||||||
|
self.raw_playlist_to_ref(playlist)
|
||||||
|
for playlist in self.get_raw_playlists()
|
||||||
|
]
|
||||||
|
|
||||||
def get_playlists_as_playlists(self):
|
def get_playlists_as_playlists(self):
|
||||||
return [self.raw_playlist_to_playlist(playlist) for playlist in self.get_raw_playlists()]
|
return [
|
||||||
|
self.raw_playlist_to_playlist(playlist)
|
||||||
|
for playlist in self.get_raw_playlists()
|
||||||
|
]
|
||||||
|
|
||||||
def get_playlist_as_playlist(self, playlist_id):
|
def get_playlist_as_playlist(self, playlist_id):
|
||||||
return self.raw_playlist_to_playlist(self.get_raw_playlist(playlist_id))
|
return self.raw_playlist_to_playlist(self.get_raw_playlist(playlist_id))
|
||||||
|
@ -277,85 +549,130 @@ class SubsonicApi():
|
||||||
playlist = self.get_raw_playlist(playlist_id)
|
playlist = self.get_raw_playlist(playlist_id)
|
||||||
if playlist is None:
|
if playlist is None:
|
||||||
return None
|
return None
|
||||||
return [self.raw_song_to_ref(song) for song in playlist.get('entry')]
|
return [self.raw_song_to_ref(song) for song in playlist.get("entry")]
|
||||||
|
|
||||||
|
def get_artist_as_songs_as_tracks_iter(self, artist_id):
|
||||||
|
albums = self.get_raw_albums(artist_id)
|
||||||
|
if albums is None:
|
||||||
|
return
|
||||||
|
for album in albums:
|
||||||
|
for song in self.get_raw_songs(album.get("id")):
|
||||||
|
yield self.raw_song_to_track(song)
|
||||||
|
|
||||||
|
def get_recursive_dir_as_songs_as_tracks_iter(self, directory_id):
|
||||||
|
diritems = self.get_raw_dir(directory_id)
|
||||||
|
if diritems is None:
|
||||||
|
return
|
||||||
|
for item in diritems:
|
||||||
|
if item.get("isDir"):
|
||||||
|
yield from self.get_recursive_dir_as_songs_as_tracks_iter(
|
||||||
|
item.get("id")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
yield self.raw_song_to_track(item)
|
||||||
|
|
||||||
def raw_song_to_ref(self, song):
|
def raw_song_to_ref(self, song):
|
||||||
if song is None:
|
if song is None:
|
||||||
return None
|
return None
|
||||||
return Ref.track(
|
return Ref.track(
|
||||||
name=song.get('title') or UNKNOWN_SONG,
|
name=song.get("title") or UNKNOWN_SONG,
|
||||||
uri=uri.get_song_uri(song.get('id')))
|
uri=uri.get_song_uri(song.get("id")),
|
||||||
|
)
|
||||||
|
|
||||||
def raw_song_to_track(self, song):
|
def raw_song_to_track(self, song):
|
||||||
if song is None:
|
if song is None:
|
||||||
return None
|
return None
|
||||||
return Track(
|
return Track(
|
||||||
name=song.get('title') or UNKNOWN_SONG,
|
name=song.get("title") or UNKNOWN_SONG,
|
||||||
uri=uri.get_song_uri(song.get('id')),
|
uri=uri.get_song_uri(song.get("id")),
|
||||||
bitrate=song.get('bitRate'),
|
bitrate=song.get("bitRate"),
|
||||||
track_no=int(song.get('track')) if song.get('track') else None,
|
track_no=int(song.get("track")) if song.get("track") else None,
|
||||||
date=str(song.get('year')) or 'none',
|
date=str(song.get("year")) or "none",
|
||||||
genre=song.get('genre'),
|
genre=song.get("genre"),
|
||||||
length=int(song.get('duration')) * 1000 if song.get('duration') else None,
|
length=int(song.get("duration")) * 1000
|
||||||
disc_no=int(song.get('discNumber')) if song.get('discNumber') else None,
|
if song.get("duration")
|
||||||
artists=[Artist(
|
else None,
|
||||||
name=song.get('artist'),
|
disc_no=int(song.get("discNumber"))
|
||||||
uri=uri.get_artist_uri(song.get('artistId')))],
|
if song.get("discNumber")
|
||||||
|
else None,
|
||||||
|
artists=[
|
||||||
|
Artist(
|
||||||
|
name=song.get("artist"),
|
||||||
|
uri=uri.get_artist_uri(song.get("artistId")),
|
||||||
|
)
|
||||||
|
],
|
||||||
album=Album(
|
album=Album(
|
||||||
name=song.get('album'),
|
name=song.get("album"),
|
||||||
uri=uri.get_album_uri(song.get('albumId'))))
|
uri=uri.get_album_uri(song.get("albumId")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def raw_album_to_ref(self, album):
|
def raw_album_to_ref(self, album):
|
||||||
if album is None:
|
if album is None:
|
||||||
return None
|
return None
|
||||||
return Ref.album(
|
return Ref.album(
|
||||||
name=album.get('title') or album.get('name') or UNKNOWN_ALBUM,
|
name=album.get("title") or album.get("name") or UNKNOWN_ALBUM,
|
||||||
uri=uri.get_album_uri(album.get('id')))
|
uri=uri.get_album_uri(album.get("id")),
|
||||||
|
)
|
||||||
|
|
||||||
def raw_album_to_album(self, album):
|
def raw_album_to_album(self, album):
|
||||||
if album is None:
|
if album is None:
|
||||||
return None
|
return None
|
||||||
return Album(
|
return Album(
|
||||||
name=album.get('title') or album.get('name') or UNKNOWN_ALBUM,
|
name=album.get("title") or album.get("name") or UNKNOWN_ALBUM,
|
||||||
uri=uri.get_album_uri(album.get('id')),
|
num_tracks=album.get("songCount"),
|
||||||
artists=[Artist(
|
uri=uri.get_album_uri(album.get("id")),
|
||||||
name=album.get('artist'),
|
artists=[
|
||||||
uri=uri.get_artist_uri(album.get('artistId')))])
|
Artist(
|
||||||
|
name=album.get("artist"),
|
||||||
|
uri=uri.get_artist_uri(album.get("artistId")),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def raw_directory_to_ref(self, directory):
|
def raw_directory_to_ref(self, directory):
|
||||||
if directory is None:
|
if directory is None:
|
||||||
return None
|
return None
|
||||||
return Ref.directory(
|
return Ref.directory(
|
||||||
name=directory.get('title') or directory.get('name'),
|
name=directory.get("title") or directory.get("name"),
|
||||||
uri=uri.get_directory_uri(directory.get('id')))
|
uri=uri.get_directory_uri(directory.get("id")),
|
||||||
|
)
|
||||||
|
|
||||||
def raw_artist_to_ref(self, artist):
|
def raw_artist_to_ref(self, artist):
|
||||||
if artist is None:
|
if artist is None:
|
||||||
return None
|
return None
|
||||||
return Ref.artist(
|
return Ref.artist(
|
||||||
name=artist.get('name') or UNKNOWN_ARTIST,
|
name=artist.get("name") or UNKNOWN_ARTIST,
|
||||||
uri=uri.get_artist_uri(artist.get('id')))
|
uri=uri.get_artist_uri(artist.get("id")),
|
||||||
|
)
|
||||||
|
|
||||||
def raw_artist_to_artist(self, artist):
|
def raw_artist_to_artist(self, artist):
|
||||||
if artist is None:
|
if artist is None:
|
||||||
return None
|
return None
|
||||||
return Artist(
|
return Artist(
|
||||||
name=artist.get('name') or UNKNOWN_ARTIST,
|
name=artist.get("name") or UNKNOWN_ARTIST,
|
||||||
uri=uri.get_artist_uri(artist.get('id')))
|
uri=uri.get_artist_uri(artist.get("id")),
|
||||||
|
)
|
||||||
|
|
||||||
def raw_playlist_to_playlist(self, playlist):
|
def raw_playlist_to_playlist(self, playlist):
|
||||||
if playlist is None:
|
if playlist is None:
|
||||||
return None
|
return None
|
||||||
entries = playlist.get('entry')
|
entries = playlist.get("entry")
|
||||||
tracks = [self.raw_song_to_track(song) for song in entries] if entries is not None else None
|
tracks = (
|
||||||
|
[self.raw_song_to_track(song) for song in entries]
|
||||||
|
if entries is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
return Playlist(
|
return Playlist(
|
||||||
uri=uri.get_playlist_uri(playlist.get('id')),
|
uri=uri.get_playlist_uri(playlist.get("id")),
|
||||||
name=playlist.get('name'),
|
name=playlist.get("name"),
|
||||||
tracks=tracks)
|
tracks=tracks,
|
||||||
|
)
|
||||||
|
|
||||||
def raw_playlist_to_ref(self, playlist):
|
def raw_playlist_to_ref(self, playlist):
|
||||||
if playlist is None:
|
if playlist is None:
|
||||||
return None
|
return None
|
||||||
return Ref.playlist(
|
return Ref.playlist(
|
||||||
uri=uri.get_playlist_uri(playlist.get('id')),
|
uri=uri.get_playlist_uri(playlist.get("id")),
|
||||||
name=playlist.get('name'))
|
name=playlist.get("name"),
|
||||||
|
)
|
||||||
|
|
|
@ -1,87 +1,110 @@
|
||||||
import re
|
import re
|
||||||
|
|
||||||
SONG = 'song'
|
SONG = "song"
|
||||||
ARTIST = 'artist'
|
ARTIST = "artist"
|
||||||
PLAYLIST = 'playlist'
|
PLAYLIST = "playlist"
|
||||||
ALBUM = 'album'
|
ALBUM = "album"
|
||||||
DIRECTORY = 'directory'
|
DIRECTORY = "directory"
|
||||||
VDIR = 'vdir'
|
VDIR = "vdir"
|
||||||
PREFIX = 'subidy'
|
PREFIX = "subidy"
|
||||||
SEARCH = 'search'
|
SEARCH = "search"
|
||||||
|
RANDOM = "random"
|
||||||
|
|
||||||
|
regex = re.compile(r"(\w+?):(\w+?)(?::|$)(.+?)?$")
|
||||||
|
|
||||||
regex = re.compile(r'(\w+?):(\w+?)(?::|$)(.+?)?$')
|
|
||||||
|
|
||||||
def is_type_result_valid(result):
|
def is_type_result_valid(result):
|
||||||
return result is not None and result.group(1) == PREFIX
|
return result is not None and result.group(1) == PREFIX
|
||||||
|
|
||||||
|
|
||||||
def is_id_result_valid(result, type):
|
def is_id_result_valid(result, type):
|
||||||
return is_type_result_valid(result) and result.group(1) == PREFIX and result.group(2) == type
|
return (
|
||||||
|
is_type_result_valid(result)
|
||||||
|
and result.group(1) == PREFIX
|
||||||
|
and result.group(2) == type
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def is_uri(uri):
|
def is_uri(uri):
|
||||||
return regex.match(uri) is not None
|
return regex.match(uri) is not None
|
||||||
|
|
||||||
|
|
||||||
def get_song_id(uri):
|
def get_song_id(uri):
|
||||||
result = regex.match(uri)
|
result = regex.match(uri)
|
||||||
if not is_id_result_valid(result, SONG):
|
if not is_id_result_valid(result, SONG):
|
||||||
return None
|
return None
|
||||||
return result.group(3)
|
return result.group(3)
|
||||||
|
|
||||||
|
|
||||||
def get_artist_id(uri):
|
def get_artist_id(uri):
|
||||||
result = regex.match(uri)
|
result = regex.match(uri)
|
||||||
if not is_id_result_valid(result, ARTIST):
|
if not is_id_result_valid(result, ARTIST):
|
||||||
return None
|
return None
|
||||||
return result.group(3)
|
return result.group(3)
|
||||||
|
|
||||||
|
|
||||||
def get_playlist_id(uri):
|
def get_playlist_id(uri):
|
||||||
result = regex.match(uri)
|
result = regex.match(uri)
|
||||||
if not is_id_result_valid(result, PLAYLIST):
|
if not is_id_result_valid(result, PLAYLIST):
|
||||||
return None
|
return None
|
||||||
return result.group(3)
|
return result.group(3)
|
||||||
|
|
||||||
|
|
||||||
def get_album_id(uri):
|
def get_album_id(uri):
|
||||||
result = regex.match(uri)
|
result = regex.match(uri)
|
||||||
if not is_id_result_valid(result, ALBUM):
|
if not is_id_result_valid(result, ALBUM):
|
||||||
return None
|
return None
|
||||||
return result.group(3)
|
return result.group(3)
|
||||||
|
|
||||||
|
|
||||||
def get_directory_id(uri):
|
def get_directory_id(uri):
|
||||||
result = regex.match(uri)
|
result = regex.match(uri)
|
||||||
if not is_id_result_valid(result, DIRECTORY):
|
if not is_id_result_valid(result, DIRECTORY):
|
||||||
return None
|
return None
|
||||||
return result.group(3)
|
return result.group(3)
|
||||||
|
|
||||||
|
|
||||||
def get_vdir_id(uri):
|
def get_vdir_id(uri):
|
||||||
result = regex.match(uri)
|
result = regex.match(uri)
|
||||||
if not is_id_result_valid(result, VDIR):
|
if not is_id_result_valid(result, VDIR):
|
||||||
return None
|
return None
|
||||||
return result.group(3)
|
return result.group(3)
|
||||||
|
|
||||||
|
|
||||||
def get_type(uri):
|
def get_type(uri):
|
||||||
result = regex.match(uri)
|
result = regex.match(uri)
|
||||||
if not is_type_result_valid(result):
|
if not is_type_result_valid(result):
|
||||||
return None
|
return None
|
||||||
return result.group(2)
|
return result.group(2)
|
||||||
|
|
||||||
|
|
||||||
def get_type_uri(type, id):
|
def get_type_uri(type, id):
|
||||||
return u'%s:%s:%s' % (PREFIX, type, id)
|
return f"{PREFIX}:{type}:{id}"
|
||||||
|
|
||||||
|
|
||||||
def get_artist_uri(id):
|
def get_artist_uri(id):
|
||||||
return get_type_uri(ARTIST, id)
|
return get_type_uri(ARTIST, id)
|
||||||
|
|
||||||
|
|
||||||
def get_album_uri(id):
|
def get_album_uri(id):
|
||||||
return get_type_uri(ALBUM, id)
|
return get_type_uri(ALBUM, id)
|
||||||
|
|
||||||
|
|
||||||
def get_song_uri(id):
|
def get_song_uri(id):
|
||||||
return get_type_uri(SONG, id)
|
return get_type_uri(SONG, id)
|
||||||
|
|
||||||
|
|
||||||
def get_directory_uri(id):
|
def get_directory_uri(id):
|
||||||
return get_type_uri(DIRECTORY, id)
|
return get_type_uri(DIRECTORY, id)
|
||||||
|
|
||||||
|
|
||||||
def get_vdir_uri(id):
|
def get_vdir_uri(id):
|
||||||
return get_type_uri(VDIR, id)
|
return get_type_uri(VDIR, id)
|
||||||
|
|
||||||
|
|
||||||
def get_playlist_uri(id):
|
def get_playlist_uri(id):
|
||||||
return get_type_uri(PLAYLIST, id)
|
return get_type_uri(PLAYLIST, id)
|
||||||
|
|
||||||
|
|
||||||
def get_search_uri(query):
|
def get_search_uri(query):
|
||||||
return get_type_uri(SEARCH, query)
|
return get_type_uri(SEARCH, query)
|
||||||
|
|
17
pyproject.toml
Normal file
17
pyproject.toml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools >= 30.3.0", "wheel"]
|
||||||
|
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
target-version = ["py37", "py38"]
|
||||||
|
line-length = 80
|
||||||
|
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
multi_line_output = 3
|
||||||
|
include_trailing_comma = true
|
||||||
|
force_grid_wrap = 0
|
||||||
|
use_parentheses = true
|
||||||
|
line_length = 88
|
||||||
|
known_tests = "tests"
|
||||||
|
sections = "FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,TESTS,LOCALFOLDER"
|
86
setup.cfg
Normal file
86
setup.cfg
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
[metadata]
|
||||||
|
name = Mopidy-Subidy
|
||||||
|
version = 1.1.0
|
||||||
|
url = https://github.com/Prior99/mopidy-subidy
|
||||||
|
author = prior99
|
||||||
|
author_email = fgnodtke@cronosx.de
|
||||||
|
license = BSD-3-Clause
|
||||||
|
license_file = LICENSE
|
||||||
|
description = Subsonic extension for Mopidy
|
||||||
|
long_description = file: README.rst
|
||||||
|
classifiers =
|
||||||
|
Environment :: No Input/Output (Daemon)
|
||||||
|
Intended Audience :: End Users/Desktop
|
||||||
|
License :: OSI Approved :: BSD License
|
||||||
|
Operating System :: OS Independent
|
||||||
|
Programming Language :: Python :: 3
|
||||||
|
Programming Language :: Python :: 3.7
|
||||||
|
Programming Language :: Python :: 3.8
|
||||||
|
Topic :: Multimedia :: Sound/Audio :: Players
|
||||||
|
|
||||||
|
|
||||||
|
[options]
|
||||||
|
zip_safe = False
|
||||||
|
include_package_data = True
|
||||||
|
packages = find:
|
||||||
|
python_requires = >= 3.7
|
||||||
|
install_requires =
|
||||||
|
Mopidy >= 3.0.0
|
||||||
|
Pykka >= 2.0.1
|
||||||
|
setuptools
|
||||||
|
py-sonic >= 0.7.7
|
||||||
|
|
||||||
|
|
||||||
|
[options.extras_require]
|
||||||
|
lint =
|
||||||
|
black
|
||||||
|
check-manifest
|
||||||
|
flake8
|
||||||
|
flake8-bugbear
|
||||||
|
flake8-import-order
|
||||||
|
isort[pyproject]
|
||||||
|
release =
|
||||||
|
twine
|
||||||
|
wheel
|
||||||
|
test =
|
||||||
|
pytest
|
||||||
|
pytest-cov
|
||||||
|
dev =
|
||||||
|
%(lint)s
|
||||||
|
%(release)s
|
||||||
|
%(test)s
|
||||||
|
|
||||||
|
|
||||||
|
[options.packages.find]
|
||||||
|
exclude =
|
||||||
|
tests
|
||||||
|
tests.*
|
||||||
|
|
||||||
|
|
||||||
|
[options.entry_points]
|
||||||
|
mopidy.ext =
|
||||||
|
subidy = mopidy_subidy:SubidyExtension
|
||||||
|
|
||||||
|
|
||||||
|
[flake8]
|
||||||
|
application-import-names = mopidy_subidy, tests
|
||||||
|
max-line-length = 80
|
||||||
|
exclude = .git, .tox, build
|
||||||
|
select =
|
||||||
|
# Regular flake8 rules
|
||||||
|
C, E, F, W
|
||||||
|
# flake8-bugbear rules
|
||||||
|
B
|
||||||
|
# B950: line too long (soft speed limit)
|
||||||
|
B950
|
||||||
|
# pep8-naming rules
|
||||||
|
N
|
||||||
|
ignore =
|
||||||
|
# E203: whitespace before ':' (not PEP8 compliant)
|
||||||
|
E203
|
||||||
|
# E501: line too long (replaced by B950)
|
||||||
|
E501
|
||||||
|
# W503: line break before binary operator (not PEP8 compliant)
|
||||||
|
W503
|
||||||
|
# B305: .next() is not a thing on Python 3 (used by playback controller)
|
||||||
|
B305
|
44
setup.py
44
setup.py
|
@ -1,43 +1,3 @@
|
||||||
from __future__ import unicode_literals
|
from setuptools import setup
|
||||||
|
|
||||||
import re
|
setup()
|
||||||
from setuptools import setup, find_packages
|
|
||||||
|
|
||||||
|
|
||||||
def get_version(filename):
|
|
||||||
content = open(filename).read()
|
|
||||||
metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", content))
|
|
||||||
return metadata['version']
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name='Mopidy-Subidy',
|
|
||||||
version=get_version('mopidy_subidy/__init__.py'),
|
|
||||||
url='http://github.com/prior99/mopidy-subidy/',
|
|
||||||
license='BSD-3-Clause',
|
|
||||||
author='prior99',
|
|
||||||
author_email='fgnodtke@cronosx.de',
|
|
||||||
description='Improved Subsonic extension for Mopidy',
|
|
||||||
long_description=open('README.md').read(),
|
|
||||||
packages=find_packages(exclude=['tests', 'tests.*']),
|
|
||||||
zip_safe=False,
|
|
||||||
include_package_data=True,
|
|
||||||
install_requires=[
|
|
||||||
'setuptools',
|
|
||||||
'Mopidy >= 2.0',
|
|
||||||
'py-sonic >= 0.6.1',
|
|
||||||
'Pykka >= 1.1'
|
|
||||||
],
|
|
||||||
entry_points={
|
|
||||||
b'mopidy.ext': [
|
|
||||||
'subidy = mopidy_subidy:SubidyExtension',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
classifiers=[
|
|
||||||
'Environment :: No Input/Output (Daemon)',
|
|
||||||
'Intended Audience :: End Users/Desktop',
|
|
||||||
'License :: OSI Approved :: BSD 3-Clause',
|
|
||||||
'Operating System :: OS Independent',
|
|
||||||
'Programming Language :: Python :: 2',
|
|
||||||
'Topic :: Multimedia :: Sound/Audio :: Players'
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
23
tests/test_extension.py
Normal file
23
tests/test_extension.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
from mopidy_subidy import SubidyExtension
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_default_config():
|
||||||
|
ext = SubidyExtension()
|
||||||
|
|
||||||
|
config = ext.get_default_config()
|
||||||
|
|
||||||
|
assert "[subidy]" in config
|
||||||
|
assert "enabled = true" in config
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_config_schema():
|
||||||
|
ext = SubidyExtension()
|
||||||
|
|
||||||
|
schema = ext.get_config_schema()
|
||||||
|
|
||||||
|
# TODO Test the content of your config schema
|
||||||
|
assert "url" in schema
|
||||||
|
# assert "password" in schema
|
||||||
|
|
||||||
|
|
||||||
|
# TODO Write more tests
|
23
tox.ini
Normal file
23
tox.ini
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
[tox]
|
||||||
|
envlist = py37, py38, black, check-manifest, flake8
|
||||||
|
|
||||||
|
[testenv]
|
||||||
|
sitepackages = true
|
||||||
|
deps = .[test]
|
||||||
|
commands =
|
||||||
|
python -m pytest \
|
||||||
|
--basetemp={envtmpdir} \
|
||||||
|
--cov=mopidy_subidy --cov-report=term-missing \
|
||||||
|
{posargs}
|
||||||
|
|
||||||
|
[testenv:black]
|
||||||
|
deps = .[lint]
|
||||||
|
commands = python -m black --check .
|
||||||
|
|
||||||
|
[testenv:check-manifest]
|
||||||
|
deps = .[lint]
|
||||||
|
commands = python -m check_manifest
|
||||||
|
|
||||||
|
[testenv:flake8]
|
||||||
|
deps = .[lint]
|
||||||
|
commands = python -m flake8 --show-source --statistics
|
Loading…
Add table
Reference in a new issue