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');