From 6a87ebe8e2257811e6f0a8f4de142f72ad756557 Mon Sep 17 00:00:00 2001 From: hhm Date: Wed, 17 May 2017 02:10:58 -0400 Subject: [PATCH 01/38] B"H add track count to Album model --- mopidy_subidy/subsonic_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mopidy_subidy/subsonic_api.py b/mopidy_subidy/subsonic_api.py index 6ff7d42..35ab43f 100644 --- a/mopidy_subidy/subsonic_api.py +++ b/mopidy_subidy/subsonic_api.py @@ -349,7 +349,8 @@ class SubsonicApi(): uri=uri.get_album_uri(album.get('id')), artists=[Artist( name=album.get('artist'), - uri=uri.get_artist_uri(album.get('artistId')))]) + uri=uri.get_artist_uri(album.get('artistId')))], + num_tracks=album.get('songCount')) def raw_directory_to_ref(self, directory): if directory is None: From de69905c54b468cfa4642bdb09d690ae2928d164 Mon Sep 17 00:00:00 2001 From: hhm Date: Thu, 18 May 2017 23:25:44 -0400 Subject: [PATCH 02/38] B"H add playlist writing code --- mopidy_subidy/playlists.py | 28 ++++++++++++++++++++++++---- mopidy_subidy/subsonic_api.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/mopidy_subidy/playlists.py b/mopidy_subidy/playlists.py index d752131..338fd28 100644 --- a/mopidy_subidy/playlists.py +++ b/mopidy_subidy/playlists.py @@ -1,5 +1,6 @@ from mopidy import backend from mopidy_subidy import uri +from mopidy.models import Playlist import logging logger = logging.getLogger(__name__) @@ -15,10 +16,22 @@ class SubidyPlaylistsProvider(backend.PlaylistsProvider): return self.playlists def create(self, name): - pass + result = self.subsonic_api.create_playlist_raw(name) + if result is None: + return None + playlist = result.get('playlist') + if playlist is None: + self.refresh() + for pl in self.playlists: + if pl.name == name: + playlist = pl + return playlist + else: + return self.subsonic_api.raw_playlist_to_playlist(playlist) - def delete(self, uri): - pass + 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): #logger.info('ITEMS %s: %s' % (lookup_uri, self.subsonic_api.get_playlist_songs_as_refs(uri.get_playlist_id(items_uri)))) @@ -32,4 +45,11 @@ class SubidyPlaylistsProvider(backend.PlaylistsProvider): self.playlists = self.subsonic_api.get_playlists_as_refs() def save(self, playlist): - pass + playlist_id = uri.get_playlist_id(playlist.uri) + track_ids = [] + for trk in playlist.tracks: + track_ids.append(uri.get_song_id(trk.uri)) + result = self.subsonic_api.update_playlist_raw(playlist_id, track_ids) + if result is None: + return None + return playlist diff --git a/mopidy_subidy/subsonic_api.py b/mopidy_subidy/subsonic_api.py index 6ff7d42..25b2f5b 100644 --- a/mopidy_subidy/subsonic_api.py +++ b/mopidy_subidy/subsonic_api.py @@ -101,6 +101,39 @@ class SubsonicApi(): 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.') + 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 as e: + 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 update_playlist_raw(self, playlist_id, song_ids): + try: + response = self.connection.updatePlaylist(playlist_id, songIdsToAdd=song_ids) + except Exception as e: + logger.warning('Connecting to subsonic failed when creating playlist.') + return None + if response.get('status') != RESPONSE_OK: + logger.warning('Got non-okay status code from subsonic: %s' % response.get('status')) + return None + return response + def get_raw_artists(self): try: response = self.connection.getArtists() From a2f23c209529de66c3e0de55afd4ad4acacfcdac Mon Sep 17 00:00:00 2001 From: hhm Date: Thu, 18 May 2017 23:42:18 -0400 Subject: [PATCH 03/38] B"H use playlists instead of playlist refs --- mopidy_subidy/playlists.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mopidy_subidy/playlists.py b/mopidy_subidy/playlists.py index 338fd28..4bfde39 100644 --- a/mopidy_subidy/playlists.py +++ b/mopidy_subidy/playlists.py @@ -21,8 +21,7 @@ class SubidyPlaylistsProvider(backend.PlaylistsProvider): return None playlist = result.get('playlist') if playlist is None: - self.refresh() - for pl in self.playlists: + for pl in self.subsonic_api.get_playlists_as_playlists(): if pl.name == name: playlist = pl return playlist From 364352a7656bdc7055f604fe102649bf4bfa561d Mon Sep 17 00:00:00 2001 From: hhm Date: Thu, 18 May 2017 23:45:46 -0400 Subject: [PATCH 04/38] B"H get list of playlists immediately instead of caching them --- mopidy_subidy/playlists.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy_subidy/playlists.py b/mopidy_subidy/playlists.py index 4bfde39..6a942ad 100644 --- a/mopidy_subidy/playlists.py +++ b/mopidy_subidy/playlists.py @@ -13,7 +13,7 @@ class SubidyPlaylistsProvider(backend.PlaylistsProvider): self.refresh() def as_list(self): - return self.playlists + return self.subsonic_api.get_playlists_as_refs() def create(self, name): result = self.subsonic_api.create_playlist_raw(name) @@ -41,7 +41,7 @@ class SubidyPlaylistsProvider(backend.PlaylistsProvider): 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() + pass def save(self, playlist): playlist_id = uri.get_playlist_id(playlist.uri) From 7592036c7fff8319a7c9b0e807eef67539fc26d3 Mon Sep 17 00:00:00 2001 From: hhm Date: Fri, 19 May 2017 02:18:42 -0400 Subject: [PATCH 05/38] B"H rewrite playlist from scratch instead of adding more tracks --- mopidy_subidy/playlists.py | 2 +- mopidy_subidy/subsonic_api.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mopidy_subidy/playlists.py b/mopidy_subidy/playlists.py index 6a942ad..0bb0321 100644 --- a/mopidy_subidy/playlists.py +++ b/mopidy_subidy/playlists.py @@ -48,7 +48,7 @@ class SubidyPlaylistsProvider(backend.PlaylistsProvider): track_ids = [] for trk in playlist.tracks: track_ids.append(uri.get_song_id(trk.uri)) - result = self.subsonic_api.update_playlist_raw(playlist_id, track_ids) + result = self.subsonic_api.save_playlist_raw(playlist_id, track_ids) if result is None: return None return playlist diff --git a/mopidy_subidy/subsonic_api.py b/mopidy_subidy/subsonic_api.py index 25b2f5b..fb82627 100644 --- a/mopidy_subidy/subsonic_api.py +++ b/mopidy_subidy/subsonic_api.py @@ -123,9 +123,9 @@ class SubsonicApi(): return None return response - def update_playlist_raw(self, playlist_id, song_ids): + def save_playlist_raw(self, playlist_id, song_ids): try: - response = self.connection.updatePlaylist(playlist_id, songIdsToAdd=song_ids) + response = self.connection.createPlaylist(playlist_id, songIds=song_ids) except Exception as e: logger.warning('Connecting to subsonic failed when creating playlist.') return None From a7039ba53ca0592169ec15fac55c8945ee7dff4c Mon Sep 17 00:00:00 2001 From: hhm Date: Sat, 20 May 2017 23:35:58 -0400 Subject: [PATCH 06/38] B"H add readme item supported --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 791da2c..c01722f 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,10 @@ The following things are supported: * Searching for any terms * Browsing playlists * Searching explicitly for one of: artists, albums, tracks + * Creating, editing and deleting playlists 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.) From f70ec43cfbf26a76518041033c5756ccd73bf223 Mon Sep 17 00:00:00 2001 From: hhm Date: Sat, 20 May 2017 23:38:50 -0400 Subject: [PATCH 07/38] B"H readme item merge --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index c01722f..57357b9 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,8 @@ The following things are supported: * Browsing all artists/albums/tracks * Searching for any terms - * Browsing playlists + * Browsing, creating, editing and deleting playlists * Searching explicitly for one of: artists, albums, tracks - * Creating, editing and deleting playlists The following things are **not** supported: From 8f5d3e02168c178bf34828fc6b8cd5dfa06a13e9 Mon Sep 17 00:00:00 2001 From: hhm Date: Sun, 4 Jun 2017 22:52:50 -0400 Subject: [PATCH 08/38] B"H shuffle code a bit to make it clearer --- mopidy_subidy/subsonic_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy_subidy/subsonic_api.py b/mopidy_subidy/subsonic_api.py index d5ddb9a..92ae400 100644 --- a/mopidy_subidy/subsonic_api.py +++ b/mopidy_subidy/subsonic_api.py @@ -347,11 +347,11 @@ class SubsonicApi(): 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')))], - num_tracks=album.get('songCount')) + uri=uri.get_artist_uri(album.get('artistId')))]) def raw_directory_to_ref(self, directory): if directory is None: From f95af9c9775be1c24b72a77bba30a29ce2572278 Mon Sep 17 00:00:00 2001 From: hhm Date: Sun, 13 Aug 2017 03:17:03 -0400 Subject: [PATCH 09/38] B"H use mopidy extension name as subsonic API app name --- mopidy_subidy/backend.py | 2 ++ mopidy_subidy/subsonic_api.py | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/mopidy_subidy/backend.py b/mopidy_subidy/backend.py index 4cc50be..8c77c30 100644 --- a/mopidy_subidy/backend.py +++ b/mopidy_subidy/backend.py @@ -1,3 +1,4 @@ +import mopidy_subidy from mopidy_subidy import library, playback, playlists, subsonic_api from mopidy import backend import pykka @@ -10,6 +11,7 @@ class SubidyBackend(pykka.ThreadingActor, backend.Backend): 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']) self.library = library.SubidyLibraryProvider(backend=self) diff --git a/mopidy_subidy/subsonic_api.py b/mopidy_subidy/subsonic_api.py index 92ae400..cfc44c7 100644 --- a/mopidy_subidy/subsonic_api.py +++ b/mopidy_subidy/subsonic_api.py @@ -37,7 +37,7 @@ def diritem_sort_key(item): return (isdir, key) class SubsonicApi(): - def __init__(self, url, username, password, legacy_auth, api_version): + 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 @@ -48,6 +48,7 @@ class SubsonicApi(): password, self.port, parsed.path + '/rest', + appName=app_name, legacyAuth=legacy_auth, apiVersion=api_version) self.url = url + '/rest' @@ -63,7 +64,7 @@ class SubsonicApi(): def get_subsonic_uri(self, view_name, params, censor=False): di_params = {} di_params.update(params) - di_params.update(c='mopidy') + di_params.update(c=self.connection.appName) di_params.update(v=self.connection.apiVersion) if censor: di_params.update(u='*****', p='*****') From 521cdf8002d03b14159b48de1a61a88f979156d3 Mon Sep 17 00:00:00 2001 From: Jonathan Christison Date: Wed, 8 Aug 2018 23:01:25 +0100 Subject: [PATCH 10/38] Remove version range for py-sonic py-sonic stopped supporting python2 > 0.6.2 Using pip to install results in an error if this is not set --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 713b741..0ef32cd 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ setup( install_requires=[ 'setuptools', 'Mopidy >= 2.0', - 'py-sonic >= 0.6.1', + 'py-sonic == 0.6.2', 'Pykka >= 1.1' ], entry_points={ From e27563edaf0ff601dfa681af1df5fd9972729fb7 Mon Sep 17 00:00:00 2001 From: "Aaron B. Gallagher" Date: Wed, 29 Jan 2020 20:28:31 -0800 Subject: [PATCH 11/38] Add support for python3 Mopidy no longer supports Python 2.7. These changes remove support for Python 2.7 in subidy as well --- mopidy_subidy/subsonic_api.py | 4 ++-- setup.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mopidy_subidy/subsonic_api.py b/mopidy_subidy/subsonic_api.py index 0e09bcc..168b072 100644 --- a/mopidy_subidy/subsonic_api.py +++ b/mopidy_subidy/subsonic_api.py @@ -1,5 +1,5 @@ -from urlparse import urlparse -from urllib import urlencode +from urllib.parse import urlparse +from urllib.parse import urlencode import libsonic import logging import itertools diff --git a/setup.py b/setup.py index 0ef32cd..41863e2 100644 --- a/setup.py +++ b/setup.py @@ -24,11 +24,11 @@ setup( install_requires=[ 'setuptools', 'Mopidy >= 2.0', - 'py-sonic == 0.6.2', + 'py-sonic >= 0.7.7', 'Pykka >= 1.1' ], entry_points={ - b'mopidy.ext': [ + 'mopidy.ext': [ 'subidy = mopidy_subidy:SubidyExtension', ], }, From 008527f115057049e60671ccf42095278446da47 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 8 Mar 2020 12:26:33 +0100 Subject: [PATCH 12/38] Update project to match cookiecutter-mopidy-ext --- .circleci/config.yml | 51 ++++++++++++++++++++++++ .gitignore | 13 +++--- MANIFEST.in | 15 ++++++- pyproject.toml | 17 ++++++++ setup.cfg | 87 +++++++++++++++++++++++++++++++++++++++++ setup.py | 44 +-------------------- tests/__init__.py | 0 tests/test_extension.py | 23 +++++++++++ tox.ini | 23 +++++++++++ 9 files changed, 224 insertions(+), 49 deletions(-) create mode 100644 .circleci/config.yml create mode 100644 pyproject.toml create mode 100644 setup.cfg create mode 100644 tests/__init__.py create mode 100644 tests/test_extension.py create mode 100644 tox.ini 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/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/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..2a0e77d --- /dev/null +++ b/setup.cfg @@ -0,0 +1,87 @@ +[metadata] +name = Mopidy-Subidy +version = 0.2.1 +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.md +long_description_content_type = text/markdown +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..b2fa67f --- /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 "username" 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 From b067352a001689b766c66879c0652e344ec00394 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 8 Mar 2020 12:30:14 +0100 Subject: [PATCH 13/38] Run pyupgrade to Python 3.7+ --- mopidy_subidy/__init__.py | 4 +--- mopidy_subidy/backend.py | 2 +- mopidy_subidy/library.py | 4 ++-- mopidy_subidy/playback.py | 2 +- mopidy_subidy/playlists.py | 2 +- mopidy_subidy/subsonic_api.py | 13 ++++++------- mopidy_subidy/uri.py | 2 +- 7 files changed, 13 insertions(+), 16 deletions(-) diff --git a/mopidy_subidy/__init__.py b/mopidy_subidy/__init__.py index c26531b..39deee2 100644 --- a/mopidy_subidy/__init__.py +++ b/mopidy_subidy/__init__.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import os from mopidy import ext, config @@ -18,7 +16,7 @@ class SubidyExtension(ext.Extension): return config.read(conf_file) def get_config_schema(self): - schema = super(SubidyExtension, self).get_config_schema() + schema = super().get_config_schema() schema['url'] = config.String() schema['username'] = config.String() schema['password'] = config.Secret() diff --git a/mopidy_subidy/backend.py b/mopidy_subidy/backend.py index 8c77c30..818edbb 100644 --- a/mopidy_subidy/backend.py +++ b/mopidy_subidy/backend.py @@ -5,7 +5,7 @@ import pykka class SubidyBackend(pykka.ThreadingActor, backend.Backend): def __init__(self, config, audio): - super(SubidyBackend, self).__init__() + super().__init__() subidy_config = config['subidy'] self.subsonic_api = subsonic_api.SubsonicApi( url=subidy_config['url'], diff --git a/mopidy_subidy/library.py b/mopidy_subidy/library.py index 2695534..e622f08 100644 --- a/mopidy_subidy/library.py +++ b/mopidy_subidy/library.py @@ -37,7 +37,7 @@ class SubidyLibraryProvider(backend.LibraryProvider): _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): @@ -112,7 +112,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 diff --git a/mopidy_subidy/playback.py b/mopidy_subidy/playback.py index 81c0710..24638a0 100644 --- a/mopidy_subidy/playback.py +++ b/mopidy_subidy/playback.py @@ -6,7 +6,7 @@ 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..6a146af 100644 --- a/mopidy_subidy/playlists.py +++ b/mopidy_subidy/playlists.py @@ -7,7 +7,7 @@ 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() diff --git a/mopidy_subidy/subsonic_api.py b/mopidy_subidy/subsonic_api.py index 168b072..5d5048a 100644 --- a/mopidy_subidy/subsonic_api.py +++ b/mopidy_subidy/subsonic_api.py @@ -9,10 +9,10 @@ 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 @@ -54,7 +54,7 @@ class SubsonicApi(): 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}, API version {api_version}') try: self.connection.ping() except Exception as e: @@ -338,8 +338,7 @@ class SubsonicApi(): 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 + yield from self.get_recursive_dir_as_songs_as_tracks_iter(item.get('id')) else: yield self.raw_song_to_track(item) diff --git a/mopidy_subidy/uri.py b/mopidy_subidy/uri.py index 613f958..42fbe33 100644 --- a/mopidy_subidy/uri.py +++ b/mopidy_subidy/uri.py @@ -63,7 +63,7 @@ def get_type(uri): 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) From 1b04266d92e100d90ae1e24b3b19a3b78271f687 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 8 Mar 2020 12:30:34 +0100 Subject: [PATCH 14/38] Format with Black --- mopidy_subidy/__init__.py | 21 +- mopidy_subidy/backend.py | 20 +- mopidy_subidy/library.py | 77 ++++-- mopidy_subidy/playback.py | 1 + mopidy_subidy/playlists.py | 16 +- mopidy_subidy/subsonic_api.py | 494 ++++++++++++++++++++++++---------- mopidy_subidy/uri.py | 44 ++- 7 files changed, 466 insertions(+), 207 deletions(-) diff --git a/mopidy_subidy/__init__.py b/mopidy_subidy/__init__.py index 39deee2..49d1cdc 100644 --- a/mopidy_subidy/__init__.py +++ b/mopidy_subidy/__init__.py @@ -2,28 +2,29 @@ import os from mopidy import ext, config -__version__ = '0.2.1' +__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): - conf_file = os.path.join(os.path.dirname(__file__), '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["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 818edbb..1dace7a 100644 --- a/mopidy_subidy/backend.py +++ b/mopidy_subidy/backend.py @@ -3,18 +3,22 @@ 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'] + 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 e622f08..b7818df 100644 --- a/mopidy_subidy/library.py +++ b/mopidy_subidy/library.py @@ -3,8 +3,10 @@ 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,11 +30,9 @@ 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) @@ -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() @@ -136,39 +146,52 @@ class SubidyLibraryProvider(backend.LibraryProvider): 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 24638a0..6c9f6c8 100644 --- a/mopidy_subidy/playback.py +++ b/mopidy_subidy/playback.py @@ -4,6 +4,7 @@ import logging logger = logging.getLogger(__name__) + class SubidyPlaybackProvider(backend.PlaybackProvider): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/mopidy_subidy/playlists.py b/mopidy_subidy/playlists.py index 6a146af..49f9a14 100644 --- a/mopidy_subidy/playlists.py +++ b/mopidy_subidy/playlists.py @@ -3,8 +3,10 @@ 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().__init__(*args, **kwargs) @@ -19,7 +21,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 +35,16 @@ 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)) + # 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)) + # 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): pass diff --git a/mopidy_subidy/subsonic_api.py b/mopidy_subidy/subsonic_api.py index 5d5048a..9f49d95 100644 --- a/mopidy_subidy/subsonic_api.py +++ b/mopidy_subidy/subsonic_api.py @@ -9,18 +9,19 @@ 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 = "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 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 +29,49 @@ 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(f'Connecting to subsonic server on url {url} as user {username}, API version {api_version}') + logger.info( + f"Connecting to subsonic server on url {url} as user {username}, 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,50 +80,84 @@ 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) + 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.') + 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.') + 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 @@ -118,21 +165,33 @@ class SubsonicApi(): try: response = self.connection.deletePlaylist(playlist_id) except Exception as e: - logger.warning('Connecting to subsonic failed when deleting playlist.') + 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) + response = self.connection.createPlaylist( + playlist_id, songIds=song_ids + ) except Exception as e: - logger.warning('Connecting to subsonic failed when creating playlist.') + 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 @@ -140,79 +199,135 @@ class SubsonicApi(): try: response = self.connection.getArtists() except Exception as e: - logger.warning('Connecting to subsonic failed when loading list of artists.') + 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.') + 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.') + 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.') + 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.') + 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.') + 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 @@ -220,25 +335,35 @@ class SubsonicApi(): try: response = self.connection.getPlaylist(playlist_id) except Exception as e: - logger.warning('Connecting to subsonic failed when loading playlist.') + 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.') + 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 @@ -246,26 +371,39 @@ class SubsonicApi(): try: response = self.connection.getArtist(artist_id) except Exception as e: - logger.warning('Connecting to subsonic failed when loading list of albums.') + 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.') + 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 [] @@ -274,46 +412,84 @@ class SubsonicApi(): try: response = self.connection.getAlbumList2(ltype=ltype, size=size) except Exception as e: - logger.warning('Connecting to subsonic failed when loading album list.') + 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 +498,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,8 +513,10 @@ 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"): + yield from self.get_recursive_dir_as_songs_as_tracks_iter( + item.get("id") + ) else: yield self.raw_song_to_track(item) @@ -346,80 +524,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 42fbe33..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 f'{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) From cd22b5f694ea3a0c0bef5bd84ce157e8c100854c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 8 Mar 2020 12:31:00 +0100 Subject: [PATCH 15/38] Sort imports with isort --- mopidy_subidy/__init__.py | 2 +- mopidy_subidy/backend.py | 7 ++++--- mopidy_subidy/library.py | 4 ++-- mopidy_subidy/playback.py | 3 ++- mopidy_subidy/playlists.py | 8 ++++---- mopidy_subidy/subsonic_api.py | 10 +++++----- 6 files changed, 18 insertions(+), 16 deletions(-) diff --git a/mopidy_subidy/__init__.py b/mopidy_subidy/__init__.py index 49d1cdc..8a92d2c 100644 --- a/mopidy_subidy/__init__.py +++ b/mopidy_subidy/__init__.py @@ -1,6 +1,6 @@ import os -from mopidy import ext, config +from mopidy import config, ext __version__ = "0.2.1" diff --git a/mopidy_subidy/backend.py b/mopidy_subidy/backend.py index 1dace7a..4aef291 100644 --- a/mopidy_subidy/backend.py +++ b/mopidy_subidy/backend.py @@ -1,8 +1,9 @@ -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): diff --git a/mopidy_subidy/library.py b/mopidy_subidy/library.py index b7818df..3c597e8 100644 --- a/mopidy_subidy/library.py +++ b/mopidy_subidy/library.py @@ -1,9 +1,9 @@ +import logging + from mopidy import backend, models from mopidy.models import Ref, SearchResult from mopidy_subidy import uri -import logging - logger = logging.getLogger(__name__) diff --git a/mopidy_subidy/playback.py b/mopidy_subidy/playback.py index 6c9f6c8..83c0a7f 100644 --- a/mopidy_subidy/playback.py +++ b/mopidy_subidy/playback.py @@ -1,6 +1,7 @@ +import logging + from mopidy import backend from mopidy_subidy import uri -import logging logger = logging.getLogger(__name__) diff --git a/mopidy_subidy/playlists.py b/mopidy_subidy/playlists.py index 49f9a14..cb0d19f 100644 --- a/mopidy_subidy/playlists.py +++ b/mopidy_subidy/playlists.py @@ -1,9 +1,9 @@ -from mopidy import backend -from mopidy_subidy import uri -from mopidy.models import Playlist - import logging +from mopidy import backend +from mopidy.models import Playlist +from mopidy_subidy import uri + logger = logging.getLogger(__name__) diff --git a/mopidy_subidy/subsonic_api.py b/mopidy_subidy/subsonic_api.py index 9f49d95..e40b765 100644 --- a/mopidy_subidy/subsonic_api.py +++ b/mopidy_subidy/subsonic_api.py @@ -1,10 +1,10 @@ -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 logging 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__) From a7fdc9f436c29384c15c7f9f7a0f09ab78db65be Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 8 Mar 2020 12:32:18 +0100 Subject: [PATCH 16/38] Replace os.path with pathlib --- mopidy_subidy/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mopidy_subidy/__init__.py b/mopidy_subidy/__init__.py index 8a92d2c..d5974a3 100644 --- a/mopidy_subidy/__init__.py +++ b/mopidy_subidy/__init__.py @@ -1,4 +1,4 @@ -import os +import pathlib from mopidy import config, ext @@ -12,8 +12,7 @@ class SubidyExtension(ext.Extension): 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().get_config_schema() From 73b5f9bcc5afde3183c0cd6fe8debcb4866ced6c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 8 Mar 2020 12:33:23 +0100 Subject: [PATCH 17/38] Use pkg_resources to read version --- mopidy_subidy/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mopidy_subidy/__init__.py b/mopidy_subidy/__init__.py index d5974a3..d9f260a 100644 --- a/mopidy_subidy/__init__.py +++ b/mopidy_subidy/__init__.py @@ -1,8 +1,10 @@ import pathlib +import pkg_resources + from mopidy import config, ext -__version__ = "0.2.1" +__version__ = pkg_resources.get_distribution("Mopidy-Subidy").version class SubidyExtension(ext.Extension): From 128ba8173f986de7a57f9e3497e30fbd9505d757 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 8 Mar 2020 12:43:10 +0100 Subject: [PATCH 18/38] Fix all flake8 warnings --- mopidy_subidy/library.py | 18 +---------------- mopidy_subidy/playlists.py | 3 --- mopidy_subidy/subsonic_api.py | 38 ++++++++++++++++++----------------- tests/test_extension.py | 2 +- 4 files changed, 22 insertions(+), 39 deletions(-) diff --git a/mopidy_subidy/library.py b/mopidy_subidy/library.py index 3c597e8..6da42e0 100644 --- a/mopidy_subidy/library.py +++ b/mopidy_subidy/library.py @@ -1,6 +1,6 @@ import logging -from mopidy import backend, models +from mopidy import backend from mopidy.models import Ref, SearchResult from mopidy_subidy import uri @@ -130,22 +130,6 @@ 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 ): diff --git a/mopidy_subidy/playlists.py b/mopidy_subidy/playlists.py index cb0d19f..c048643 100644 --- a/mopidy_subidy/playlists.py +++ b/mopidy_subidy/playlists.py @@ -1,7 +1,6 @@ import logging from mopidy import backend -from mopidy.models import Playlist from mopidy_subidy import uri logger = logging.getLogger(__name__) @@ -35,13 +34,11 @@ 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) ) 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) ) diff --git a/mopidy_subidy/subsonic_api.py b/mopidy_subidy/subsonic_api.py index e40b765..e8b4ad2 100644 --- a/mopidy_subidy/subsonic_api.py +++ b/mopidy_subidy/subsonic_api.py @@ -1,4 +1,3 @@ -import itertools import logging import re from urllib.parse import urlencode, urlparse @@ -16,7 +15,9 @@ 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): @@ -66,7 +67,8 @@ class SubsonicApi: self.username = username self.password = password logger.info( - f"Connecting to subsonic server on url {url} as user {username}, API version {api_version}" + f"Connecting to subsonic server on url {url} as user {username}, " + f"API version {api_version}" ) try: self.connection.ping() @@ -108,7 +110,7 @@ class SubsonicApi: MAX_SEARCH_RESULTS if not exclude_songs else 0, 0, ) - except Exception as e: + except Exception: logger.warning("Connecting to subsonic failed when searching.") return None if response.get("status") != RESPONSE_OK: @@ -148,7 +150,7 @@ class SubsonicApi: def create_playlist_raw(self, name): try: response = self.connection.createPlaylist(name=name) - except Exception as e: + except Exception: logger.warning( "Connecting to subsonic failed when creating playlist." ) @@ -164,7 +166,7 @@ class SubsonicApi: def delete_playlist_raw(self, playlist_id): try: response = self.connection.deletePlaylist(playlist_id) - except Exception as e: + except Exception: logger.warning( "Connecting to subsonic failed when deleting playlist." ) @@ -182,7 +184,7 @@ class SubsonicApi: response = self.connection.createPlaylist( playlist_id, songIds=song_ids ) - except Exception as e: + except Exception: logger.warning( "Connecting to subsonic failed when creating playlist." ) @@ -198,7 +200,7 @@ class SubsonicApi: def get_raw_artists(self): try: response = self.connection.getArtists() - except Exception as e: + except Exception: logger.warning( "Connecting to subsonic failed when loading list of artists." ) @@ -225,7 +227,7 @@ class SubsonicApi: def get_raw_rootdirs(self): try: response = self.connection.getIndexes() - except Exception as e: + except Exception: logger.warning( "Connecting to subsonic failed when loading list of rootdirs." ) @@ -252,7 +254,7 @@ class SubsonicApi: def get_song_by_id(self, song_id): try: response = self.connection.getSong(song_id) - except Exception as e: + except Exception: logger.warning( "Connecting to subsonic failed when loading song by id." ) @@ -272,7 +274,7 @@ class SubsonicApi: def get_album_by_id(self, album_id): try: response = self.connection.getAlbum(album_id) - except Exception as e: + except Exception: logger.warning( "Connecting to subsonic failed when loading album by id." ) @@ -292,7 +294,7 @@ class SubsonicApi: def get_artist_by_id(self, artist_id): try: response = self.connection.getArtist(artist_id) - except Exception as e: + except Exception: logger.warning( "Connecting to subsonic failed when loading artist by id." ) @@ -312,7 +314,7 @@ class SubsonicApi: def get_raw_playlists(self): try: response = self.connection.getPlaylists() - except Exception as e: + except Exception: logger.warning( "Connecting to subsonic failed when loading list of playlists." ) @@ -334,7 +336,7 @@ class SubsonicApi: def get_raw_playlist(self, playlist_id): try: response = self.connection.getPlaylist(playlist_id) - except Exception as e: + except Exception: logger.warning( "Connecting to subsonic failed when loading playlist." ) @@ -350,7 +352,7 @@ class SubsonicApi: def get_raw_dir(self, parent_id): try: response = self.connection.getMusicDirectory(parent_id) - except Exception as e: + except Exception: logger.warning( "Connecting to subsonic failed when listing content of music directory." ) @@ -370,7 +372,7 @@ class SubsonicApi: def get_raw_albums(self, artist_id): try: response = self.connection.getArtist(artist_id) - except Exception as e: + except Exception: logger.warning( "Connecting to subsonic failed when loading list of albums." ) @@ -392,7 +394,7 @@ class SubsonicApi: def get_raw_songs(self, album_id): try: response = self.connection.getAlbum(album_id) - except Exception as e: + except Exception: logger.warning( "Connecting to subsonic failed when loading list of songs in album." ) @@ -411,7 +413,7 @@ 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: + except Exception: logger.warning( "Connecting to subsonic failed when loading album list." ) diff --git a/tests/test_extension.py b/tests/test_extension.py index b2fa67f..70ea844 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -16,7 +16,7 @@ def test_get_config_schema(): schema = ext.get_config_schema() # TODO Test the content of your config schema - # assert "username" in schema + assert "url" in schema # assert "password" in schema From f421bd2a9096a13d51be01b1552ec9560d9e61d4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 8 Mar 2020 21:15:12 +0100 Subject: [PATCH 19/38] docs: Add changelog --- CHANGELOG.rst | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.rst 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/setup.cfg b/setup.cfg index 2a0e77d..5a73cd6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = Mopidy-Subidy -version = 0.2.1 +version = 1.0.0 url = https://github.com/Prior99/mopidy-subidy author = prior99 author_email = fgnodtke@cronosx.de From bd2306a5fbf9764c2ce62d72708f074de1178b4b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 8 Mar 2020 21:27:04 +0100 Subject: [PATCH 20/38] docs: Add badges, install instructions, etc --- README.md | 39 --------------------------- README.rst | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ setup.cfg | 3 +-- 3 files changed, 80 insertions(+), 41 deletions(-) delete mode 100644 README.md create mode 100644 README.rst 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/setup.cfg b/setup.cfg index 5a73cd6..2bff96f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,8 +7,7 @@ author_email = fgnodtke@cronosx.de license = BSD-3-Clause license_file = LICENSE description = Subsonic extension for Mopidy -long_description = file: README.md -long_description_content_type = text/markdown +long_description = file: README.rst classifiers = Environment :: No Input/Output (Daemon) Intended Audience :: End Users/Desktop From 89093e7064dd6a13cd47a374efb902ff8ef58084 Mon Sep 17 00:00:00 2001 From: Frederick Date: Fri, 13 Mar 2020 13:35:28 +0100 Subject: [PATCH 21/38] Release 1.0.0 --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a5a1c85..9aceeaa 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,7 +3,7 @@ Changelog ********* -v1.0.0 (UNRELEASED) +v1.0.0 (2020-03-13) =================== - Require Mopidy 3.0 or newer. From 713845090cee75e08af1bebb23c6a329a71565ef Mon Sep 17 00:00:00 2001 From: Bryn Edwards Date: Sat, 14 Mar 2020 18:43:53 +0000 Subject: [PATCH 22/38] Make repeated getAlbumList2 requests with offset to get all albums --- mopidy_subidy/subsonic_api.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/mopidy_subidy/subsonic_api.py b/mopidy_subidy/subsonic_api.py index e8b4ad2..78c789c 100644 --- a/mopidy_subidy/subsonic_api.py +++ b/mopidy_subidy/subsonic_api.py @@ -410,9 +410,9 @@ class SubsonicApi: return songs return [] - def get_raw_album_list(self, ltype, size=MAX_LIST_RESULTS): + def get_more_albums(self, ltype, size=MAX_LIST_RESULTS, offset=0): try: - response = self.connection.getAlbumList2(ltype=ltype, size=size) + response = self.connection.getAlbumList2(ltype=ltype, size=size, offset=offset) except Exception: logger.warning( "Connecting to subsonic failed when loading album list." @@ -429,6 +429,18 @@ class SubsonicApi: return albums return [] + def get_raw_album_list(self, ltype, size=MAX_LIST_RESULTS): + offset = 0 + total = [] + albums = self.get_more_albums(ltype, size, offset) + total = albums + while len(albums) == size: + # try getting more albums + 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") From ee3e36408ddd7c21b4bff45de3a605fa52114a2b Mon Sep 17 00:00:00 2001 From: Bryn Edwards Date: Sat, 14 Mar 2020 18:57:55 +0000 Subject: [PATCH 23/38] formatting --- mopidy_subidy/subsonic_api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mopidy_subidy/subsonic_api.py b/mopidy_subidy/subsonic_api.py index 78c789c..4afec91 100644 --- a/mopidy_subidy/subsonic_api.py +++ b/mopidy_subidy/subsonic_api.py @@ -412,7 +412,9 @@ class SubsonicApi: def get_more_albums(self, ltype, size=MAX_LIST_RESULTS, offset=0): try: - response = self.connection.getAlbumList2(ltype=ltype, size=size, offset=offset) + response = self.connection.getAlbumList2( + ltype=ltype, size=size, offset=offset + ) except Exception: logger.warning( "Connecting to subsonic failed when loading album list." From 584209c1347e6a0c20577da29a8211fb91a7f434 Mon Sep 17 00:00:00 2001 From: Bryn Edwards Date: Wed, 25 Mar 2020 08:03:46 +0000 Subject: [PATCH 24/38] comment --- mopidy_subidy/subsonic_api.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mopidy_subidy/subsonic_api.py b/mopidy_subidy/subsonic_api.py index 4afec91..dfa1db1 100644 --- a/mopidy_subidy/subsonic_api.py +++ b/mopidy_subidy/subsonic_api.py @@ -432,12 +432,18 @@ class SubsonicApi: 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: - # try getting more albums offset = offset + size albums = self.get_more_albums(ltype, size, offset) total = total + albums From eff25672d9e988d0f1bf836ff148c42a800ed254 Mon Sep 17 00:00:00 2001 From: vincent Date: Mon, 2 Nov 2020 16:08:50 +0100 Subject: [PATCH 25/38] improve research by artist --- mopidy_subidy/library.py | 3 ++- mopidy_subidy/subsonic_api.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/mopidy_subidy/library.py b/mopidy_subidy/library.py index 6da42e0..483fbf8 100644 --- a/mopidy_subidy/library.py +++ b/mopidy_subidy/library.py @@ -147,6 +147,7 @@ class SubidyLibraryProvider(backend.LibraryProvider): return SearchResult( tracks=self.subsonic_api.get_songs_as_tracks(album.get("id")) ) + def get_distinct(self, field, query): search_result = self.search(query) @@ -173,7 +174,7 @@ class SubidyLibraryProvider(backend.LibraryProvider): query.get("artist")[0], query.get("album")[0] ) if "artist" in query: - return self.subsonic_api.find_as_search_result( + return self.subsonic_api.find_artist_as_search_result( query.get("artist")[0] ) if "any" in query: diff --git a/mopidy_subidy/subsonic_api.py b/mopidy_subidy/subsonic_api.py index e8b4ad2..dbae5df 100644 --- a/mopidy_subidy/subsonic_api.py +++ b/mopidy_subidy/subsonic_api.py @@ -93,6 +93,7 @@ class SubsonicApi: def get_censored_song_stream_uri(self, song_id): return self.get_subsonic_uri("stream", dict(id=song_id), True) + def find_raw( self, query, @@ -121,6 +122,34 @@ class SubsonicApi: return None return response.get("searchResult2") + def find_artist_as_search_result ( + self, + artist_search + ): + result = self.find_raw(artist_search) + if result is None: + return None + return SearchResult( + uri=uri.get_search_uri(artist_search), + artists=[ + self.raw_artist_to_artist(artist) + for artist in result.get("artist") or [] + if artist_search.casefold() in artist.get("name").casefold() + + ], + albums=[ + self.raw_album_to_album(album) + for album in result.get("album") or [] + if artist_search.casefold() in album.get("artist").casefold() + ], + tracks=[ + self.raw_song_to_track(song) + for song in result.get("song") or [] + if artist_search.casefold() in song.get("artist").casefold() + ], + ) + + def find_as_search_result( self, query, From 31023236ae3288a6bab8b242007e996839697974 Mon Sep 17 00:00:00 2001 From: vincent Date: Mon, 2 Nov 2020 16:25:57 +0100 Subject: [PATCH 26/38] correct formatting --- mopidy_subidy/library.py | 1 - mopidy_subidy/subsonic_api.py | 9 +++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/mopidy_subidy/library.py b/mopidy_subidy/library.py index 483fbf8..90c6bdc 100644 --- a/mopidy_subidy/library.py +++ b/mopidy_subidy/library.py @@ -147,7 +147,6 @@ class SubidyLibraryProvider(backend.LibraryProvider): return SearchResult( tracks=self.subsonic_api.get_songs_as_tracks(album.get("id")) ) - def get_distinct(self, field, query): search_result = self.search(query) diff --git a/mopidy_subidy/subsonic_api.py b/mopidy_subidy/subsonic_api.py index dbae5df..9be1e7c 100644 --- a/mopidy_subidy/subsonic_api.py +++ b/mopidy_subidy/subsonic_api.py @@ -93,7 +93,6 @@ class SubsonicApi: def get_censored_song_stream_uri(self, song_id): return self.get_subsonic_uri("stream", dict(id=song_id), True) - def find_raw( self, query, @@ -122,7 +121,7 @@ class SubsonicApi: return None return response.get("searchResult2") - def find_artist_as_search_result ( + def find_artist_as_search_result( self, artist_search ): @@ -132,10 +131,9 @@ class SubsonicApi: return SearchResult( uri=uri.get_search_uri(artist_search), artists=[ - self.raw_artist_to_artist(artist) - for artist in result.get("artist") or [] + self.raw_artist_to_artist(artist) + for artist in result.get("artist") or [] if artist_search.casefold() in artist.get("name").casefold() - ], albums=[ self.raw_album_to_album(album) @@ -149,7 +147,6 @@ class SubsonicApi: ], ) - def find_as_search_result( self, query, From f7090127feae2d0314f82166aa8bcae948337978 Mon Sep 17 00:00:00 2001 From: vincent Date: Mon, 2 Nov 2020 16:34:21 +0100 Subject: [PATCH 27/38] black reformating --- mopidy_subidy/subsonic_api.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/mopidy_subidy/subsonic_api.py b/mopidy_subidy/subsonic_api.py index 9be1e7c..c26a1b0 100644 --- a/mopidy_subidy/subsonic_api.py +++ b/mopidy_subidy/subsonic_api.py @@ -121,10 +121,7 @@ class SubsonicApi: return None return response.get("searchResult2") - def find_artist_as_search_result( - self, - artist_search - ): + def find_artist_as_search_result(self, artist_search): result = self.find_raw(artist_search) if result is None: return None From 511a101c2c4866bd13fdedf6bcaddfa21c1177af Mon Sep 17 00:00:00 2001 From: vincent Date: Mon, 2 Nov 2020 19:54:41 +0100 Subject: [PATCH 28/38] modify artist search to retur all artist track --- mopidy_subidy/library.py | 20 +++++++++++++++++--- mopidy_subidy/subsonic_api.py | 27 ++------------------------- 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/mopidy_subidy/library.py b/mopidy_subidy/library.py index 90c6bdc..bd87352 100644 --- a/mopidy_subidy/library.py +++ b/mopidy_subidy/library.py @@ -148,6 +148,22 @@ class SubidyLibraryProvider(backend.LibraryProvider): tracks=self.subsonic_api.get_songs_as_tracks(album.get("id")) ) + def search_by_artist(self, artist_search): + result = self.subsonic_api.find_raw(artist_search) + if result is None: + return None + + tracks = [] + for artist in result.get("artist"): + albums = self.subsonic_api.get_raw_albums(artist.get("id")) + for album in albums: + tracks.extend( + self.subsonic_api.get_songs_as_tracks(album.get("id")) + ) + return SearchResult( + uri=uri.get_search_uri(artist_search), tracks=tracks + ) + def get_distinct(self, field, query): search_result = self.search(query) if not search_result: @@ -173,9 +189,7 @@ class SubidyLibraryProvider(backend.LibraryProvider): query.get("artist")[0], query.get("album")[0] ) if "artist" in query: - return self.subsonic_api.find_artist_as_search_result( - query.get("artist")[0] - ) + return self.search_by_artist(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/subsonic_api.py b/mopidy_subidy/subsonic_api.py index c26a1b0..329a482 100644 --- a/mopidy_subidy/subsonic_api.py +++ b/mopidy_subidy/subsonic_api.py @@ -101,7 +101,7 @@ class SubsonicApi: exclude_songs=False, ): try: - response = self.connection.search2( + response = self.connection.search3( query.encode("utf-8"), MAX_SEARCH_RESULTS if not exclude_artists else 0, 0, @@ -119,30 +119,7 @@ class SubsonicApi: % response.get("status") ) return None - return response.get("searchResult2") - - def find_artist_as_search_result(self, artist_search): - result = self.find_raw(artist_search) - if result is None: - return None - return SearchResult( - uri=uri.get_search_uri(artist_search), - artists=[ - self.raw_artist_to_artist(artist) - for artist in result.get("artist") or [] - if artist_search.casefold() in artist.get("name").casefold() - ], - albums=[ - self.raw_album_to_album(album) - for album in result.get("album") or [] - if artist_search.casefold() in album.get("artist").casefold() - ], - tracks=[ - self.raw_song_to_track(song) - for song in result.get("song") or [] - if artist_search.casefold() in song.get("artist").casefold() - ], - ) + return response.get("searchResult3") def find_as_search_result( self, From 6857653cdb65badf6057d06b5534c157c61b8a11 Mon Sep 17 00:00:00 2001 From: vincent Date: Mon, 2 Nov 2020 21:13:27 +0100 Subject: [PATCH 29/38] modifyng search_by_artist_and_album --- mopidy_subidy/library.py | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/mopidy_subidy/library.py b/mopidy_subidy/library.py index bd87352..d63aa66 100644 --- a/mopidy_subidy/library.py +++ b/mopidy_subidy/library.py @@ -138,31 +138,30 @@ class SubidyLibraryProvider(backend.LibraryProvider): 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")) - ) + 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_search): - result = self.subsonic_api.find_raw(artist_search) + def search_by_artist(self, artist_name): + result = self.subsonic_api.find_raw(artist_name) if result is None: return None - tracks = [] for artist in result.get("artist"): - albums = self.subsonic_api.get_raw_albums(artist.get("id")) - for album in albums: - tracks.extend( - self.subsonic_api.get_songs_as_tracks(album.get("id")) + tracks.extend( + self.subsonic_api.get_artist_as_songs_as_tracks_iter( + artist.get("id") ) - return SearchResult( - uri=uri.get_search_uri(artist_search), tracks=tracks - ) + ) + return SearchResult(uri=uri.get_search_uri(artist_name), tracks=tracks) def get_distinct(self, field, query): search_result = self.search(query) From 1bc9e35d837de8422b388db07c0b65ac31b10494 Mon Sep 17 00:00:00 2001 From: vincent Date: Mon, 2 Nov 2020 23:12:39 +0100 Subject: [PATCH 30/38] add exact to search artist --- mopidy_subidy/library.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mopidy_subidy/library.py b/mopidy_subidy/library.py index d63aa66..9ae885b 100644 --- a/mopidy_subidy/library.py +++ b/mopidy_subidy/library.py @@ -150,12 +150,16 @@ class SubidyLibraryProvider(backend.LibraryProvider): ) return SearchResult(tracks=tracks) - def search_by_artist(self, artist_name): + 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") @@ -188,7 +192,8 @@ class SubidyLibraryProvider(backend.LibraryProvider): query.get("artist")[0], query.get("album")[0] ) if "artist" in query: - return self.search_by_artist(query.get("artist")[0]) + return self.search_by_artist(query.get("artist")[0], + exact) 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()) From 81a62cdca4fec88f1e417bf497c5333ff1d92e63 Mon Sep 17 00:00:00 2001 From: vincent Date: Tue, 3 Nov 2020 20:36:28 +0100 Subject: [PATCH 31/38] add random mode in browse and search --- mopidy_subidy/library.py | 19 +++++++++++++++---- mopidy_subidy/subsonic_api.py | 30 ++++++++++++++++++++++++++++++ mopidy_subidy/uri.py | 1 + 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/mopidy_subidy/library.py b/mopidy_subidy/library.py index 9ae885b..bc5e7c2 100644 --- a/mopidy_subidy/library.py +++ b/mopidy_subidy/library.py @@ -14,6 +14,7 @@ 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`, @@ -52,6 +53,9 @@ 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) @@ -82,7 +86,7 @@ class SubidyLibraryProvider(backend.LibraryProvider): def browse(self, browse_uri): if browse_uri == uri.get_vdir_uri("root"): - root_vdir_names = ["rootdirs", "artists", "albums"] + root_vdir_names = ["rootdirs", "artists", "albums", "random"] root_vdirs = [ self._vdirs[vdir_name] for vdir_name in root_vdir_names ] @@ -96,6 +100,9 @@ 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: @@ -150,7 +157,7 @@ class SubidyLibraryProvider(backend.LibraryProvider): ) return SearchResult(tracks=tracks) - def search_by_artist(self, artist_name,exact): + def search_by_artist(self, artist_name, exact): result = self.subsonic_api.find_raw(artist_name) if result is None: return None @@ -192,8 +199,12 @@ class SubidyLibraryProvider(backend.LibraryProvider): query.get("artist")[0], query.get("album")[0] ) if "artist" in query: - return self.search_by_artist(query.get("artist")[0], - exact) + return self.search_by_artist(query.get("artist")[0], exact) + if "comment" in query: + if query.get("comment")[0] == "random": + return SearchResult( + tracks=self.subsonic_api.get_random_songs_as_tracks() + ) if "any" in query: return self.subsonic_api.find_as_search_result(query.get("any")[0]) return SearchResult(artists=self.subsonic_api.get_artists_as_artists()) diff --git a/mopidy_subidy/subsonic_api.py b/mopidy_subidy/subsonic_api.py index 329a482..789dc25 100644 --- a/mopidy_subidy/subsonic_api.py +++ b/mopidy_subidy/subsonic_api.py @@ -410,6 +410,25 @@ class SubsonicApi: return songs return [] + def get_raw_random_song(self, size=MAX_LIST_RESULTS): + try: + response = self.connection.getRandomSongs(size) + 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") + ) + return [] + songs = response.get("randomSongs").get("song") + if songs is not None: + return songs + return [] + def get_raw_album_list(self, ltype, size=MAX_LIST_RESULTS): try: response = self.connection.getAlbumList2(ltype=ltype, size=size) @@ -475,6 +494,17 @@ class SubsonicApi: 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(MAX_LIST_RESULTS) + ] + def get_artists_as_artists(self): return [ self.raw_artist_to_artist(artist) diff --git a/mopidy_subidy/uri.py b/mopidy_subidy/uri.py index 57338b8..a4d8464 100644 --- a/mopidy_subidy/uri.py +++ b/mopidy_subidy/uri.py @@ -8,6 +8,7 @@ DIRECTORY = "directory" VDIR = "vdir" PREFIX = "subidy" SEARCH = "search" +RANDOM = "random" regex = re.compile(r"(\w+?):(\w+?)(?::|$)(.+?)?$") From 7064dd9e50b15d1d2937fd962800493d30d76245 Mon Sep 17 00:00:00 2001 From: vincent Date: Tue, 3 Nov 2020 20:38:01 +0100 Subject: [PATCH 32/38] reformating --- mopidy_subidy/library.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mopidy_subidy/library.py b/mopidy_subidy/library.py index 9ae885b..8cfb319 100644 --- a/mopidy_subidy/library.py +++ b/mopidy_subidy/library.py @@ -150,7 +150,7 @@ class SubidyLibraryProvider(backend.LibraryProvider): ) return SearchResult(tracks=tracks) - def search_by_artist(self, artist_name,exact): + def search_by_artist(self, artist_name, exact): result = self.subsonic_api.find_raw(artist_name) if result is None: return None @@ -192,8 +192,7 @@ class SubidyLibraryProvider(backend.LibraryProvider): query.get("artist")[0], query.get("album")[0] ) if "artist" in query: - return self.search_by_artist(query.get("artist")[0], - exact) + return self.search_by_artist(query.get("artist")[0], exact) 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()) From cf588481c638d7b911109b96ae138c194dd33029 Mon Sep 17 00:00:00 2001 From: Frederick Date: Thu, 19 Nov 2020 09:06:50 +0100 Subject: [PATCH 33/38] Add note about maintenance --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index bf10e40..dc5e316 100644 --- a/README.rst +++ b/README.rst @@ -14,6 +14,8 @@ 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 `_. From e8493923c1c15db1424d41444aa799c81a337e57 Mon Sep 17 00:00:00 2001 From: Frederick Date: Thu, 19 Nov 2020 09:14:44 +0100 Subject: [PATCH 34/38] Update version --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 2bff96f..9610cce 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = Mopidy-Subidy -version = 1.0.0 +version = 1.1.0 url = https://github.com/Prior99/mopidy-subidy author = prior99 author_email = fgnodtke@cronosx.de From b5e0de1e4deea0f043f061f9e2b2747be44c5984 Mon Sep 17 00:00:00 2001 From: vincent Date: Tue, 24 Nov 2020 14:53:17 +0100 Subject: [PATCH 35/38] correct warning --- mopidy_subidy/subsonic_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy_subidy/subsonic_api.py b/mopidy_subidy/subsonic_api.py index 789dc25..793401f 100644 --- a/mopidy_subidy/subsonic_api.py +++ b/mopidy_subidy/subsonic_api.py @@ -415,7 +415,7 @@ class SubsonicApi: response = self.connection.getRandomSongs(size) except Exception: logger.warning( - "Connecting to subsonic failed when loading list of songs in album." + "Connecting to subsonic failed when loading ramdom song list." ) return [] if response.get("status") != RESPONSE_OK: From 5ce65f0a0882e78cd86d68b8ae4c5a5fb92c2f3e Mon Sep 17 00:00:00 2001 From: vincent Date: Thu, 14 Jan 2021 17:31:05 +0100 Subject: [PATCH 36/38] change argument --- mopidy_subidy/subsonic_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy_subidy/subsonic_api.py b/mopidy_subidy/subsonic_api.py index 793401f..8aba02d 100644 --- a/mopidy_subidy/subsonic_api.py +++ b/mopidy_subidy/subsonic_api.py @@ -502,7 +502,7 @@ class SubsonicApi: def get_random_songs_as_tracks(self): return [ self.raw_song_to_track(song) - for song in self.get_raw_random_song(MAX_LIST_RESULTS) + for song in self.get_raw_random_song() ] def get_artists_as_artists(self): From 62429f22bd3174982a6c5b05718024fd81c3edf1 Mon Sep 17 00:00:00 2001 From: lubiana Date: Tue, 4 Feb 2025 16:41:22 +0000 Subject: [PATCH 37/38] Update mopidy_subidy/playback.py --- mopidy_subidy/playback.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mopidy_subidy/playback.py b/mopidy_subidy/playback.py index 83c0a7f..972446c 100644 --- a/mopidy_subidy/playback.py +++ b/mopidy_subidy/playback.py @@ -16,3 +16,6 @@ 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 From ebb2dab571eae062d94adc40b0f24c7cadc6f030 Mon Sep 17 00:00:00 2001 From: lubiana Date: Wed, 5 Feb 2025 18:55:01 +0100 Subject: [PATCH 38/38] add pkbuild --- PKGBUILD | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 PKGBUILD diff --git a/PKGBUILD b/PKGBUILD new file mode 100644 index 0000000..dc95100 --- /dev/null +++ b/PKGBUILD @@ -0,0 +1,35 @@ +# 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" +}