Initial Alexa integration

This commit is contained in:
Adam Lavin 2016-09-30 22:21:17 +01:00
parent d0a6022ed8
commit 14211f4ea4
4 changed files with 223 additions and 16 deletions

View file

@ -2,31 +2,71 @@
namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; namespace Poniverse\Ponyfm\Http\Controllers\Api\Web;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Session\Store;
use Poniverse\Ponyfm\Http\Controllers\Controller; use Poniverse\Ponyfm\Http\Controllers\Controller;
use Poniverse\Ponyfm\Models\AlexaSession;
use Poniverse\Ponyfm\Models\Track;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
class AlexaController extends Controller class AlexaController extends Controller
{ {
/**
* @var AlexaSession
*/
protected $session;
public function handle(Request $request, LoggerInterface $logger) public function handle(Request $request, LoggerInterface $logger)
{ {
$type = $request->json('request.type'); $type = $request->json('request.type');
$intent = $request->json('request.intent.name'); $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', [ $logger->debug('Incoming Alexa Request', [
'type' => $type, 'type' => $type,
'intent' => $intent '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) { switch ($type) {
case 'LaunchRequest': case 'LaunchRequest':
return $this->launch(); return $this->launch();
case 'PlayAudio'; case 'PlayAudio';
return $this->play(); return $this->play();
// case 'AudioPlayer.PlaybackNearlyFinished': case 'AudioPlayer.PlaybackNearlyFinished':
// return $this->queueNextTrack(); return $this->queueNextTrack();
case 'IntentRequest': case 'IntentRequest':
return $this->handleIntent($request); return $this->handleIntent($request);
default: default:
@ -41,8 +81,13 @@ class AlexaController extends Controller
switch ($intent) { switch ($intent) {
case 'AMAZON.PauseIntent': case 'AMAZON.PauseIntent':
return $this->stop(); return $this->stop();
case 'PlayAudio':
case 'AMAZON.ResumeIntent': case 'AMAZON.ResumeIntent':
return $this->play(); return $this->play();
case 'AMAZON.NextIntent':
return $this->queueNextTrack(true);
case 'AMAZON.PreviousIntent':
return $this->previousTrack();
case 'Author': case 'Author':
return $this->author(); return $this->author();
default: default:
@ -101,6 +146,11 @@ class AlexaController extends Controller
public function play() public function play()
{ {
$track = array_first(Track::popular(1));
$this->session->put('current_position', 1);
$this->session->put('track_id', $track['id']);
return [ return [
'version' => '1.0', 'version' => '1.0',
'sessionAttributes' => (object)[], 'sessionAttributes' => (object)[],
@ -111,8 +161,8 @@ class AlexaController extends Controller
'playBehavior' => 'REPLACE_ALL', 'playBehavior' => 'REPLACE_ALL',
'audioItem' => [ 'audioItem' => [
'stream' => [ 'stream' => [
'url' => 'https://pony.fm/t13840/stream.mp3', 'url' => $track['streams']['mp3'],
'token' => 't13840', 'token' => '1',
'offsetInMilliseconds' => 0, '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" => "
<speak>
You've reached the end of the popular tracks today. To start from the beginning say 'Alexa, ask pony fm to play'
</speak>
"],
'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 [ return [
'version' => '1.0', 'version' => '1.0',
'sessionAttributes' => (object)[], 'sessionAttributes' => (object)[],
@ -132,14 +242,47 @@ class AlexaController extends Controller
'directives' => [ 'directives' => [
[ [
'type' => 'AudioPlayer.Play', 'type' => 'AudioPlayer.Play',
'playBehavior' => 'ENQUEUE', 'playBehavior' => $replace ? 'REPLACE_ALL' : 'ENQUEUE',
'audioItem' => [ 'audioItem' => [
'stream' => [ 'stream' => $stream,
'url' => 'https://pony.fm/t13840/stream.mp3', ],
'token' => 't13840', ],
'expectedPreviousToken' => 'fma', ]
'offsetInMilliseconds' => 0, ],
], ];
}
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,
], ],
], ],
] ]

View file

@ -0,0 +1,30 @@
<?php
namespace Poniverse\Ponyfm\Models;
use Illuminate\Database\Eloquent\Model;
class AlexaSession extends Model
{
public $incrementing = false;
protected $table = 'alexa_session';
protected $casts = [
'payload' => '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;
}
}

View file

@ -287,12 +287,12 @@ class Track extends Model implements Searchable, Commentable, Favouritable
* @param integer $count * @param integer $count
* @return array * @return array
*/ */
public static function popular($count, $allowExplicit = false) public static function popular($count, $allowExplicit = false, $skip = 0)
{ {
$trackIds = Cache::remember( $trackIds = Cache::remember(
'popular_tracks'.$count.'-'.($allowExplicit ? 'explicit' : 'safe'), 'popular_tracks'.$count.'-'.($allowExplicit ? 'explicit' : 'safe'),
5, 5,
function () use ($allowExplicit, $count) { function () use ($allowExplicit, $count, $skip) {
$query = static $query = static
::published() ::published()
->listed() ->listed()
@ -308,6 +308,7 @@ class Track extends Model implements Searchable, Commentable, Favouritable
) )
->groupBy(['id', 'track_id']) ->groupBy(['id', 'track_id'])
->orderBy('plays', 'desc') ->orderBy('plays', 'desc')
->skip($skip)
->take($count); ->take($count);
if (!$allowExplicit) { if (!$allowExplicit) {
@ -455,7 +456,7 @@ class Track extends Model implements Searchable, Commentable, Favouritable
'original' => $track->getCoverUrl(Image::ORIGINAL) 'original' => $track->getCoverUrl(Image::ORIGINAL)
], ],
'streams' => [ '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, '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 'ogg' => (Config::get('app.debug') || is_file($track->getFileFor('OGG Vorbis'))) ? $track->getStreamUrl('OGG Vorbis') : null
], ],

View file

@ -0,0 +1,33 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateAlexaSession extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('alexa_session', function(Blueprint $table)
{
$table->string('id')->unique();
$table->text('payload');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('alexa_session');
}
}