Skip to content

Commit

Permalink
Release v0.3.0
Browse files Browse the repository at this point in the history
  • Loading branch information
jcass77 committed Jul 8, 2016
2 parents 4b9fe68 + 9259880 commit df9935b
Show file tree
Hide file tree
Showing 16 changed files with 364 additions and 52 deletions.
4 changes: 1 addition & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -29,5 +29,3 @@ script:

after_success:
- "if [ $TOX_ENV == 'py27' ]; then pip install coveralls; coveralls; fi"


15 changes: 14 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
Changelog
=========

v0.3.0 (Jul 8, 2016)
--------------------

**Features and improvements**

- Add support for searching Pandora stations. (Addresses: `#36 <https://github.com/rectalogic/mopidy-pandora/issues/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 <https://github.com/rectalogic/mopidy-pandora/issues/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 <https://github.com/rectalogic/mopidy-pandora#configuration>`_ options for
``preferred_audio_quality`` for details on the effect that the chosen partner device has on stream quality options.
(Fixes: `#48 <https://github.com/rectalogic/mopidy-pandora/issues/48>`_).
Expand Down
9 changes: 5 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 =

Expand All @@ -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 <http://6xq.net/playground/pandora-apidoc/json/partners/#partners>`_
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.

Expand Down
42 changes: 28 additions & 14 deletions docs/troubleshooting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,60 @@ 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
``enabled = false`` in the ``pandora`` section of your configuration file,
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 <http://mopidy.readthedocs.org/en/latest/config/?highlight=proxy#proxy-configuration>`_.

Run pydora directly
-------------------

4. Check the installed versions of OpenSSL and certifi
------------------------------------------------------

There is a `known problem <https://lukasa.co.uk/2015/04/Certifi_State_Of_Union/>`_
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 <https://github.com/mcrute/pydora#installing>`_
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
Expand Down
2 changes: 1 addition & 1 deletion mopidy_pandora/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from mopidy import config, ext

__version__ = '0.2.2'
__version__ = '0.3.0'


class Extension(ext.Extension):
Expand Down
4 changes: 2 additions & 2 deletions mopidy_pandora/ext.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion mopidy_pandora/frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
87 changes: 73 additions & 14 deletions mopidy_pandora/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import logging

import re

from collections import namedtuple

from cachetools import LRUCache
Expand All @@ -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__)

Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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)]

Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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
Loading

0 comments on commit df9935b

Please sign in to comment.