From d0a6022ed8219ce60a2cd11150497a27bec5c057 Mon Sep 17 00:00:00 2001 From: Adam Lavin Date: Fri, 30 Sep 2016 02:36:44 +0100 Subject: [PATCH 1/2] Added basic hardcoded alexa endpoints --- .../Controllers/Api/Web/AlexaController.php | 165 ++++++++++++++++++ routes/web.php | 2 + 2 files changed, 167 insertions(+) create mode 100644 app/Http/Controllers/Api/Web/AlexaController.php diff --git a/app/Http/Controllers/Api/Web/AlexaController.php b/app/Http/Controllers/Api/Web/AlexaController.php new file mode 100644 index 00000000..cab5f158 --- /dev/null +++ b/app/Http/Controllers/Api/Web/AlexaController.php @@ -0,0 +1,165 @@ +json('request.type'); + $intent = $request->json('request.intent.name'); + + $logger->debug('Incoming Alexa Request', [ + 'type' => $type, + 'intent' => $intent + ]); + + $logger->debug('Incoming Alexa Full Request', $request->json()->all()); + + switch ($type) { + case 'LaunchRequest': + return $this->launch(); + case 'PlayAudio'; + return $this->play(); +// case 'AudioPlayer.PlaybackNearlyFinished': +// return $this->queueNextTrack(); + case 'IntentRequest': + return $this->handleIntent($request); + default: + return response()->make('', 204); + } + } + + public function handleIntent(Request $request) + { + $intent = $request->json('request.intent.name'); + + switch ($intent) { + case 'AMAZON.PauseIntent': + return $this->stop(); + case 'AMAZON.ResumeIntent': + return $this->play(); + case 'Author': + return $this->author(); + default: + return response()->make('', 204); + } + } + + public function launch() + { + return [ + 'version' => '1.0', + 'sessionAttributes' => (object)[], + 'response' => [ + "outputSpeech" => [ + "type" => "SSML", + "ssml" => "If you want to play music, say 'Alexa, ask pony fm to play'" + ], + 'shouldEndSession' => true, + ], + ]; + } + + public function unknown() + { + return [ + 'version' => '1.0', + 'sessionAttributes' => (object)[], + 'response' => [ + "outputSpeech" => [ + "type" => "SSML", + "ssml" => "Sorry, I don't recognise that command." + ], + 'shouldEndSession' => true, + ], + ]; + } + + public function author() + { + return [ + 'version' => '1.0', + 'sessionAttributes' => (object)[], + 'response' => [ + "outputSpeech" => [ + "type" => "SSML", + "ssml" => " + + Pony.fm was built by Pixel Wavelength for Viola to keep all her music in one place. + + " + ], + 'shouldEndSession' => true, + ], + ]; + } + + public function play() + { + return [ + 'version' => '1.0', + 'sessionAttributes' => (object)[], + 'response' => [ + 'directives' => [ + [ + 'type' => 'AudioPlayer.Play', + 'playBehavior' => 'REPLACE_ALL', + 'audioItem' => [ + 'stream' => [ + 'url' => 'https://pony.fm/t13840/stream.mp3', + 'token' => 't13840', + 'offsetInMilliseconds' => 0, + ], + ], + ], + ], + 'shouldEndSession' => true, + ], + ]; + } + + public function queueNextTrack() + { + return [ + 'version' => '1.0', + 'sessionAttributes' => (object)[], + 'response' => [ + 'directives' => [ + [ + 'type' => 'AudioPlayer.Play', + 'playBehavior' => 'ENQUEUE', + 'audioItem' => [ + 'stream' => [ + 'url' => 'https://pony.fm/t13840/stream.mp3', + 'token' => 't13840', + 'expectedPreviousToken' => 'fma', + 'offsetInMilliseconds' => 0, + ], + ], + ], + ] + ], + ]; + } + + public function stop() + { + return [ + 'version' => '1.0', + 'sessionAttributes' => (object)[], + 'response' => [ + 'directives' => [ + [ + 'type' => 'AudioPlayer.Stop' + ], + ], + 'shouldEndSession' => true, + ], + ]; + } +} diff --git a/routes/web.php b/routes/web.php index 960b7034..284d0b9e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -87,6 +87,8 @@ Route::group(['prefix' => 'api/v1', 'middleware' => 'json-exceptions'], function Route::group(['prefix' => 'api/web'], function () { + Route::post('/alexa', 'Api\Web\AlexaController@handle'); + Route::get('/taxonomies/all', 'Api\Web\TaxonomiesController@getAll'); Route::get('/search', 'Api\Web\SearchController@getSearch'); From 14211f4ea479c9b3e31732316b2c7cbdff569e05 Mon Sep 17 00:00:00 2001 From: Adam Lavin Date: Fri, 30 Sep 2016 22:21:17 +0100 Subject: [PATCH 2/2] Initial Alexa integration --- .../Controllers/Api/Web/AlexaController.php | 169 ++++++++++++++++-- app/Models/AlexaSession.php | 30 ++++ app/Models/Track.php | 7 +- ...2016_09_30_202330_create_alexa_session.php | 33 ++++ 4 files changed, 223 insertions(+), 16 deletions(-) create mode 100644 app/Models/AlexaSession.php create mode 100644 database/migrations/2016_09_30_202330_create_alexa_session.php diff --git a/app/Http/Controllers/Api/Web/AlexaController.php b/app/Http/Controllers/Api/Web/AlexaController.php index cab5f158..69651c31 100644 --- a/app/Http/Controllers/Api/Web/AlexaController.php +++ b/app/Http/Controllers/Api/Web/AlexaController.php @@ -2,31 +2,71 @@ namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Session\Store; use Poniverse\Ponyfm\Http\Controllers\Controller; +use Poniverse\Ponyfm\Models\AlexaSession; +use Poniverse\Ponyfm\Models\Track; use Psr\Log\LoggerInterface; class AlexaController extends Controller { + /** + * @var AlexaSession + */ + protected $session; + public function handle(Request $request, LoggerInterface $logger) { $type = $request->json('request.type'); $intent = $request->json('request.intent.name'); + $sessId = $request->json('session.user.userId', $request->json('context.System.user.userId')); + + if ($sessId) { + $this->session = AlexaSession::find($sessId); + + if (!$this->session) { + $this->session = new AlexaSession(); + $this->session->id = $sessId; + } + } + $logger->debug('Incoming Alexa Request', [ 'type' => $type, 'intent' => $intent ]); - $logger->debug('Incoming Alexa Full Request', $request->json()->all()); + $logger->debug('Incoming Alexa Full Request', [ + 'json' => json_encode($request->json()->all(), JSON_PRETTY_PRINT) + ]); + + /** @var JsonResponse $response */ + $response = $this->handleType($request); + + if ($response instanceof JsonResponse) { + $logger->debug('Alexa Response', ['json' => $response->getContent()]); + } + + if ($this->session) { + $this->session->save(); + } + + return $response; + } + + public function handleType(Request $request) + { + $type = $request->json('request.type'); switch ($type) { case 'LaunchRequest': return $this->launch(); case 'PlayAudio'; return $this->play(); -// case 'AudioPlayer.PlaybackNearlyFinished': -// return $this->queueNextTrack(); + case 'AudioPlayer.PlaybackNearlyFinished': + return $this->queueNextTrack(); case 'IntentRequest': return $this->handleIntent($request); default: @@ -41,8 +81,13 @@ class AlexaController extends Controller switch ($intent) { case 'AMAZON.PauseIntent': return $this->stop(); + case 'PlayAudio': case 'AMAZON.ResumeIntent': return $this->play(); + case 'AMAZON.NextIntent': + return $this->queueNextTrack(true); + case 'AMAZON.PreviousIntent': + return $this->previousTrack(); case 'Author': return $this->author(); default: @@ -101,6 +146,11 @@ class AlexaController extends Controller public function play() { + $track = array_first(Track::popular(1)); + + $this->session->put('current_position', 1); + $this->session->put('track_id', $track['id']); + return [ 'version' => '1.0', 'sessionAttributes' => (object)[], @@ -111,8 +161,8 @@ class AlexaController extends Controller 'playBehavior' => 'REPLACE_ALL', 'audioItem' => [ 'stream' => [ - 'url' => 'https://pony.fm/t13840/stream.mp3', - 'token' => 't13840', + 'url' => $track['streams']['mp3'], + 'token' => '1', 'offsetInMilliseconds' => 0, ], ], @@ -123,8 +173,68 @@ class AlexaController extends Controller ]; } - public function queueNextTrack() + public function queueNextTrack($replace = false) { + $trackId = $this->session->get('track_id'); + $position = $this->session->get('current_position', 1); + $trackHistory = $this->session->get('track_history', []); + $playlist = $this->session->get('playlist', []); + $playlistNum = $this->session->get('playlist-num', 1); + + if (count($playlist) === 0) { + $playlist = Track::popular(30); + + $this->session->put('playlist', $playlist); + } + + if ($position === 30) { + $playlist = Track::popular(30, false, $playlistNum * 30); + + $position = 1; + $playlistNum++; + } + + if (count($playlist) === 0) { + return [ + 'version' => '1.0', + 'sessionAttributes' => (object)[], + 'response' => [ + "outputSpeech" => [ + "type" => "SSML", + "ssml" => " + + You've reached the end of the popular tracks today. To start from the beginning say 'Alexa, ask pony fm to play' + + "], + 'directives' => [ + [ + 'type' => 'AudioPlayer.Stop' + ], + ], + 'shouldEndSession' => true, + ], + ]; + } + + $track = $playlist[$position-1]; + + $trackHistory[] = $trackId; + + $this->session->put('current_position', $position + 1); + $this->session->put('track_id', $track['id']); + $this->session->put('track_history', $trackHistory); + $this->session->put('playlist-num', $playlistNum); + + $stream = [ + 'url' => $track['streams']['mp3'], + 'token' => $track['id'], + 'offsetInMilliseconds' => 0, + ]; + + if (!$replace) { + $stream['expectedPreviousToken'] = $trackId; + } + return [ 'version' => '1.0', 'sessionAttributes' => (object)[], @@ -132,14 +242,47 @@ class AlexaController extends Controller 'directives' => [ [ 'type' => 'AudioPlayer.Play', - 'playBehavior' => 'ENQUEUE', + 'playBehavior' => $replace ? 'REPLACE_ALL' : 'ENQUEUE', 'audioItem' => [ - 'stream' => [ - 'url' => 'https://pony.fm/t13840/stream.mp3', - 'token' => 't13840', - 'expectedPreviousToken' => 'fma', - 'offsetInMilliseconds' => 0, - ], + 'stream' => $stream, + ], + ], + ] + ], + ]; + } + + public function previousTrack() + { + $trackId = $this->session->get('track_id'); + $position = $this->session->get('current_position', 1); + $trackHistory = $this->session->get('track_history', []); + $playlist = $this->session->get('playlist', []); + + $track = $playlist[$position-2]; + + $trackHistory[] = $trackId; + + $this->session->put('current_position', $position - 1); + $this->session->put('track_id', $track['id']); + $this->session->put('track_history', $trackHistory); + + $stream = [ + 'url' => $track['streams']['mp3'], + 'token' => $track['id'], + 'offsetInMilliseconds' => 0, + ]; + + return [ + 'version' => '1.0', + 'sessionAttributes' => (object)[], + 'response' => [ + 'directives' => [ + [ + 'type' => 'AudioPlayer.Play', + 'playBehavior' => 'REPLACE_ALL', + 'audioItem' => [ + 'stream' => $stream, ], ], ] diff --git a/app/Models/AlexaSession.php b/app/Models/AlexaSession.php new file mode 100644 index 00000000..b1628435 --- /dev/null +++ b/app/Models/AlexaSession.php @@ -0,0 +1,30 @@ + 'array' + ]; + + public function put($key, $value) + { + $payload = $this->payload; + + $payload[$key] = $value; + + $this->payload = $payload; + } + + public function get($key, $default = null) + { + return $this->payload[$key] ?? $default; + } +} diff --git a/app/Models/Track.php b/app/Models/Track.php index e9211ca6..c3bfc141 100644 --- a/app/Models/Track.php +++ b/app/Models/Track.php @@ -287,12 +287,12 @@ class Track extends Model implements Searchable, Commentable, Favouritable * @param integer $count * @return array */ - public static function popular($count, $allowExplicit = false) + public static function popular($count, $allowExplicit = false, $skip = 0) { $trackIds = Cache::remember( 'popular_tracks'.$count.'-'.($allowExplicit ? 'explicit' : 'safe'), 5, - function () use ($allowExplicit, $count) { + function () use ($allowExplicit, $count, $skip) { $query = static ::published() ->listed() @@ -308,6 +308,7 @@ class Track extends Model implements Searchable, Commentable, Favouritable ) ->groupBy(['id', 'track_id']) ->orderBy('plays', 'desc') + ->skip($skip) ->take($count); if (!$allowExplicit) { @@ -455,7 +456,7 @@ class Track extends Model implements Searchable, Commentable, Favouritable 'original' => $track->getCoverUrl(Image::ORIGINAL) ], 'streams' => [ - 'mp3' => $track->getStreamUrl('MP3'), + 'mp3' => str_replace('http://adamlav.in', 'https://pony.fm', str_replace('http://ponyfm-dev.poni','https://pony.fm', $track->getStreamUrl('MP3'))), 'aac' => (!Config::get('app.debug') || is_file($track->getFileFor('AAC'))) ? $track->getStreamUrl('AAC') : null, 'ogg' => (Config::get('app.debug') || is_file($track->getFileFor('OGG Vorbis'))) ? $track->getStreamUrl('OGG Vorbis') : null ], diff --git a/database/migrations/2016_09_30_202330_create_alexa_session.php b/database/migrations/2016_09_30_202330_create_alexa_session.php new file mode 100644 index 00000000..9db31910 --- /dev/null +++ b/database/migrations/2016_09_30_202330_create_alexa_session.php @@ -0,0 +1,33 @@ +string('id')->unique(); + $table->text('payload'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('alexa_session'); + } +}