diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..bfd4689 --- /dev/null +++ b/.circleci/config.yml @@ -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 diff --git a/.gitignore b/.gitignore index 4cd6c95..03640c9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ -build/ -dist/ -*.conf -venv/ -*.egg-info *.pyc +/.coverage +/.mypy_cache/ +/.pytest_cache/ +/.tox/ +/*.egg-info +/build/ +/dist/ +/MANIFEST diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..a5a1c85 --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,111 @@ +********* +Changelog +********* + + +v1.0.0 (UNRELEASED) +=================== + +- 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. diff --git a/MANIFEST.in b/MANIFEST.in index 6def079..7734bb0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,15 @@ +include *.py +include *.rst +include .mailmap include LICENSE include MANIFEST.in -include README.md -include mopidy_subidy/ext.conf +include pyproject.toml +include tox.ini + +recursive-include .circleci * +recursive-include .github * + +include mopidy_*/ext.conf + +recursive-include tests *.py +recursive-include tests/data * diff --git a/README.md b/README.md deleted file mode 100644 index 0df614d..0000000 --- a/README.md +++ /dev/null @@ -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) -api_version=(optional - specify which API version to use. Subsonic 6.2 uses 1.14.0) -``` - -## 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, creating, editing and deleting playlists - * Searching explicitly for one of: artists, albums, tracks - -The following things are **not** supported: - - * 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 diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..bf10e40 --- /dev/null +++ b/README.rst @@ -0,0 +1,79 @@ +************* +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 + +A Subsonic backend for Mopidy using `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 `__ +- Current maintainer: `Frederick Gnodtke `__ +- `Contributors `_ diff --git a/mopidy_subidy/__init__.py b/mopidy_subidy/__init__.py index c26531b..d9f260a 100644 --- a/mopidy_subidy/__init__.py +++ b/mopidy_subidy/__init__.py @@ -1,31 +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): - dist_name = 'Mopidy-Subidy' - ext_name = 'subidy' + dist_name = "Mopidy-Subidy" + ext_name = "subidy" version = __version__ def get_default_config(self): - conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf') - return config.read(conf_file) + return config.read(pathlib.Path(__file__).parent / "ext.conf") def get_config_schema(self): - schema = super(SubidyExtension, self).get_config_schema() - schema['url'] = config.String() - schema['username'] = config.String() - schema['password'] = config.Secret() - schema['legacy_auth'] = config.Boolean(optional=True) - schema['api_version'] = config.String(optional=True) + schema = super().get_config_schema() + schema["url"] = config.String() + schema["username"] = config.String() + schema["password"] = config.Secret() + schema["legacy_auth"] = config.Boolean(optional=True) + schema["api_version"] = config.String(optional=True) return schema def setup(self, registry): from .backend import SubidyBackend - registry.add('backend', SubidyBackend) + + registry.add("backend", SubidyBackend) diff --git a/mopidy_subidy/backend.py b/mopidy_subidy/backend.py index 8c77c30..4aef291 100644 --- a/mopidy_subidy/backend.py +++ b/mopidy_subidy/backend.py @@ -1,20 +1,25 @@ -import mopidy_subidy -from mopidy_subidy import library, playback, playlists, subsonic_api -from mopidy import backend import pykka +import mopidy_subidy +from mopidy import backend +from mopidy_subidy import library, playback, playlists, subsonic_api + + class SubidyBackend(pykka.ThreadingActor, backend.Backend): def __init__(self, config, audio): - super(SubidyBackend, self).__init__() - subidy_config = config['subidy'] + super().__init__() + subidy_config = config["subidy"] self.subsonic_api = subsonic_api.SubsonicApi( - url=subidy_config['url'], - username=subidy_config['username'], - password=subidy_config['password'], + url=subidy_config["url"], + username=subidy_config["username"], + password=subidy_config["password"], app_name=mopidy_subidy.SubidyExtension.dist_name, - legacy_auth=subidy_config['legacy_auth'], - api_version=subidy_config['api_version']) + legacy_auth=subidy_config["legacy_auth"], + api_version=subidy_config["api_version"], + ) 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.uri_schemes = ['subidy'] + self.uri_schemes = ["subidy"] diff --git a/mopidy_subidy/library.py b/mopidy_subidy/library.py index 2695534..6da42e0 100644 --- a/mopidy_subidy/library.py +++ b/mopidy_subidy/library.py @@ -1,10 +1,12 @@ -from mopidy import backend, models +import logging + +from mopidy import backend from mopidy.models import Ref, SearchResult from mopidy_subidy import uri -import logging logger = logging.getLogger(__name__) + class SubidyLibraryProvider(backend.LibraryProvider): def __create_vdirs(): vdir_templates = [ @@ -20,7 +22,7 @@ class SubidyLibraryProvider(backend.LibraryProvider): for template in vdir_templates: vdir = template.copy() vdir.update(uri=uri.get_vdir_uri(vdir["id"])) - vdirs[template['id']] = vdir + vdirs[template["id"]] = vdir return vdirs _vdirs = __create_vdirs() @@ -28,16 +30,14 @@ class SubidyLibraryProvider(backend.LibraryProvider): def __raw_vdir_to_ref(vdir): if vdir is None: return None - return Ref.directory( - name=vdir['name'], - uri=vdir['uri']) + return Ref.directory(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) def __init__(self, *args, **kwargs): - super(SubidyLibraryProvider, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.subsonic_api = self.backend.subsonic_api def browse_songs(self, album_id): @@ -66,19 +66,29 @@ class SubidyLibraryProvider(backend.LibraryProvider): return self.subsonic_api.get_songs_as_tracks(album_id) def lookup_artist(self, artist_id): - return list(self.subsonic_api.get_artist_as_songs_as_tracks_iter(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)) + 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): - if browse_uri == uri.get_vdir_uri('root'): + if browse_uri == uri.get_vdir_uri("root"): root_vdir_names = ["rootdirs", "artists", "albums"] - root_vdirs = [self._vdirs[vdir_name] for vdir_name in root_vdir_names] - sorted_root_vdirs = sorted(root_vdirs, key=lambda vdir: vdir["name"]) + root_vdirs = [ + 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] elif browse_uri == uri.get_vdir_uri("rootdirs"): return self.browse_rootdirs() @@ -112,7 +122,7 @@ class SubidyLibraryProvider(backend.LibraryProvider): def lookup(self, uri=None, uris=None): if uris is not None: - return dict((uri, self.lookup_one(uri)) for uri in uris) + return {uri: self.lookup_one(uri) for uri in uris} if uri is not None: return self.lookup_one(uri) return None @@ -120,55 +130,52 @@ class SubidyLibraryProvider(backend.LibraryProvider): def refresh(self, uri): pass - def search_uri(self, query): - type = uri.get_type(lookup_uri) - 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): + 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) track = next(item for item in tracks.tracks if track_name in item.name) return SearchResult(tracks=[track]) def search_by_artist_and_album(self, artist_name, album_name): artists = self.subsonic_api.get_raw_artists() - artist = next(item for item in artists if artist_name in item.get('name')) - albums = self.subsonic_api.get_raw_albums(artist.get('id')) - album = next(item for item in albums if album_name in item.get('title')) - return SearchResult(tracks=self.subsonic_api.get_songs_as_tracks(album.get('id'))) + artist = next( + item for item in artists if artist_name in item.get("name") + ) + albums = self.subsonic_api.get_raw_albums(artist.get("id")) + album = next(item for item in albums if album_name in item.get("title")) + return SearchResult( + tracks=self.subsonic_api.get_songs_as_tracks(album.get("id")) + ) def get_distinct(self, field, query): search_result = self.search(query) if not search_result: return [] - if field == 'track' or field == 'title': + if field == "track" or field == "title": 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 [])] - if field == 'artist': + if field == "artist": if not search_result.artists: return [artist.name for artist in self.browse_artists()] return [artist.name for artist in search_result.artists] def search(self, query=None, uris=None, exact=False): - 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]) - if 'artist' in query and 'album' in query: - return self.search_by_artist_and_album(query.get('artist')[0], query.get('album')[0]) - if 'artist' in query: - return self.subsonic_api.find_as_search_result(query.get('artist')[0]) - if 'any' in query: - return self.subsonic_api.find_as_search_result(query.get('any')[0]) + 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], + ) + if "artist" in query and "album" in query: + return self.search_by_artist_and_album( + query.get("artist")[0], query.get("album")[0] + ) + if "artist" in query: + return self.subsonic_api.find_as_search_result( + query.get("artist")[0] + ) + 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()) - diff --git a/mopidy_subidy/playback.py b/mopidy_subidy/playback.py index 81c0710..83c0a7f 100644 --- a/mopidy_subidy/playback.py +++ b/mopidy_subidy/playback.py @@ -1,12 +1,14 @@ +import logging + from mopidy import backend from mopidy_subidy import uri -import logging logger = logging.getLogger(__name__) + class SubidyPlaybackProvider(backend.PlaybackProvider): def __init__(self, *args, **kwargs): - super(SubidyPlaybackProvider, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.subsonic_api = self.backend.subsonic_api def translate_uri(self, translate_uri): diff --git a/mopidy_subidy/playlists.py b/mopidy_subidy/playlists.py index 0bb0321..c048643 100644 --- a/mopidy_subidy/playlists.py +++ b/mopidy_subidy/playlists.py @@ -1,13 +1,14 @@ +import logging + from mopidy import backend from mopidy_subidy import uri -from mopidy.models import Playlist -import logging logger = logging.getLogger(__name__) + class SubidyPlaylistsProvider(backend.PlaylistsProvider): def __init__(self, *args, **kwargs): - super(SubidyPlaylistsProvider, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.subsonic_api = self.backend.subsonic_api self.playlists = [] self.refresh() @@ -19,7 +20,7 @@ class SubidyPlaylistsProvider(backend.PlaylistsProvider): result = self.subsonic_api.create_playlist_raw(name) if result is None: return None - playlist = result.get('playlist') + playlist = result.get("playlist") if playlist is None: for pl in self.subsonic_api.get_playlists_as_playlists(): if pl.name == name: @@ -33,12 +34,14 @@ class SubidyPlaylistsProvider(backend.PlaylistsProvider): self.subsonic_api.delete_playlist_raw(playlist_id) 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(uri.get_playlist_id(items_uri)) + return self.subsonic_api.get_playlist_as_songs_as_refs( + uri.get_playlist_id(items_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(uri.get_playlist_id(lookup_uri)) + return self.subsonic_api.get_playlist_as_playlist( + uri.get_playlist_id(lookup_uri) + ) def refresh(self): pass diff --git a/mopidy_subidy/subsonic_api.py b/mopidy_subidy/subsonic_api.py index 168b072..e8b4ad2 100644 --- a/mopidy_subidy/subsonic_api.py +++ b/mopidy_subidy/subsonic_api.py @@ -1,26 +1,28 @@ -from urllib.parse import urlparse -from urllib.parse import urlencode -import libsonic import logging -import itertools -from mopidy.models import Track, Album, Artist, Playlist, Ref, SearchResult 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 logger = logging.getLogger(__name__) -RESPONSE_OK = u'ok' -UNKNOWN_SONG = u'Unknown Song' -UNKNOWN_ALBUM = u'Unknown Album' -UNKNOWN_ARTIST = u'Unknown Artist' +RESPONSE_OK = "ok" +UNKNOWN_SONG = "Unknown Song" +UNKNOWN_ALBUM = "Unknown Album" +UNKNOWN_ARTIST = "Unknown Artist" MAX_SEARCH_RESULTS = 100 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): segments = [] - for substr in re.split(r'(\d+)', s): + for substr in re.split(r"(\d+)", s): if substr.isdigit(): seg = int(substr) else: @@ -28,37 +30,50 @@ def string_nums_nocase_sort_key(s): segments.append(seg) return segments + def diritem_sort_key(item): - isdir = item['isDir'] + isdir = item["isDir"] if isdir: - key = string_nums_nocase_sort_key(item['title']) + key = string_nums_nocase_sort_key(item["title"]) else: - key = int(item.get('track', 1)) + key = int(item.get("track", 1)) return (isdir, key) -class SubsonicApi(): - def __init__(self, url, username, password, app_name, legacy_auth, api_version): + +class SubsonicApi: + def __init__( + self, url, username, password, app_name, legacy_auth, api_version + ): parsed = urlparse(url) - self.port = parsed.port if parsed.port else \ - 443 if parsed.scheme == 'https' else 80 - base_url = parsed.scheme + '://' + parsed.hostname + self.port = ( + parsed.port + if parsed.port + else 443 + if parsed.scheme == "https" + else 80 + ) + base_url = parsed.scheme + "://" + parsed.hostname self.connection = libsonic.Connection( base_url, username, password, self.port, - parsed.path + '/rest', + parsed.path + "/rest", appName=app_name, legacyAuth=legacy_auth, - apiVersion=api_version) - self.url = url + '/rest' + apiVersion=api_version, + ) + self.url = url + "/rest" self.username = username self.password = password - logger.info('Connecting to subsonic server on url %s as user %s, API version %s' % (url, username, api_version)) + logger.info( + f"Connecting to subsonic server on url {url} as user {username}, " + f"API version {api_version}" + ) try: self.connection.ping() except Exception as e: - logger.error('Unable to reach subsonic server: %s' % e) + logger.error("Unable to reach subsonic server: %s" % e) exit() def get_subsonic_uri(self, view_name, params, censor=False): @@ -67,205 +82,330 @@ class SubsonicApi(): di_params.update(c=self.connection.appName) di_params.update(v=self.connection.apiVersion) if censor: - di_params.update(u='*****', p='*****') + 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)) + return "{}/{}.view?{}".format(self.url, view_name, urlencode(di_params)) def get_song_stream_uri(self, song_id): - return self.get_subsonic_uri('stream', dict(id=song_id)) + return self.get_subsonic_uri("stream", dict(id=song_id)) def get_censored_song_stream_uri(self, song_id): - return self.get_subsonic_uri('stream', dict(id=song_id), True) + return self.get_subsonic_uri("stream", dict(id=song_id), True) - 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: response = self.connection.search2( - query.encode('utf-8'), - MAX_SEARCH_RESULTS if not exclude_artists else 0, 0, - MAX_SEARCH_RESULTS if not exclude_albums else 0, 0, - MAX_SEARCH_RESULTS if not exclude_songs else 0, 0) - except Exception as e: - logger.warning('Connecting to subsonic failed when searching.') + query.encode("utf-8"), + MAX_SEARCH_RESULTS if not exclude_artists else 0, + 0, + MAX_SEARCH_RESULTS if not exclude_albums else 0, + 0, + MAX_SEARCH_RESULTS if not exclude_songs else 0, + 0, + ) + except Exception: + logger.warning("Connecting to subsonic failed when searching.") return None - if response.get('status') != RESPONSE_OK: - logger.warning('Got non-okay status code from subsonic: %s' % response.get('status')) + if response.get("status") != RESPONSE_OK: + logger.warning( + "Got non-okay status code from subsonic: %s" + % response.get("status") + ) return None - return response.get('searchResult2') + return response.get("searchResult2") - 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) if result is None: return None return SearchResult( uri=uri.get_search_uri(query), - artists=[self.raw_artist_to_artist(artist) 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 []]) + artists=[ + self.raw_artist_to_artist(artist) + 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 as e: - logger.warning('Connecting to subsonic failed when creating playlist.') + 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')) + 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 as e: - logger.warning('Connecting to subsonic failed when deleting playlist.') + 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')) + 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 as e: - logger.warning('Connecting to subsonic failed when creating playlist.') + 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')) + 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): try: response = self.connection.getArtists() - except Exception as e: - logger.warning('Connecting to subsonic failed when loading list of artists.') + except Exception: + logger.warning( + "Connecting to subsonic failed when loading list of artists." + ) return [] - if response.get('status') != RESPONSE_OK: - logger.warning('Got non-okay status code from subsonic: %s' % response.get('status')) + if response.get("status") != RESPONSE_OK: + logger.warning( + "Got non-okay status code from subsonic: %s" + % response.get("status") + ) return [] - letters = response.get('artists').get('index') + letters = response.get("artists").get("index") 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 - 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 [] def get_raw_rootdirs(self): try: response = self.connection.getIndexes() - except Exception as e: - logger.warning('Connecting to subsonic failed when loading list of rootdirs.') + except Exception: + logger.warning( + "Connecting to subsonic failed when loading list of rootdirs." + ) return [] - if response.get('status') != RESPONSE_OK: - logger.warning('Got non-okay status code from subsonic: %s' % response.get('status')) + if response.get("status") != RESPONSE_OK: + logger.warning( + "Got non-okay status code from subsonic: %s" + % response.get("status") + ) return [] - letters = response.get('indexes').get('index') + letters = response.get("indexes").get("index") 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 - 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 [] def get_song_by_id(self, song_id): try: response = self.connection.getSong(song_id) - except Exception as e: - logger.warning('Connecting to subsonic failed when loading song by id.') + except Exception: + logger.warning( + "Connecting to subsonic failed when loading song by id." + ) return None - if response.get('status') != RESPONSE_OK: - logger.warning('Got non-okay status code from subsonic: %s' % response.get('status')) + if response.get("status") != RESPONSE_OK: + logger.warning( + "Got non-okay status code from subsonic: %s" + % response.get("status") + ) 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): try: response = self.connection.getAlbum(album_id) - except Exception as e: - logger.warning('Connecting to subsonic failed when loading album by id.') + except Exception: + logger.warning( + "Connecting to subsonic failed when loading album by id." + ) return None - if response.get('status') != RESPONSE_OK: - logger.warning('Got non-okay status code from subsonic: %s' % response.get('status')) + if response.get("status") != RESPONSE_OK: + logger.warning( + "Got non-okay status code from subsonic: %s" + % response.get("status") + ) 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): try: response = self.connection.getArtist(artist_id) - except Exception as e: - logger.warning('Connecting to subsonic failed when loading artist by id.') + except Exception: + logger.warning( + "Connecting to subsonic failed when loading artist by id." + ) return None - if response.get('status') != RESPONSE_OK: - logger.warning('Got non-okay status code from subsonic: %s' % response.get('status')) + if response.get("status") != RESPONSE_OK: + logger.warning( + "Got non-okay status code from subsonic: %s" + % response.get("status") + ) 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): try: response = self.connection.getPlaylists() - except Exception as e: - logger.warning('Connecting to subsonic failed when loading list of playlists.') + except Exception: + logger.warning( + "Connecting to subsonic failed when loading list of playlists." + ) return [] - if response.get('status') != RESPONSE_OK: - logger.warning('Got non-okay status code from subsonic: %s' % response.get('status')) + if response.get("status") != RESPONSE_OK: + logger.warning( + "Got non-okay status code from subsonic: %s" + % response.get("status") + ) return [] - playlists = response.get('playlists').get('playlist') + playlists = response.get("playlists").get("playlist") 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 playlists def get_raw_playlist(self, playlist_id): try: response = self.connection.getPlaylist(playlist_id) - except Exception as e: - logger.warning('Connecting to subsonic failed when loading playlist.') + except Exception: + logger.warning( + "Connecting to subsonic failed when loading playlist." + ) return None - if response.get('status') != RESPONSE_OK: - logger.warning('Got non-okay status code from subsonic: %s' % response.get('status')) + if response.get("status") != RESPONSE_OK: + logger.warning( + "Got non-okay status code from subsonic: %s" + % response.get("status") + ) return None - return response.get('playlist') + return response.get("playlist") def get_raw_dir(self, parent_id): try: response = self.connection.getMusicDirectory(parent_id) - except Exception as e: - logger.warning('Connecting to subsonic failed when listing content of music directory.') + except Exception: + logger.warning( + "Connecting to subsonic failed when listing content of music directory." + ) return None - if response.get('status') != RESPONSE_OK: - logger.warning('Got non-okay status code from subsonic: %s' % response.get('status')) + if response.get("status") != RESPONSE_OK: + logger.warning( + "Got non-okay status code from subsonic: %s" + % response.get("status") + ) return None - directory = response.get('directory') + directory = response.get("directory") if directory is not None: - diritems = directory.get('child') + diritems = directory.get("child") return sorted(diritems, key=diritem_sort_key) return None def get_raw_albums(self, artist_id): try: response = self.connection.getArtist(artist_id) - except Exception as e: - logger.warning('Connecting to subsonic failed when loading list of albums.') + except Exception: + logger.warning( + "Connecting to subsonic failed when loading list of albums." + ) return [] - if response.get('status') != RESPONSE_OK: - logger.warning('Got non-okay status code from subsonic: %s' % response.get('status')) + if response.get("status") != RESPONSE_OK: + logger.warning( + "Got non-okay status code from subsonic: %s" + % response.get("status") + ) return [] - albums = response.get('artist').get('album') + albums = response.get("artist").get("album") 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 [] def get_raw_songs(self, album_id): try: response = self.connection.getAlbum(album_id) - except Exception as e: - logger.warning('Connecting to subsonic failed when loading list of songs in album.') + except Exception: + logger.warning( + "Connecting to subsonic failed when loading list of songs in album." + ) return [] - if response.get('status') != RESPONSE_OK: - logger.warning('Got non-okay status code from subsonic: %s' % response.get('status')) + if response.get("status") != RESPONSE_OK: + logger.warning( + "Got non-okay status code from subsonic: %s" + % response.get("status") + ) return [] - songs = response.get('album').get('song') + songs = response.get("album").get("song") if songs is not None: return songs return [] @@ -273,47 +413,85 @@ class SubsonicApi(): def get_raw_album_list(self, ltype, size=MAX_LIST_RESULTS): try: response = self.connection.getAlbumList2(ltype=ltype, size=size) - except Exception as e: - logger.warning('Connecting to subsonic failed when loading album list.') + 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')) + 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') + albums = response.get("albumList2").get("album") if albums is not None: return albums return [] 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] 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): - 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): - 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): - 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): - 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): - 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_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): - 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): - 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): return self.raw_playlist_to_playlist(self.get_raw_playlist(playlist_id)) @@ -322,14 +500,14 @@ class SubsonicApi(): playlist = self.get_raw_playlist(playlist_id) if playlist is 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')): + 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): @@ -337,9 +515,10 @@ class SubsonicApi(): if diritems is None: return for item in diritems: - if item.get('isDir'): - for song in self.get_recursive_dir_as_songs_as_tracks_iter(item.get('id')): - yield song + 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) @@ -347,80 +526,104 @@ class SubsonicApi(): if song is None: return None return Ref.track( - name=song.get('title') or UNKNOWN_SONG, - uri=uri.get_song_uri(song.get('id'))) + name=song.get("title") or UNKNOWN_SONG, + uri=uri.get_song_uri(song.get("id")), + ) def raw_song_to_track(self, song): if song is None: return None return Track( - name=song.get('title') or UNKNOWN_SONG, - uri=uri.get_song_uri(song.get('id')), - bitrate=song.get('bitRate'), - track_no=int(song.get('track')) if song.get('track') else None, - date=str(song.get('year')) or 'none', - genre=song.get('genre'), - length=int(song.get('duration')) * 1000 if song.get('duration') else None, - disc_no=int(song.get('discNumber')) if song.get('discNumber') else None, - artists=[Artist( - name=song.get('artist'), - uri=uri.get_artist_uri(song.get('artistId')))], + name=song.get("title") or UNKNOWN_SONG, + uri=uri.get_song_uri(song.get("id")), + bitrate=song.get("bitRate"), + track_no=int(song.get("track")) if song.get("track") else None, + date=str(song.get("year")) or "none", + genre=song.get("genre"), + length=int(song.get("duration")) * 1000 + if song.get("duration") + else None, + disc_no=int(song.get("discNumber")) + if song.get("discNumber") + else None, + artists=[ + Artist( + name=song.get("artist"), + uri=uri.get_artist_uri(song.get("artistId")), + ) + ], album=Album( - name=song.get('album'), - uri=uri.get_album_uri(song.get('albumId')))) + name=song.get("album"), + uri=uri.get_album_uri(song.get("albumId")), + ), + ) def raw_album_to_ref(self, album): if album is None: return None return Ref.album( - name=album.get('title') or album.get('name') or UNKNOWN_ALBUM, - uri=uri.get_album_uri(album.get('id'))) + name=album.get("title") or album.get("name") or UNKNOWN_ALBUM, + uri=uri.get_album_uri(album.get("id")), + ) def raw_album_to_album(self, album): if album is None: return None return Album( - name=album.get('title') or album.get('name') or UNKNOWN_ALBUM, - num_tracks=album.get('songCount'), - uri=uri.get_album_uri(album.get('id')), - artists=[Artist( - name=album.get('artist'), - uri=uri.get_artist_uri(album.get('artistId')))]) + name=album.get("title") or album.get("name") or UNKNOWN_ALBUM, + num_tracks=album.get("songCount"), + uri=uri.get_album_uri(album.get("id")), + artists=[ + Artist( + name=album.get("artist"), + uri=uri.get_artist_uri(album.get("artistId")), + ) + ], + ) def raw_directory_to_ref(self, directory): if directory is None: return None return Ref.directory( - name=directory.get('title') or directory.get('name'), - uri=uri.get_directory_uri(directory.get('id'))) + name=directory.get("title") or directory.get("name"), + uri=uri.get_directory_uri(directory.get("id")), + ) def raw_artist_to_ref(self, artist): if artist is None: return None return Ref.artist( - name=artist.get('name') or UNKNOWN_ARTIST, - uri=uri.get_artist_uri(artist.get('id'))) + name=artist.get("name") or UNKNOWN_ARTIST, + uri=uri.get_artist_uri(artist.get("id")), + ) def raw_artist_to_artist(self, artist): if artist is None: return None return Artist( - name=artist.get('name') or UNKNOWN_ARTIST, - uri=uri.get_artist_uri(artist.get('id'))) + name=artist.get("name") or UNKNOWN_ARTIST, + uri=uri.get_artist_uri(artist.get("id")), + ) def raw_playlist_to_playlist(self, playlist): if playlist is None: return None - entries = playlist.get('entry') - tracks = [self.raw_song_to_track(song) for song in entries] if entries is not None else None + entries = playlist.get("entry") + tracks = ( + [self.raw_song_to_track(song) for song in entries] + if entries is not None + else None + ) return Playlist( - uri=uri.get_playlist_uri(playlist.get('id')), - name=playlist.get('name'), - tracks=tracks) + uri=uri.get_playlist_uri(playlist.get("id")), + name=playlist.get("name"), + tracks=tracks, + ) def raw_playlist_to_ref(self, playlist): if playlist is None: return None return Ref.playlist( - uri=uri.get_playlist_uri(playlist.get('id')), - name=playlist.get('name')) + uri=uri.get_playlist_uri(playlist.get("id")), + name=playlist.get("name"), + ) diff --git a/mopidy_subidy/uri.py b/mopidy_subidy/uri.py index 613f958..57338b8 100644 --- a/mopidy_subidy/uri.py +++ b/mopidy_subidy/uri.py @@ -1,87 +1,109 @@ import re -SONG = 'song' -ARTIST = 'artist' -PLAYLIST = 'playlist' -ALBUM = 'album' -DIRECTORY = 'directory' -VDIR = 'vdir' -PREFIX = 'subidy' -SEARCH = 'search' +SONG = "song" +ARTIST = "artist" +PLAYLIST = "playlist" +ALBUM = "album" +DIRECTORY = "directory" +VDIR = "vdir" +PREFIX = "subidy" +SEARCH = "search" + +regex = re.compile(r"(\w+?):(\w+?)(?::|$)(.+?)?$") -regex = re.compile(r'(\w+?):(\w+?)(?::|$)(.+?)?$') def is_type_result_valid(result): return result is not None and result.group(1) == PREFIX + 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): return regex.match(uri) is not None + def get_song_id(uri): result = regex.match(uri) if not is_id_result_valid(result, SONG): return None return result.group(3) + def get_artist_id(uri): result = regex.match(uri) if not is_id_result_valid(result, ARTIST): return None return result.group(3) + def get_playlist_id(uri): result = regex.match(uri) if not is_id_result_valid(result, PLAYLIST): return None return result.group(3) + def get_album_id(uri): result = regex.match(uri) if not is_id_result_valid(result, ALBUM): return None return result.group(3) + def get_directory_id(uri): result = regex.match(uri) if not is_id_result_valid(result, DIRECTORY): return None return result.group(3) + def get_vdir_id(uri): result = regex.match(uri) if not is_id_result_valid(result, VDIR): return None return result.group(3) + def get_type(uri): result = regex.match(uri) if not is_type_result_valid(result): return None return result.group(2) + def get_type_uri(type, id): - return u'%s:%s:%s' % (PREFIX, type, id) + return f"{PREFIX}:{type}:{id}" + def get_artist_uri(id): return get_type_uri(ARTIST, id) + def get_album_uri(id): return get_type_uri(ALBUM, id) + def get_song_uri(id): return get_type_uri(SONG, id) + def get_directory_uri(id): return get_type_uri(DIRECTORY, id) + def get_vdir_uri(id): return get_type_uri(VDIR, id) + def get_playlist_uri(id): return get_type_uri(PLAYLIST, id) + def get_search_uri(query): return get_type_uri(SEARCH, query) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..bff16e0 --- /dev/null +++ b/pyproject.toml @@ -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" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..2bff96f --- /dev/null +++ b/setup.cfg @@ -0,0 +1,86 @@ +[metadata] +name = Mopidy-Subidy +version = 1.0.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 diff --git a/setup.py b/setup.py index 41863e2..6068493 100644 --- a/setup.py +++ b/setup.py @@ -1,43 +1,3 @@ -from __future__ import unicode_literals +from setuptools import setup -import re -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.7.7', - 'Pykka >= 1.1' - ], - entry_points={ - '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' - ] -) +setup() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_extension.py b/tests/test_extension.py new file mode 100644 index 0000000..70ea844 --- /dev/null +++ b/tests/test_extension.py @@ -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 diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..50c10fe --- /dev/null +++ b/tox.ini @@ -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