diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index bfd4689..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,51 +0,0 @@ -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 03640c9..4cd6c95 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,6 @@ +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 deleted file mode 100644 index 9aceeaa..0000000 --- a/CHANGELOG.rst +++ /dev/null @@ -1,111 +0,0 @@ -********* -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. diff --git a/MANIFEST.in b/MANIFEST.in index 7734bb0..6def079 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,15 +1,4 @@ -include *.py -include *.rst -include .mailmap include LICENSE include MANIFEST.in -include pyproject.toml -include tox.ini - -recursive-include .circleci * -recursive-include .github * - -include mopidy_*/ext.conf - -recursive-include tests *.py -recursive-include tests/data * +include README.md +include mopidy_subidy/ext.conf diff --git a/PKGBUILD b/PKGBUILD deleted file mode 100644 index dc95100..0000000 --- a/PKGBUILD +++ /dev/null @@ -1,35 +0,0 @@ -# Maintainer: Matthew Gamble -# Contributor: Frederick Gnodtke - -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" -} diff --git a/README.md b/README.md new file mode 100644 index 0000000..4906e37 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# 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 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 diff --git a/README.rst b/README.rst deleted file mode 100644 index dc5e316..0000000 --- a/README.rst +++ /dev/null @@ -1,81 +0,0 @@ -************* -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 -`_. - - -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 d9f260a..c26531b 100644 --- a/mopidy_subidy/__init__.py +++ b/mopidy_subidy/__init__.py @@ -1,31 +1,31 @@ -import pathlib +from __future__ import unicode_literals -import pkg_resources +import os -from mopidy import config, ext +from mopidy import ext, config -__version__ = pkg_resources.get_distribution("Mopidy-Subidy").version +__version__ = '0.2.1' 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): - return config.read(pathlib.Path(__file__).parent / "ext.conf") + conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf') + return config.read(conf_file) def get_config_schema(self): - 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) + 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) 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 4aef291..4cc50be 100644 --- a/mopidy_subidy/backend.py +++ b/mopidy_subidy/backend.py @@ -1,25 +1,18 @@ -import pykka - -import mopidy_subidy -from mopidy import backend from mopidy_subidy import library, playback, playlists, subsonic_api - +from mopidy import backend +import pykka class SubidyBackend(pykka.ThreadingActor, backend.Backend): def __init__(self, config, audio): - super().__init__() - subidy_config = config["subidy"] + super(SubidyBackend, self).__init__() + subidy_config = config['subidy'] self.subsonic_api = subsonic_api.SubsonicApi( - 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"], - ) + url=subidy_config['url'], + username=subidy_config['username'], + password=subidy_config['password'], + 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 bc5e7c2..2695534 100644 --- a/mopidy_subidy/library.py +++ b/mopidy_subidy/library.py @@ -1,12 +1,10 @@ -import logging - -from mopidy import backend +from mopidy import backend, models 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 = [ @@ -14,7 +12,6 @@ class SubidyLibraryProvider(backend.LibraryProvider): dict(id="artists", name="Artists"), dict(id="albums", name="Albums"), dict(id="rootdirs", name="Directories"), - dict(id="random", name="Random"), ] # Create a dict with the keys being the `id`s in `vdir_templates` # and the values being objects containing the vdir `id`, @@ -23,7 +20,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() @@ -31,14 +28,16 @@ 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().__init__(*args, **kwargs) + super(SubidyLibraryProvider, self).__init__(*args, **kwargs) self.subsonic_api = self.backend.subsonic_api def browse_songs(self, album_id): @@ -53,9 +52,6 @@ class SubidyLibraryProvider(backend.LibraryProvider): def browse_rootdirs(self): 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): return self.subsonic_api.get_diritems_as_refs(directory_id) @@ -70,29 +66,19 @@ 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"): - root_vdir_names = ["rootdirs", "artists", "albums", "random"] - root_vdirs = [ - self._vdirs[vdir_name] for vdir_name in root_vdir_names - ] - sorted_root_vdirs = sorted( - root_vdirs, key=lambda vdir: vdir["name"] - ) + 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"]) 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() @@ -100,9 +86,6 @@ class SubidyLibraryProvider(backend.LibraryProvider): return self.browse_artists() elif browse_uri == uri.get_vdir_uri("albums"): return self.browse_albums() - elif browse_uri == uri.get_vdir_uri("random"): - return self.browse_random_songs() - else: uri_type = uri.get_type(browse_uri) if uri_type == uri.DIRECTORY: @@ -129,7 +112,7 @@ class SubidyLibraryProvider(backend.LibraryProvider): def lookup(self, uri=None, uris=None): if uris is not None: - return {uri: self.lookup_one(uri) for uri in uris} + return dict((uri, self.lookup_one(uri)) for uri in uris) if uri is not None: return self.lookup_one(uri) return None @@ -137,74 +120,55 @@ class SubidyLibraryProvider(backend.LibraryProvider): def refresh(self, uri): pass - def search_by_artist_album_and_track( - self, artist_name, album_name, track_name - ): + 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): 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.find_raw(artist_name).get("artist") - if artists is None: - return None - tracks = [] - 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) + 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'))) 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.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]) + 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 972446c..81c0710 100644 --- a/mopidy_subidy/playback.py +++ b/mopidy_subidy/playback.py @@ -1,14 +1,12 @@ -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().__init__(*args, **kwargs) + super(SubidyPlaybackProvider, self).__init__(*args, **kwargs) self.subsonic_api = self.backend.subsonic_api def translate_uri(self, translate_uri): @@ -16,6 +14,3 @@ class SubidyPlaybackProvider(backend.PlaybackProvider): censored_url = self.subsonic_api.get_censored_song_stream_uri(song_id) logger.debug("Loading song from subsonic with url: '%s'" % censored_url) return self.subsonic_api.get_song_stream_uri(song_id) - - def should_download(self, uri): - return True diff --git a/mopidy_subidy/playlists.py b/mopidy_subidy/playlists.py index c048643..d752131 100644 --- a/mopidy_subidy/playlists.py +++ b/mopidy_subidy/playlists.py @@ -1,57 +1,35 @@ -import logging - from mopidy import backend from mopidy_subidy import uri +import logging logger = logging.getLogger(__name__) - class SubidyPlaylistsProvider(backend.PlaylistsProvider): def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + super(SubidyPlaylistsProvider, self).__init__(*args, **kwargs) self.subsonic_api = self.backend.subsonic_api self.playlists = [] self.refresh() def as_list(self): - return self.subsonic_api.get_playlists_as_refs() + return self.playlists def create(self, name): - 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, playlist_uri): - playlist_id = uri.get_playlist_id(playlist_uri) - self.subsonic_api.delete_playlist_raw(playlist_id) - - def get_items(self, items_uri): - return self.subsonic_api.get_playlist_as_songs_as_refs( - uri.get_playlist_id(items_uri) - ) - - def lookup(self, lookup_uri): - return self.subsonic_api.get_playlist_as_playlist( - uri.get_playlist_id(lookup_uri) - ) - - def refresh(self): pass + def delete(self, uri): + pass + + 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)) + + 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)) + + def refresh(self): + self.playlists = self.subsonic_api.get_playlists_as_refs() + def save(self, playlist): - 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 + pass diff --git a/mopidy_subidy/subsonic_api.py b/mopidy_subidy/subsonic_api.py index 50aacf7..4c8228e 100644 --- a/mopidy_subidy/subsonic_api.py +++ b/mopidy_subidy/subsonic_api.py @@ -1,28 +1,26 @@ -import logging -import re -from urllib.parse import urlencode, urlparse - +from urlparse import urlparse +from urllib import urlencode import libsonic -from mopidy.models import Album, Artist, Playlist, Ref, SearchResult, Track +import logging +import itertools +from mopidy.models import Track, Album, Artist, Playlist, Ref, SearchResult +import re from mopidy_subidy import uri logger = logging.getLogger(__name__) -RESPONSE_OK = "ok" -UNKNOWN_SONG = "Unknown Song" -UNKNOWN_ALBUM = "Unknown Album" -UNKNOWN_ARTIST = "Unknown Artist" +RESPONSE_OK = u'ok' +UNKNOWN_SONG = u'Unknown Song' +UNKNOWN_ALBUM = u'Unknown Album' +UNKNOWN_ARTIST = u'Unknown Artist' MAX_SEARCH_RESULTS = 100 MAX_LIST_RESULTS = 500 - -def ref_sort_key(ref): - return ref.name - +ref_sort_key = lambda ref: 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: @@ -30,517 +28,258 @@ 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, 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", - appName=app_name, + parsed.path + '/rest', 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( - f"Connecting to subsonic server on url {url} as user {username}, " - f"API version {api_version}" - ) + logger.info('Connecting to subsonic server on url %s as user %s, API version %s' % (url, username, 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): di_params = {} di_params.update(params) - di_params.update(c=self.connection.appName) + di_params.update(c='mopidy') 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.search3( - 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.") + 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.') 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("searchResult3") + 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 [] - ], - ) - - 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 + 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 get_raw_artists(self): try: response = self.connection.getArtists() - except Exception: - logger.warning( - "Connecting to subsonic failed when loading list of artists." - ) + except Exception as e: + 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: - logger.warning( - "Connecting to subsonic failed when loading list of rootdirs." - ) + except Exception as e: + 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: - logger.warning( - "Connecting to subsonic failed when loading song by id." - ) + except Exception as e: + 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: - logger.warning( - "Connecting to subsonic failed when loading album by id." - ) + except Exception as e: + 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: - logger.warning( - "Connecting to subsonic failed when loading artist by id." - ) + except Exception as e: + 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: - logger.warning( - "Connecting to subsonic failed when loading list of playlists." - ) + except Exception as e: + 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: - logger.warning( - "Connecting to subsonic failed when loading playlist." - ) + except Exception as e: + 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: - logger.warning( - "Connecting to subsonic failed when listing content of music directory." - ) + except Exception as e: + 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: - logger.warning( - "Connecting to subsonic failed when loading list of albums." - ) + except Exception as e: + 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: - logger.warning( - "Connecting to subsonic failed when loading list of songs in album." - ) + except Exception as e: + 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 [] - def get_raw_random_song(self, size=MAX_LIST_RESULTS): + def get_raw_album_list(self, ltype, size=MAX_LIST_RESULTS): try: - response = self.connection.getRandomSongs(size) - except Exception: - logger.warning( - "Connecting to subsonic failed when loading ramdom song list." - ) + response = self.connection.getAlbumList2(ltype=ltype, size=size) + except Exception as e: + 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 [] - 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") + albums = response.get('albumList2').get('album') if albums is not None: return albums 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): - 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) - ] - - 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() - ] + 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)) @@ -549,14 +288,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): @@ -564,10 +303,9 @@ class SubsonicApi: 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") - ) + if item.get('isDir'): + for song in self.get_recursive_dir_as_songs_as_tracks_iter(item.get('id')): + yield song else: yield self.raw_song_to_track(item) @@ -575,104 +313,79 @@ 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, + 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 a4d8464..613f958 100644 --- a/mopidy_subidy/uri.py +++ b/mopidy_subidy/uri.py @@ -1,110 +1,87 @@ import re -SONG = "song" -ARTIST = "artist" -PLAYLIST = "playlist" -ALBUM = "album" -DIRECTORY = "directory" -VDIR = "vdir" -PREFIX = "subidy" -SEARCH = "search" -RANDOM = "random" - -regex = re.compile(r"(\w+?):(\w+?)(?::|$)(.+?)?$") +SONG = 'song' +ARTIST = 'artist' +PLAYLIST = 'playlist' +ALBUM = 'album' +DIRECTORY = 'directory' +VDIR = 'vdir' +PREFIX = 'subidy' +SEARCH = 'search' +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 f"{PREFIX}:{type}:{id}" - + return u'%s:%s:%s' % (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 deleted file mode 100644 index bff16e0..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,17 +0,0 @@ -[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 deleted file mode 100644 index 9610cce..0000000 --- a/setup.cfg +++ /dev/null @@ -1,86 +0,0 @@ -[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 diff --git a/setup.py b/setup.py index 6068493..713b741 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,43 @@ -from setuptools import setup +from __future__ import unicode_literals -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.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' + ] +) diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_extension.py b/tests/test_extension.py deleted file mode 100644 index 70ea844..0000000 --- a/tests/test_extension.py +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 50c10fe..0000000 --- a/tox.ini +++ /dev/null @@ -1,23 +0,0 @@ -[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