diff --git a/.travis.yml b/.travis.yml index 2b079f7..f4320ee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ env: before_install: - "sudo apt-get update -qq" - - "sudo apt-get install -y gstreamer0.10-plugins-good python-gst0.10" + - "sudo apt-get install -y gir1.2-gst-plugins-base-1.0 gir1.2-gstreamer-1.0 graphviz-dev gstreamer1.0-plugins-good gstreamer1.0-plugins-bad python-gst-1.0" install: - "pip install tox" @@ -29,5 +29,3 @@ script: after_success: - "if [ $TOX_ENV == 'py27' ]; then pip install coveralls; coveralls; fi" - - diff --git a/CHANGES.rst b/CHANGES.rst index d094542..9c5441b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,11 +1,24 @@ Changelog ========= +v0.3.0 (Jul 8, 2016) +-------------------- + +**Features and improvements** + +- Add support for searching Pandora stations. (Addresses: `#36 `_). +- Switch default partner device configuration values from ``IP01`` (iPhone) to ``android-generic``, which provides more + stream quality configuration options. + +**Fixes** + +- Album and artist URIs now point back to the Pandora track. (Fixes: `#51 `_). + v0.2.2 (Apr 13, 2016) --------------------- -- Fix an issue that would cause Mopidy-Pandora to raise an exception if a track did not have the `bitrate` field specified. +- Fix an issue that would cause Mopidy-Pandora to raise an exception if a track did not have the ``bitrate`` field specified. Please refer to the updated `configuration `_ options for ``preferred_audio_quality`` for details on the effect that the chosen partner device has on stream quality options. (Fixes: `#48 `_). diff --git a/README.rst b/README.rst index c2b8590..09ef8cf 100644 --- a/README.rst +++ b/README.rst @@ -28,6 +28,7 @@ Features - Add ratings to tracks (thumbs up, thumbs down, sleep, etc.). - Bookmark songs or artists. - Browse and add genre stations. +- Search for song, artist, and genre stations. - Play QuickMix stations. - Sort stations alphabetically or by date added. - Delete stations from the user's Pandora profile. @@ -55,7 +56,7 @@ Dependencies - Requires a Pandora user account. Users with a Pandora One subscription will have access to the higher quality 192 Kbps audio stream. Free accounts will play advertisements. -- ``pydora`` >= 1.7.0. The Python Pandora API Client. The package is available as ``pydora`` on PyPI. +- ``pydora`` >= 1.7.3. The Python Pandora API Client. The package is available as ``pydora`` on PyPI. - ``cachetools`` >= 1.0. Extensible memoizing collections and decorators. The package is available as ``cachetools`` on PyPI. @@ -84,9 +85,9 @@ configuration also requires that you provide the details of the JSON API endpoin api_host = tuner.pandora.com/services/json/ partner_encryption_key = partner_decryption_key = - partner_username = iphone + partner_username = android partner_password = - partner_device = IP01 + partner_device = android-generic username = password = @@ -98,7 +99,7 @@ The following configuration values are available: the endpoints are different for Pandora One and free accounts (details in the link provided). - ``pandora/partner_*`` related values: The `credentials `_ - to use for the Pandora API entry point. + to use for the Pandora API entry point. You *must* provide these values based on your device preferences. - ``pandora/username``: Your Pandora username. You *must* provide this. diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index aa44045..d55d1a0 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -5,15 +5,17 @@ Troubleshooting These are the recommended steps to follow if you run into any issues using Mopidy-Pandora. -Check the logs --------------- + +1. Check the logs +----------------- Have a look at the contents of ``mopidy.log`` to see if there are any obvious issues that require attention. This could range from ``mopidy.conf`` parsing errors, or problems with the Pandora account that you are using. -Ensure that Mopidy is running ------------------------------ + +2. Ensure that Mopidy is running +-------------------------------- Make sure that Mopidy itself is working correctly and that it is accessible via the browser. Disable the Mopidy-Pandora extension by setting @@ -21,30 +23,42 @@ via the browser. Disable the Mopidy-Pandora extension by setting restart Mopidy, and confirm that the other Mopidy extensions that you have installed work as expected. -Ensure that you are connected to the internet ---------------------------------------------- + +3. Ensure that you are connected to the internet +------------------------------------------------ This sounds rather obvious but Mopidy-Pandora relies on a working internet connection to log on to the Pandora servers and retrieve station information. If you are behind a proxy, you may have to configure some of Mopidy's `proxy settings `_. -Run pydora directly -------------------- + +4. Check the installed versions of OpenSSL and certifi +------------------------------------------------------ + +There is a `known problem `_ +with cross-signed certificates and versions of OpenSSL prior to 1.0.2. If you +are running Mopidy on a Raspberry Pi it is likely that you still have an older +version of OpenSSL installed. You could try upgrading OpenSSL, or as a +workaround, revert to an older version of certifi with ``pip install certifi==2015.4.28``. + + +5. Run pydora directly +---------------------- Mopidy-Pandora makes use of the pydora API, which comes bundled with its own -command-line player that can be run completely independently of Mopidy. This -is often useful for isolating issues to determine if they are Mopidy related, -or due to problems with your Pandora user account or any of a range of -technical issues in reaching and logging in to the Pandora servers. +command-line player. Running pydora completely independently of Mopidy +is often useful for isolating issues, and can be used to determine if they are +Mopidy related or not. Follow the `installation instructions `_ and use ``pydora-configure`` to create the necessary configuration file in ``~/.pydora.cfg``. Once that is done running ``pydora`` from the command line will give you a quick indication of whether the issues are Mopidy-specific or not. -Try a different Pandora user account ------------------------------------- + +6. Try a different Pandora user account +--------------------------------------- It sometimes happens that Pandora will temporarily block a user account if you exceed any of the internal skip or station request limits. It may be a good diff --git a/mopidy_pandora/__init__.py b/mopidy_pandora/__init__.py index 5e98eaf..ace8696 100644 --- a/mopidy_pandora/__init__.py +++ b/mopidy_pandora/__init__.py @@ -4,7 +4,7 @@ from mopidy import config, ext -__version__ = '0.2.2' +__version__ = '0.3.0' class Extension(ext.Extension): diff --git a/mopidy_pandora/ext.conf b/mopidy_pandora/ext.conf index eee7c83..c11e207 100644 --- a/mopidy_pandora/ext.conf +++ b/mopidy_pandora/ext.conf @@ -3,9 +3,9 @@ enabled = true api_host = tuner.pandora.com/services/json/ partner_encryption_key = partner_decryption_key = -partner_username = iphone +partner_username = android partner_password = -partner_device = IP01 +partner_device = android-generic username = password = preferred_audio_quality = highQuality diff --git a/mopidy_pandora/frontend.py b/mopidy_pandora/frontend.py index 82f44d7..7251c8b 100644 --- a/mopidy_pandora/frontend.py +++ b/mopidy_pandora/frontend.py @@ -340,7 +340,7 @@ def monitor_sequences(self): self.sequence_match_results.task_done() if match and match.ratio == 1.0: - if match.marker.uri and type(PandoraUri.factory(match.marker.uri)) is AdItemUri: + if match.marker.uri and isinstance(PandoraUri.factory(match.marker.uri), AdItemUri): logger.info('Ignoring doubleclick event for Pandora advertisement...') else: self._trigger_event_triggered(match.marker.event, match.marker.uri) diff --git a/mopidy_pandora/library.py b/mopidy_pandora/library.py index ff72f7f..edb01d2 100644 --- a/mopidy_pandora/library.py +++ b/mopidy_pandora/library.py @@ -2,6 +2,8 @@ import logging +import re + from collections import namedtuple from cachetools import LRUCache @@ -12,7 +14,7 @@ from pydora.utils import iterate_forever -from mopidy_pandora.uri import AdItemUri, GenreStationUri, GenreUri, PandoraUri, StationUri, TrackUri # noqa I101 +from mopidy_pandora.uri import AdItemUri, GenreUri, PandoraUri, SearchUri, StationUri, TrackUri # noqa I101 logger = logging.getLogger(__name__) @@ -44,14 +46,22 @@ def browse(self, uri): pandora_uri = PandoraUri.factory(uri) - if type(pandora_uri) is GenreUri: + if isinstance(pandora_uri, GenreUri): return self._browse_genre_stations(uri) - if type(pandora_uri) is StationUri or type(pandora_uri) is GenreStationUri: + if isinstance(pandora_uri, StationUri): return self._browse_tracks(uri) def lookup(self, uri): pandora_uri = PandoraUri.factory(uri) + if isinstance(pandora_uri, SearchUri): + # Create the station first so that it can be browsed. + station_uri = self._create_station_for_token(pandora_uri.token) + track = self._browse_tracks(station_uri.uri)[0] + + # Recursive call to look up first track in station that was searched for. + return self.lookup(track.uri) + if isinstance(pandora_uri, TrackUri): try: track = self.lookup_pandora_track(uri) @@ -67,7 +77,7 @@ def lookup(self, uri): if len(images) > 0: album_kwargs = {'images': [image.uri for image in images]} - if type(pandora_uri) is AdItemUri: + if isinstance(pandora_uri, AdItemUri): track_kwargs['name'] = 'Advertisement' if not track.title: @@ -77,8 +87,6 @@ def lookup(self, uri): if not track.company_name: track.company_name = '(Company name not specified)' album_kwargs['name'] = track.company_name - - album_kwargs['uri'] = track.click_through_url else: track_kwargs['name'] = track.song_name track_kwargs['length'] = track.track_length * 1000 @@ -89,11 +97,12 @@ def lookup(self, uri): pass artist_kwargs['name'] = track.artist_name album_kwargs['name'] = track.album_name - album_kwargs['uri'] = track.album_detail_url else: raise ValueError('Unexpected type to perform Pandora track lookup: {}.'.format(pandora_uri.uri_type)) + artist_kwargs['uri'] = uri # Artist lookups should just point back to the track itself. track_kwargs['artists'] = [models.Artist(**artist_kwargs)] + album_kwargs['uri'] = uri # Album lookups should just point back to the track itself. track_kwargs['album'] = models.Album(**album_kwargs) return [models.Track(**track_kwargs)] @@ -110,7 +119,13 @@ def get_images(self, uris): if image_uri: image_uris.update([image_uri]) except (TypeError, KeyError): - logger.exception("Failed to lookup image for Pandora URI '{}'.".format(uri)) + pandora_uri = PandoraUri.factory(uri) + if isinstance(pandora_uri, TrackUri): + # Could not find the track as expected - exception. + logger.exception("Failed to lookup image for Pandora URI '{}'.".format(uri)) + else: + # Lookup + logger.warning("No images available for Pandora URIs of type '{}'.".format(pandora_uri.uri_type)) pass result[uri] = [models.Image(uri=u) for u in image_uris] return result @@ -157,8 +172,8 @@ def _browse_tracks(self, uri): pandora_uri = PandoraUri.factory(uri) return [self.get_next_pandora_track(pandora_uri.station_id)] - def _create_station_for_genre(self, genre_token): - json_result = self.backend.api.create_station(search_token=genre_token) + def _create_station_for_token(self, token): + json_result = self.backend.api.create_station(search_token=token) new_station = Station.from_json(self.backend.api, json_result) self.refresh() @@ -177,8 +192,8 @@ def lookup_pandora_track(self, uri): return self.pandora_track_cache[uri].track def get_station_cache_item(self, station_id): - if GenreStationUri.pattern.match(station_id): - pandora_uri = self._create_station_for_genre(station_id) + if re.match('^([SRCG])', station_id): + pandora_uri = self._create_station_for_token(station_id) station_id = pandora_uri.station_id station = self.backend.api.get_station(station_id) @@ -194,7 +209,7 @@ def get_next_pandora_track(self, station_id): return None track_uri = PandoraUri.factory(track) - if type(track_uri) is AdItemUri: + if isinstance(track_uri, AdItemUri): track_name = 'Advertisement' else: track_name = track.song_name @@ -210,7 +225,7 @@ def refresh(self, uri=None): self.backend.api.get_genre_stations(force_refresh=True) else: pandora_uri = PandoraUri.factory(uri) - if type(pandora_uri) is StationUri: + if isinstance(pandora_uri, StationUri): try: self.pandora_station_cache.pop(pandora_uri.station_id) except KeyError: @@ -219,3 +234,47 @@ def refresh(self, uri=None): else: raise ValueError('Unexpected URI type to perform refresh of Pandora directory: {}.' .format(pandora_uri.uri_type)) + + def search(self, query=None, uris=None, exact=False, **kwargs): + search_text = self._formatted_search_query(query) + + if not search_text: + # No value provided for search query, abort. + logger.info('Unsupported Pandora search query: {}'.format(query)) + return [] + + search_result = self.backend.api.search(search_text, include_near_matches=False, include_genre_stations=True) + + tracks = [] + for genre in search_result.genre_stations: + tracks.append(models.Track(uri=SearchUri(genre.token).uri, + name='{} (Pandora genre)'.format(genre.station_name), + artists=[models.Artist(name=genre.station_name)])) + + for song in search_result.songs: + tracks.append(models.Track(uri=SearchUri(song.token).uri, + name='{} (Pandora station)'.format(song.song_name), + artists=[models.Artist(name=song.artist)])) + + artists = [] + for artist in search_result.artists: + search_uri = SearchUri(artist.token) + if search_uri.is_artist_search: + station_name = '{} (Pandora artist)'.format(artist.artist) + else: + station_name = '{} (Pandora composer)'.format(artist.artist) + artists.append(models.Artist(uri=search_uri.uri, + name=station_name)) + + return models.SearchResult(uri='pandora:search:{}'.format(search_text), tracks=tracks, artists=artists) + + def _formatted_search_query(self, query): + search_text = [] + for (field, values) in iter(query.items()): + if not hasattr(values, '__iter__'): + values = [values] + for value in values: + if field == 'any' or field == 'artist' or field == 'track_name': + search_text.append(value) + search_text = ' '.join(search_text) + return search_text diff --git a/mopidy_pandora/uri.py b/mopidy_pandora/uri.py index aae9b75..8c1e9c8 100644 --- a/mopidy_pandora/uri.py +++ b/mopidy_pandora/uri.py @@ -18,7 +18,7 @@ def with_metaclass(meta, *bases): class _PandoraUriMeta(type): - def __init__(cls, name, bases, clsdict): # noqa N805 + def __init__(cls, name, bases, clsdict): # noqa: N805 super(_PandoraUriMeta, cls).__init__(name, bases, clsdict) if hasattr(cls, 'uri_type'): cls.TYPES[cls.uri_type] = cls @@ -184,3 +184,37 @@ def __repr__(self): super(AdItemUri, self).__repr__(), **self.encoded_attributes ) + + +class SearchUri(PandoraUri): + uri_type = 'search' + + def __init__(self, token): + super(SearchUri, self).__init__(self.uri_type) + + # Check that this really is a search result URI as opposed to a regular URI. + # Search result tokens always start with 'S' (song), 'R' (artist), 'C' (composer), or 'G' (genre station). + assert re.match('^([SRCG])', token) + self.token = token + + def __repr__(self): + return '{}:{token}'.format( + super(SearchUri, self).__repr__(), + **self.encoded_attributes + ) + + @property + def is_track_search(self): + return self.token.startswith('S') + + @property + def is_artist_search(self): + return self.token.startswith('R') + + @property + def is_composer_search(self): + return self.token.startswith('C') + + @property + def is_genre_search(self): + return self.token.startswith('G') diff --git a/setup.py b/setup.py index 2af89af..b31a5cb 100644 --- a/setup.py +++ b/setup.py @@ -52,7 +52,7 @@ def run_tests(self): 'cachetools >= 1.0.0', 'Mopidy >= 1.1.2', 'Pykka >= 1.1', - 'pydora >= 1.7.0', + 'pydora >= 1.7.3', 'requests >= 2.5.0' ], tests_require=['tox'], diff --git a/tests/conftest.py b/tests/conftest.py index a71f13c..8e7018a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,7 +7,8 @@ from pandora import APIClient -from pandora.models.pandora import AdItem, GenreStation, GenreStationList, PlaylistItem, Station, StationList +from pandora.models.pandora import AdItem, GenreStation, GenreStationList, PlaylistItem, SearchResult, \ + SearchResultItem, Station, StationList import pytest @@ -205,7 +206,6 @@ def ad_metadata_result_mock(): @pytest.fixture(scope='session') def playlist_mock(simulate_request_exceptions=False): with mock.patch.object(APIClient, '__call__', mock.Mock()) as call_mock: - call_mock.return_value = playlist_result_mock()['result'] return get_backend(config(), simulate_request_exceptions).api.get_playlist(MOCK_STATION_TOKEN) @@ -278,6 +278,41 @@ def station_list_result_mock(): return mock_result['result'] +@pytest.fixture(scope='session') +def search_result_mock(): + mock_result = {'stat': 'ok', + 'result': {'nearMatchesAvailable': True, + 'explanation': '', + 'songs': [{ + 'artistName': 'search_song_artist_mock', + 'musicToken': 'S1234567', + 'songName': MOCK_TRACK_NAME, + 'score': 100 + }], + 'artists': [ + { + 'artistName': 'search_artist_artist_mock', + 'musicToken': 'R123456', + 'likelyMatch': False, + 'score': 100 + }, + { + 'artistName': 'search_artist_composer_mock', + 'musicToken': 'C123456', + 'likelyMatch': False, + 'score': 100 + }, + ], + 'genreStations': [{ + 'musicToken': 'G123', + 'score': 100, + 'stationName': 'search_genre_mock' + }]} + } + + return mock_result['result'] + + @pytest.fixture def get_station_list_mock(self, force_refresh=False): return StationList.from_json(get_backend(config()).api, station_list_result_mock()) @@ -298,6 +333,17 @@ def transport_call_not_implemented_mock(self, method, **data): raise TransportCallTestNotImplemented(method + '(' + json.dumps(self.remove_empty_values(data)) + ')') +@pytest.fixture +def search_item_mock(): + return SearchResultItem.from_json(get_backend( + config()).api, search_result_mock()['genreStations'][0]) + + +@pytest.fixture +def search_mock(self, search_text, include_near_matches=False, include_genre_stations=False): + return SearchResult.from_json(get_backend(config()).api, search_result_mock()) + + class TransportCallTestNotImplemented(Exception): pass diff --git a/tests/test_client.py b/tests/test_client.py index 1cdc965..e937f8d 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -212,6 +212,6 @@ def test_create_genre_station_invalidates_cache(config): backend.api.station_list_cache[t] = mock.Mock(spec=StationList) assert t in list(backend.api.station_list_cache) - backend.library._create_station_for_genre('test_token') + backend.library._create_station_for_token('test_token') assert t not in list(backend.api.station_list_cache) assert backend.api.station_list_cache.currsize == 1 diff --git a/tests/test_extension.py b/tests/test_extension.py index a9387de..0aa2f3d 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -22,9 +22,9 @@ def test_get_default_config(self): assert 'api_host = tuner.pandora.com/services/json/'in config assert 'partner_encryption_key ='in config assert 'partner_decryption_key ='in config - assert 'partner_username ='in config + assert 'partner_username = android'in config assert 'partner_password ='in config - assert 'partner_device ='in config + assert 'partner_device = android-generic'in config assert 'username ='in config assert 'password ='in config assert 'preferred_audio_quality = highQuality'in config diff --git a/tests/test_library.py b/tests/test_library.py index c9767bc..14ad276 100644 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -48,6 +48,15 @@ def test_get_images_for_unknown_uri_returns_empty_list(config, caplog): assert "Failed to lookup image for Pandora URI '{}'.".format(track_uri.uri) in caplog.text() +def test_get_images_for_unsupported_uri_type_issues_warning(config, caplog): + backend = conftest.get_backend(config) + + search_uri = PandoraUri.factory('pandora:search:R12345') + results = backend.library.get_images([search_uri.uri]) + assert len(results[search_uri.uri]) == 0 + assert "No images available for Pandora URIs of type 'search'.".format(search_uri.uri) in caplog.text() + + def test_get_images_for_track_without_images(config, playlist_item_mock): backend = conftest.get_backend(config) @@ -75,7 +84,6 @@ def test_get_next_pandora_track_fetches_track(config, playlist_item_mock): station_mock = mock.Mock(spec=Station) station_mock.id = 'id_token_mock' - station_mock.id = 'id_token_mock' backend.library.pandora_station_cache[station_mock.id] = StationCacheItem(station_mock, iter([playlist_item_mock])) ref = backend.library.get_next_pandora_track('id_token_mock') @@ -89,7 +97,6 @@ def test_get_next_pandora_track_handles_no_more_tracks_available(config, caplog) station_mock = mock.Mock(spec=Station) station_mock.id = 'id_token_mock' - station_mock.id = 'id_token_mock' backend.library.pandora_station_cache[station_mock.id] = StationCacheItem(station_mock, iter([])) track = backend.library.get_next_pandora_track('id_token_mock') @@ -157,6 +164,31 @@ def test_lookup_of_ad_uri_defaults_missing_values(config, ad_item_mock): assert track.album.name == '(Company name not specified)' +def test_lookup_of_search_uri(config, playlist_item_mock): + with mock.patch.object(MopidyAPIClient, 'get_station', conftest.get_station_mock): + with mock.patch.object(APIClient, 'create_station', + mock.Mock(return_value=conftest.station_result_mock()['result'])) as create_station_mock: + with mock.patch.object(APIClient, 'get_station_list', conftest.get_station_list_mock): + + backend = conftest.get_backend(config) + + station_mock = mock.Mock(spec=Station) + station_mock.id = conftest.MOCK_STATION_ID + backend.library.pandora_station_cache[station_mock.id] = \ + StationCacheItem(conftest.station_result_mock()['result'], + iter([playlist_item_mock])) + + track_uri = PlaylistItemUri._from_track(playlist_item_mock) + backend.library.pandora_track_cache[track_uri.uri] = TrackCacheItem(mock.Mock(spec=models.Ref.track), + playlist_item_mock) + + results = backend.library.lookup("pandora:search:S1234567") + # Make sure a station is created for the search URI first + assert create_station_mock.called + # Check that the first track to be played is returned correctly. + assert results[0].uri == track_uri.uri + + def test_lookup_of_track_uri(config, playlist_item_mock): backend = conftest.get_backend(config) @@ -197,6 +229,19 @@ def test_lookup_of_missing_track(config, playlist_item_mock, caplog): assert "Failed to lookup Pandora URI '{}'.".format(track_uri.uri) in caplog.text() +def test_lookup_overrides_album_and_artist_uris(config, playlist_item_mock): + backend = conftest.get_backend(config) + + track_uri = PlaylistItemUri._from_track(playlist_item_mock) + backend.library.pandora_track_cache[track_uri.uri] = TrackCacheItem(mock.Mock(spec=models.Ref.track), + playlist_item_mock) + + results = backend.library.lookup(track_uri.uri) + track = results[0] + assert next(iter(track.artists)).uri == track_uri.uri + assert track.album.uri == track_uri.uri + + def test_browse_directory_uri(config): with mock.patch.object(APIClient, 'get_station_list', conftest.get_station_list_mock): @@ -329,6 +374,22 @@ def test_browse_station_uri(config, station_mock): assert len(results) == 1 +def test_formatted_search_query_concatenates_queries_into_free_text(config): + backend = conftest.get_backend(config) + + result = backend.library._formatted_search_query({ + 'any': ['any_mock'], 'artist': ['artist_mock'], 'track_name': ['track_mock'] + }) + assert 'any_mock' in result and 'artist_mock' in result and 'track_mock' in result + + +def test_formatted_search_query_ignores_unsupported_attributes(config): + backend = conftest.get_backend(config) + + result = backend.library._formatted_search_query({'album': ['album_mock']}) + assert len(result) is 0 + + def test_refresh_without_uri_refreshes_root(config): backend = conftest.get_backend(config) backend.api.get_station_list = mock.Mock() @@ -375,7 +436,6 @@ def test_refresh_station_directory(config): station_mock = mock.Mock(spec=Station) station_mock.id = 'id_token_mock' - station_mock.id = 'id_token_mock' backend.library.pandora_station_cache[station_mock.id] = StationCacheItem(station_mock, iter([])) backend.library.refresh('pandora:station:id_token_mock:id_token_mock') @@ -393,3 +453,30 @@ def test_refresh_station_directory_not_in_cache_handles_key_error(config): assert backend.library.pandora_station_cache.currsize == 0 assert not backend.api.get_station_list.called assert not backend.api.get_genre_stations.called + + +def test_search_returns_empty_result_for_unsupported_queries(config, caplog): + backend = conftest.get_backend(config) + assert len(backend.library.search({'album': ['album_name_mock']})) is 0 + assert 'Unsupported Pandora search query:' in caplog.text() + + +def test_search(config): + with mock.patch.object(APIClient, 'search', conftest.search_mock): + + backend = conftest.get_backend(config) + search_result = backend.library.search({'any': 'search_mock'}) + + assert len(search_result.tracks) is 2 + assert search_result.tracks[0].uri == 'pandora:search:G123' + assert search_result.tracks[0].name == 'search_genre_mock (Pandora genre)' + + assert search_result.tracks[1].uri == 'pandora:search:S1234567' + assert search_result.tracks[1].name == conftest.MOCK_TRACK_NAME + ' (Pandora station)' + + assert len(search_result.artists) is 2 + assert search_result.artists[0].uri == 'pandora:search:R123456' + assert search_result.artists[0].name == 'search_artist_artist_mock (Pandora artist)' + + assert search_result.artists[1].uri == 'pandora:search:C123456' + assert search_result.artists[1].name == 'search_artist_composer_mock (Pandora composer)' diff --git a/tests/test_uri.py b/tests/test_uri.py index 443d80a..729a4d4 100644 --- a/tests/test_uri.py +++ b/tests/test_uri.py @@ -9,7 +9,8 @@ import pytest -from mopidy_pandora.uri import AdItemUri, GenreStationUri, GenreUri, PandoraUri, PlaylistItemUri, StationUri, TrackUri +from mopidy_pandora.uri import AdItemUri, GenreStationUri, GenreUri, PandoraUri, PlaylistItemUri, SearchUri,\ + StationUri, TrackUri from . import conftest @@ -119,6 +120,65 @@ def test_pandora_parse_invalid_scheme_raises_exception(): PandoraUri()._from_uri('not_the_pandora_scheme:invalid') +def test_search_uri_parse(): + + obj = PandoraUri._from_uri('pandora:search:S1234567') + assert type(obj) is SearchUri + + assert obj.uri_type == SearchUri.uri_type + assert obj.token == 'S1234567' + + obj = PandoraUri._from_uri('pandora:search:R123456') + assert type(obj) is SearchUri + + assert obj.uri_type == SearchUri.uri_type + assert obj.token == 'R123456' + + obj = PandoraUri._from_uri('pandora:search:C12345') + assert type(obj) is SearchUri + + assert obj.uri_type == SearchUri.uri_type + assert obj.token == 'C12345' + + obj = PandoraUri._from_uri('pandora:search:G123') + assert type(obj) is SearchUri + + assert obj.uri_type == SearchUri.uri_type + assert obj.token == 'G123' + + +def test_search_uri_is_track_search(): + obj = PandoraUri._from_uri('pandora:search:S1234567') + assert obj.is_track_search + + obj.token = 'R123456' + assert not obj.is_track_search + + +def test_search_uri_is_artist_search(): + obj = PandoraUri._from_uri('pandora:search:S1234567') + assert not obj.is_artist_search + + obj.token = 'R123456' + assert obj.is_artist_search + + +def test_search_uri_is_composer_search(): + obj = PandoraUri._from_uri('pandora:search:S1234567') + assert not obj.is_composer_search + + obj.token = 'C12345' + assert obj.is_composer_search + + +def test_search_uri_is_genre_search(): + obj = PandoraUri._from_uri('pandora:search:S1234567') + assert not obj.is_genre_search + + obj.token = 'G123' + assert obj.is_genre_search + + def test_station_uri_from_station(station_mock): station_uri = StationUri._from_station(station_mock) diff --git a/tox.ini b/tox.ini index 8c74207..53c6d9f 100644 --- a/tox.ini +++ b/tox.ini @@ -24,7 +24,7 @@ commands = sitepackages = false deps = flake8 - flake8-import-order +# flake8-import-order TODO: broken in flake8 v3.0: https://github.com/PyCQA/flake8-import-order/pull/75 pep8-naming skip_install = true commands = flake8 --show-source --statistics --max-line-length 120 mopidy_pandora/ setup.py tests/