Skip to content

Commit

Permalink
Merge pull request #146 from jaedb/develop
Browse files Browse the repository at this point in the history
2.10.0
  • Loading branch information
jaedb committed Sep 22, 2016
2 parents 3540593 + ab3b5b7 commit ffbed1a
Show file tree
Hide file tree
Showing 108 changed files with 3,688 additions and 3,767 deletions.
74 changes: 19 additions & 55 deletions mopidy_spotmop/__init__.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
from __future__ import unicode_literals

import logging
import os
import logging, os, json
import tornado.web
import tornado.websocket
import json

from services.upgrade import upgrade
from services.pusher import pusher
from services.auth import auth
from services.queuer import queuer
from mopidy import config, ext

__version__ = '2.9.2'
__ext_name__ = 'spotmop'
__verbosemode__ = False
from frontend import SpotmopFrontend

logger = logging.getLogger(__name__)
__version__ = '2.10.0'

##
# Core extension class
#
# Loads config and gets the party started. Initiates any additional frontends, etc.
##
class SpotmopExtension( ext.Extension ):

class SpotmopExtension(ext.Extension):
dist_name = 'Mopidy-Spotmop'
ext_name = __ext_name__
ext_name = 'spotmop'
version = __version__

def get_default_config(self):
Expand All @@ -34,59 +31,26 @@ def get_config_schema(self):
return schema

def setup(self, registry):

# Add web extension
registry.add('http:app', {
'name': self.ext_name,
'factory': spotmop_client_factory
'factory': factory
})

logger.info('Starting Spotmop web client '+ self.version)

class ArtworkHandler(tornado.web.RequestHandler):
def get(self, file):
self.write("You requested the file " + file)

def spotmop_client_factory(config, core):
# add our frontend
registry.add('frontend', SpotmopFrontend)

def factory(config, core):

# TODO create minified version of the project for production (or use Bower or Grunt for building??)
environment = 'dev' if config.get(__ext_name__)['debug'] is True else 'prod'
spotmoppath = os.path.join( os.path.dirname(__file__), 'static')

# PUSHER: TODO: need to fire this up from within the PusherHandler class... somehow
pusherport = str(config['spotmop']['pusherport'])
application = tornado.web.Application([
('/pusher', pusher.PusherHandler, {
'version': __version__
}),
])
application.listen(pusherport)

logger.info( 'Pusher server running on []:'+ str(pusherport) )
path = os.path.join( os.path.dirname(__file__), 'static')

return [
(r'/upgrade', upgrade.UpgradeRequestHandler, {
'core': core,
'config': config,
'version': __version__
}),
(r'/pusher/([^/]+)', pusher.PusherRequestHandler, {
'core': core,
'config': config
}),
(r'/auth', auth.AuthRequestHandler, {
'core': core,
'config': config
}),
(r'/queuer/([^/]*)', queuer.QueuerRequestHandler, {
'core': core,
'config': config
}),
(r"/images/(.*)", tornado.web.StaticFileHandler, {
"path": config['local-images']['image_dir']
}),
(r'/(.*)', tornado.web.StaticFileHandler, {
"path": spotmoppath,
"path": path,
"default_filename": "index.html"
}),
]
244 changes: 244 additions & 0 deletions mopidy_spotmop/frontend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
from __future__ import unicode_literals

import logging, json, pykka, pylast, pusher, urllib, urllib2, os, sys, mopidy_spotmop, subprocess
import tornado.web
import tornado.websocket
import tornado.ioloop
from mopidy import config, ext
from mopidy.core import CoreListener
from pkg_resources import parse_version
from spotipy import Spotify

# import logger
logger = logging.getLogger(__name__)


###
# Spotmop supporting frontend
#
# This provides a wrapping thread for the Pusher websocket, as well as the radio infrastructure
##
class SpotmopFrontend(pykka.ThreadingActor, CoreListener):

def __init__(self, config, core):
global spotmop
super(SpotmopFrontend, self).__init__()
self.config = config
self.core = core
self.version = mopidy_spotmop.__version__
self.is_root = ( os.geteuid() == 0 )
self.spotify_token = False
self.radio = {
"enabled": 0,
"seed_artists": [],
"seed_genres": [],
"seed_tracks": []
}

def on_start(self):

logger.info('Starting Spotmop '+self.version)

# try and start a pusher server
port = str(self.config['spotmop']['pusherport'])
try:
self.pusher = tornado.web.Application([( '/pusher', pusher.PusherWebsocketHandler, { 'frontend': self } )])
self.pusher.listen(port)
logger.info('Pusher server running at [0.0.0.0]:'+port)

except( pylast.NetworkError, pylast.MalformedResponseError, pylast.WSError ) as e:
logger.error('Error starting Pusher: %s', e)
self.stop()

# get a fresh spotify authentication token and store for future use
self.refresh_spotify_token()

##
# Listen for core events, and update our frontend as required
##
def track_playback_ended( self, tl_track, time_position ):
self.check_for_radio_update()


##
# See if we need to perform updates to our radio
#
# We see if we've got one or two tracks left, if so, go get some more
##
def check_for_radio_update( self ):
try:
tracklistLength = self.core.tracklist.length.get()
if( tracklistLength <= 5 and self.radio['enabled'] == 1 ):
self.load_more_tracks()

except RuntimeError:
logger.warning('RadioHandler: Could not fetch tracklist length')
pass


##
# Load some more radio tracks
#
# We need to build a Spotify authentication token first, and then fetch recommendations
##
def load_more_tracks( self ):

# this is crude, but it means we don't need to handle expired tokens
# TODO: address this when it's clear what Jodal and the team want to do with Pyspotify
self.refresh_spotify_token()

try:
token = self.spotify_token
token = token['access_token']
except:
logger.error('SpotmopFrontend: access_token missing or invalid')

try:
spotify = Spotify( auth = token )
response = spotify.recommendations(seed_artists = self.radio['seed_artists'], seed_genres = self.radio['seed_genres'], seed_tracks = self.radio['seed_tracks'], limit = 5)

uris = []
for track in response['tracks']:
uris.append( track['uri'] )

self.core.tracklist.add( uris = uris )
except:
logger.error('SpotmopFrontend: Failed to fetch recommendations from Spotify')


##
# Start radio
#
# Take the provided radio details, and start a new radio process
##
def start_radio( self, new_state ):

# TODO: validate payload has the required seed values

# set our new radio state
self.radio = new_state
self.radio['enabled'] = 1;

# clear all tracks
self.core.tracklist.clear()

# explicitly set consume, to ensure we don't end up with a huge tracklist (and it's how a radio should 'feel')
self.core.tracklist.set_consume( True )

# load me some tracks, and start playing!
self.load_more_tracks()
self.core.playback.play()

# notify clients
pusher.broadcast( 'radio_started', { 'radio': self.radio })

# return new radio state to initial call
return self.radio

##
# Stop radio
##
def stop_radio( self ):

# reset radio
self.radio = {
"enabled": 0,
"seed_artists": [],
"seed_genres": [],
"seed_tracks": []
}

# stop track playback
self.core.playback.stop()

# notify clients
pusher.broadcast( 'radio_stopped', { 'radio': self.radio })

# return new radio state to initial call
return self.radio


##
# Get a new spotify authentication token
#
# Uses the Client Credentials Flow, so is invisible to the user. We need this token for
# any backend spotify requests (we don't tap in to Mopidy-Spotify, yet). Also used for
# passing token to frontend for javascript requests without use of the Authorization Code Flow.
##
def refresh_spotify_token( self ):

url = 'https://accounts.spotify.com/api/token'
authorization = 'YTg3ZmI0ZGJlZDMwNDc1YjhjZWMzODUyM2RmZjUzZTI6ZDdjODlkMDc1M2VmNDA2OGJiYTE2NzhjNmNmMjZlZDY='

headers = {'Authorization' : 'Basic ' + authorization}
data = {'grant_type': 'client_credentials'}
data_encoded = urllib.urlencode( data )
req = urllib2.Request(url, data_encoded, headers)

try:
response = urllib2.urlopen(req, timeout=30).read()
response_dict = json.loads(response)
self.spotify_token = response_dict
return response_dict
except urllib2.HTTPError as e:
return e


# get our spotify token
def get_spotify_token( self ):
return self.spotify_token


##
# Get Spotmop version, and check for updates
#
# We compare our version with the latest available on PyPi
##
def get_version( self ):

url = 'https://pypi.python.org/pypi/Mopidy-Spotmop/json'
req = urllib2.Request(url)

try:
response = urllib2.urlopen(req, timeout=30).read()
response = json.loads(response)
latest_version = response['info']['version']
except urllib2.HTTPError as e:
latest_version = False

# compare our versions, and convert result to boolean
upgrade_available = cmp( parse_version( latest_version ), parse_version( self.version ) )
upgrade_available = ( upgrade_available == 1 )

# prepare our response
data = {
'current': self.version,
'latest': latest_version,
'is_root': self.is_root,
'upgrade_available': upgrade_available
}
return data


##
# Upgrade Spotmop module
#
# Upgrade myself to the latest version available on PyPi
##
def perform_upgrade( self ):
try:
subprocess.check_call(["pip", "install", "--upgrade", "Mopidy-Spotmop"])
return True
except subprocess.CalledProcessError:
return False

##
# Restart Mopidy
#
# This is untested and may require installation of an upstart script to properly restart
##
def restart( self ):
os.execl(sys.executable, *([sys.executable]+sys.argv))



Loading

0 comments on commit ffbed1a

Please sign in to comment.