diff --git a/app/Http/Controllers/Api/Web/AlexaController.php b/app/Http/Controllers/Api/Web/AlexaController.php new file mode 100644 index 00000000..69651c31 --- /dev/null +++ b/app/Http/Controllers/Api/Web/AlexaController.php @@ -0,0 +1,308 @@ +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', [ + '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 '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 '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: + 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() + { + $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)[], + 'response' => [ + 'directives' => [ + [ + 'type' => 'AudioPlayer.Play', + 'playBehavior' => 'REPLACE_ALL', + 'audioItem' => [ + 'stream' => [ + 'url' => $track['streams']['mp3'], + 'token' => '1', + 'offsetInMilliseconds' => 0, + ], + ], + ], + ], + 'shouldEndSession' => true, + ], + ]; + } + + 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)[], + 'response' => [ + 'directives' => [ + [ + 'type' => 'AudioPlayer.Play', + 'playBehavior' => $replace ? 'REPLACE_ALL' : 'ENQUEUE', + 'audioItem' => [ + '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, + ], + ], + ] + ], + ]; + } + + public function stop() + { + return [ + 'version' => '1.0', + 'sessionAttributes' => (object)[], + 'response' => [ + 'directives' => [ + [ + 'type' => 'AudioPlayer.Stop' + ], + ], + 'shouldEndSession' => true, + ], + ]; + } +} 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'); + } +} 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');