Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
rectalogic committed Nov 2, 2015
2 parents 6568b09 + b009384 commit 85426fe
Show file tree
Hide file tree
Showing 13 changed files with 377 additions and 120 deletions.
36 changes: 22 additions & 14 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Mopidy-Pandora to your Mopidy configuration file::
username =
password =
sort_order = date
auto_set_repeat = true
auto_setup = true

### EXPERIMENTAL EVENT HANDLING IMPLEMENTATION ###
event_support_enabled = false
Expand All @@ -74,9 +74,9 @@ alphabetical order.
and web extensions:

- double_click_interval - successive button clicks that occur within this interval (in seconds) will trigger the event.
- on_pause_resume_click - click pause and then play while a song is playing to trigger the event. Song continues afterwards.
- on_pause_next_click - click pause and then next in quick succession. Calls event and skips to next song. You will have to click play again for the next song to start :(
- on_pause_previous_click - click pause and then previous in quick succession. Calls event and skips to next song. You will have to click play again for the next song to start :(
- on_pause_resume_click - click pause and then play while a song is playing to trigger the event.
- on_pause_next_click - click pause and then next in quick succession. Calls event and skips to next song.
- on_pause_previous_click - click pause and then previous in quick succession. Calls event and skips to next song.

The supported events are: thumbs_up, thumbs_down, sleep, add_artist_bookmark, add_song_bookmark

Expand All @@ -85,9 +85,9 @@ Usage

Mopidy needs `dynamic playlist <https://github.com/mopidy/mopidy/issues/620>`_ and
`core extensions <https://github.com/mopidy/mopidy/issues/1100>`_ support to properly support Pandora. In the meantime,
Mopidy-Pandora represents each Pandora station as a separate playlist. The Playlist needs to be played **in repeat mode**,
and Mopidy-Pandora will enable this automatically just before each track is changed unless you set the **auto_set_repeat**
config parameter to 'false'.
Mopidy-Pandora represents each Pandora station as a separate playlist. The Playlist needs to be played **in repeat mode**
and **consume**, **random**, and **single** should be turned off. Mopidy-Pandora will set this up automatically unless
you set the **auto_setup** config parameter to 'false'.

Each time a track is played, the next dynamic track for that Pandora station will be played. The playlist will consist
of a single track unless the experimental ratings support is enabled. With ratings support enabled, the playlist will
Expand All @@ -107,29 +107,37 @@ Project resources
Changelog
=========

v0.1.7 (Oct 31, 2015)
----------------------------------------

- Configuration parameter 'auto_set_repeat' has been renamed to 'auto_setup' - please update your Mopidy configuration file.
- Now resumes playback after a track has been rated.
- Enhanced auto_setup routines to ensure that 'consume', 'random', and 'single' modes are disabled as well.
- Optimized auto_setup routines: now only called when the Mopidy tracklist changes.

v0.1.6 (Oct 26, 2015)
----------------------------------------

- Release to pypi

v0.1.5 (UNRELEASED)
v0.1.5 (Aug 20, 2015)
----------------------------------------

- Add option to automatically set tracks to play in repeat mode, using Mopidy's 'about-to-finish' callback.
- Add option to automatically set tracks to play in repeat mode when Mopidy-Pandora starts.
- Add experimental support for rating songs by re-using buttons available in the current front-end Mopidy extensions.
- Audio quality now defaults to the highest setting.
- Improved caching to revert to Pandora server if station cannot be found in the local cache.
- Fix to retrieve stations by ID instead of token.
- Add unit tests to increase test coverage.

v0.1.4 (UNRELEASED)
v0.1.4 (Aug 17, 2015)
----------------------------------------

- Limit number of consecutive track skips to prevent Mopidy's skip-to-next-on-error behaviour from locking the user's Pandora account.
- Better handling of exceptions that occur in the backend to prevent Mopidy actor crashes.
- Add support for unicode characters in station and track names.

v0.1.3 (UNRELEASED)
v0.1.3 (Jul 11, 2015)
----------------------------------------

- Update to work with release of Mopidy version 1.0
Expand All @@ -138,19 +146,19 @@ v0.1.3 (UNRELEASED)
- Get rid of 'Stations' root directory. Browsing now displays all of the available stations immediately.
- Fill artist name to improve how tracks are displayed in various Mopidy front-end extensions.

v0.1.2 (UNRELEASED)
v0.1.2 (Jun 20, 2015)
----------------------------------------

- Enhancement to handle 'Invalid Auth Token' exceptions when the Pandora session expires after long periods of
inactivity. Allows Mopidy-Pandora to run indefinitely on dedicated music servers like the Pi MusicBox.
- Add configuration option to sort stations alphabetically, instead of by date.

v0.1.1 (UNRELEASED)
v0.1.1 (Mar 22, 2015)
----------------------------------------

- Added ability to make preferred audio quality user-configurable.

v0.1.0 (UNRELEASED)
v0.1.0 (Dec 28, 2014)
----------------------------------------

- Initial release.
6 changes: 4 additions & 2 deletions mopidy_pandora/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
import os

from mopidy import config, ext
from mopidy.config import Deprecated

from pandora import BaseAPIClient


__version__ = '0.1.6'
__version__ = '0.1.7'

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -37,7 +38,8 @@ def get_config_schema(self):
BaseAPIClient.MED_AUDIO_QUALITY,
BaseAPIClient.HIGH_AUDIO_QUALITY])
schema['sort_order'] = config.String(optional=True, choices=['date', 'A-Z', 'a-z'])
schema['auto_set_repeat'] = config.Boolean()
schema['auto_setup'] = config.Boolean()
schema['auto_set_repeat'] = Deprecated()
schema['event_support_enabled'] = config.Boolean()
schema['double_click_interval'] = config.String()
schema['on_pause_resume_click'] = config.String(choices=['thumbs_up', 'thumbs_down', 'sleep'])
Expand Down
11 changes: 8 additions & 3 deletions mopidy_pandora/backend.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from mopidy import backend
from mopidy import backend, core
from mopidy.internal import encoding

from pandora import BaseAPIClient, clientbuilder
Expand All @@ -15,7 +15,7 @@
from mopidy_pandora.uri import logger


class PandoraBackend(pykka.ThreadingActor, backend.Backend):
class PandoraBackend(pykka.ThreadingActor, backend.Backend, core.CoreListener):

def __init__(self, config, audio):
super(PandoraBackend, self).__init__()
Expand All @@ -33,7 +33,9 @@ def __init__(self, config, audio):

self.library = PandoraLibraryProvider(backend=self, sort_order=self._config['sort_order'])

self.auto_set_repeat = self._config['auto_set_repeat']
self.auto_setup = self._config['auto_setup']
self.setup_required = self.auto_setup

self.rpc_client = rpc.RPCClient(config['http']['hostname'], config['http']['port'])

self.supports_events = False
Expand All @@ -50,3 +52,6 @@ def on_start(self):
self.api.login(self._config["username"], self._config["password"])
except requests.exceptions.RequestException as e:
logger.error('Error logging in to Pandora: %s', encoding.locale_decode(e))

def tracklist_changed(self):
self.setup_required = self.auto_setup
63 changes: 46 additions & 17 deletions mopidy_pandora/doubleclick.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

import time

from mopidy.internal import encoding

from pandora.errors import PandoraException

from mopidy_pandora.library import PandoraUri

logger = logging.getLogger(__name__)
Expand All @@ -14,56 +18,81 @@ def __init__(self, config, client):
self.on_pause_previous_click = config["on_pause_previous_click"]
self.double_click_interval = config['double_click_interval']
self.client = client
self.click_time = 0
self._click_time = 0

def set_click_time(self, click_time=None):
if click_time is None:
self._click_time = time.time()
else:
self._click_time = click_time

def set_click(self):
self.click_time = time.time()
def get_click_time(self):
return self._click_time

def is_double_click(self):
return time.time() - self.click_time < float(self.double_click_interval)

double_clicked = self._click_time > 0 and time.time() - self._click_time < float(self.double_click_interval)

if double_clicked is False:
self._click_time = 0

return double_clicked

def on_change_track(self, active_track_uri, new_track_uri):
from mopidy_pandora.uri import PandoraUri

if not self.is_double_click():
return
return False

# TODO: Won't work if 'shuffle' or 'consume' modes are enabled
if active_track_uri is not None:

new_track_index = int(PandoraUri.parse(new_track_uri).index)
active_track_index = int(PandoraUri.parse(active_track_uri).index)

# TODO: the order of the tracks will no longer be sequential if the user has 'shuffled' the tracklist
# Need to find a better approach for determining whether 'next' or 'previous' was clicked.
if new_track_index > active_track_index or new_track_index == 0 and active_track_index == 2:
self.process_click(self.on_pause_next_click, active_track_uri)
return self.process_click(self.on_pause_next_click, active_track_uri)

elif new_track_index < active_track_index or new_track_index == active_track_index:
self.process_click(self.on_pause_previous_click, active_track_uri)
return self.process_click(self.on_pause_previous_click, active_track_uri)

return False

def on_resume_click(self, track_uri, time_position):
if not self.is_double_click() or time_position == 0:
return
return False

self.process_click(self.on_pause_resume_click, track_uri)
return self.process_click(self.on_pause_resume_click, track_uri)

def process_click(self, method, track_uri):

self.set_click_time(0)

uri = PandoraUri.parse(track_uri)
logger.info("Triggering event '%s' for song: %s", method, uri.name)

func = getattr(self, method)
func(uri.token)
self.click_time = 0

try:
func(uri.token)
except PandoraException as e:
logger.error('Error calling event: %s', encoding.locale_decode(e))
return False

return True

def thumbs_up(self, track_token):
self.client.add_feedback(track_token, True)
return self.client.add_feedback(track_token, True)

def thumbs_down(self, track_token):
self.client.add_feedback(track_token, False)
return self.client.add_feedback(track_token, False)

def sleep(self, track_token):
self.client.sleep_song(track_token)
return self.client.sleep_song(track_token)

def add_artist_bookmark(self, track_token):
self.client.add_artist_bookmark(track_token)
return self.client.add_artist_bookmark(track_token)

def add_song_bookmark(self, track_token):
self.client.add_song_bookmark(track_token)
return self.client.add_song_bookmark(track_token)
2 changes: 1 addition & 1 deletion mopidy_pandora/ext.conf
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ username =
password =
preferred_audio_quality = highQuality
sort_order = date
auto_set_repeat = true
auto_setup = true

### EXPERIMENTAL RATINGS IMPLEMENTATION ###
event_support_enabled = false
Expand Down
55 changes: 41 additions & 14 deletions mopidy_pandora/playback.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from threading import Thread

from mopidy import backend, models
from mopidy.internal import encoding

Expand All @@ -11,38 +13,54 @@


class PandoraPlaybackProvider(backend.PlaybackProvider):
SKIP_LIMIT = 3

def __init__(self, audio, backend):
super(PandoraPlaybackProvider, self).__init__(audio, backend)
self._station = None
self._station_iter = None
self.active_track_uri = None

if self.backend.auto_set_repeat:
# Make sure that tracks are being played in 'repeat mode'.
self.audio.set_about_to_finish_callback(self.callback).get()

def callback(self):
# TODO: add gapless playback when it is supported in Mopidy > 1.1
# self.audio.set_about_to_finish_callback(self.callback).get()

# def callback(self):
# See: https://discuss.mopidy.com/t/has-the-gapless-playback-implementation-been-completed-yet/784/2
# self.audio.set_uri(self.translate_uri(self.get_next_track())).get()

uri = self.backend.rpc_client.get_current_track_uri()
if uri is not None and uri.startswith("pandora:"):
self.backend.rpc_client.set_repeat()
def _auto_setup(self):

self.backend.rpc_client.set_repeat()
self.backend.rpc_client.set_consume(False)
self.backend.rpc_client.set_random(False)
self.backend.rpc_client.set_single(False)

self.backend.setup_required = False

def prepare_change(self):

if self.backend.auto_setup and self.backend.setup_required:
Thread(target=self._auto_setup).start()

super(PandoraPlaybackProvider, self).prepare_change()

def change_track(self, track):

if track.uri is None:
return False

track_uri = TrackUri.parse(track.uri)

station_id = PandoraUri.parse(track.uri).station_id

if not self._station or station_id != self._station.id:
# TODO: should be able to perform check on is_ad() once dynamic tracklist support is available
# if not self._station or (not track.is_ad() and station_id != self._station.id):
if self._station is None or (station_id != '' and station_id != self._station.id):
self._station = self.backend.api.get_station(station_id)
self._station_iter = iterate_forever(self._station.get_playlist)

try:
next_track = self.get_next_track(TrackUri.parse(track.uri).index)
next_track = self.get_next_track(track_uri.index)
if next_track:
return super(PandoraPlaybackProvider, self).change_track(next_track)
except requests.exceptions.RequestException as e:
Expand All @@ -67,7 +85,7 @@ def get_next_track(self, index):
else:
consecutive_track_skips += 1
logger.warning("Track with uri '%s' is not playable.", TrackUri.from_track(track).uri)
if consecutive_track_skips >= 4:
if consecutive_track_skips >= self.SKIP_LIMIT:
logger.error('Unplayable track skip limit exceeded!')
return None

Expand All @@ -84,13 +102,22 @@ def __init__(self, audio, backend):

def change_track(self, track):

self._double_click_handler.on_change_track(self.active_track_uri, track.uri)
return super(EventSupportPlaybackProvider, self).change_track(track)
event_processed = self._double_click_handler.on_change_track(self.active_track_uri, track.uri)
return_value = super(EventSupportPlaybackProvider, self).change_track(track)

if event_processed:
Thread(target=self.backend.rpc_client.resume_playback).start()

return return_value

def pause(self):
self._double_click_handler.set_click()

if self.get_time_position() > 0:
self._double_click_handler.set_click_time()

return super(EventSupportPlaybackProvider, self).pause()

def resume(self):
self._double_click_handler.on_resume_click(self.active_track_uri, self.get_time_position())

return super(EventSupportPlaybackProvider, self).resume()
Loading

0 comments on commit 85426fe

Please sign in to comment.