mirror of
https://github.com/Poniverse/Pony.fm.git
synced 2024-11-22 13:07:59 +01:00
Merge branch 'feature/alexa'
This commit is contained in:
commit
95a209aeab
5 changed files with 377 additions and 3 deletions
308
app/Http/Controllers/Api/Web/AlexaController.php
Normal file
308
app/Http/Controllers/Api/Web/AlexaController.php
Normal file
|
@ -0,0 +1,308 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
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', [
|
||||||
|
'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" => "<speak>If you want to play music, say 'Alexa, ask pony fm to play'</speak>"
|
||||||
|
],
|
||||||
|
'shouldEndSession' => true,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unknown()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'version' => '1.0',
|
||||||
|
'sessionAttributes' => (object)[],
|
||||||
|
'response' => [
|
||||||
|
"outputSpeech" => [
|
||||||
|
"type" => "SSML",
|
||||||
|
"ssml" => "<speak>Sorry, I don't recognise that command.</speak>"
|
||||||
|
],
|
||||||
|
'shouldEndSession' => true,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function author()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'version' => '1.0',
|
||||||
|
'sessionAttributes' => (object)[],
|
||||||
|
'response' => [
|
||||||
|
"outputSpeech" => [
|
||||||
|
"type" => "SSML",
|
||||||
|
"ssml" => "
|
||||||
|
<speak>
|
||||||
|
Pony.fm was built by Pixel Wavelength for Viola to keep all her music in one place.
|
||||||
|
</speak>
|
||||||
|
"
|
||||||
|
],
|
||||||
|
'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" => "
|
||||||
|
<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 [
|
||||||
|
'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,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
30
app/Models/AlexaSession.php
Normal file
30
app/Models/AlexaSession.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
],
|
],
|
||||||
|
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
|
@ -87,6 +87,8 @@ Route::group(['prefix' => 'api/v1', 'middleware' => 'json-exceptions'], function
|
||||||
|
|
||||||
|
|
||||||
Route::group(['prefix' => 'api/web'], function () {
|
Route::group(['prefix' => 'api/web'], function () {
|
||||||
|
Route::post('/alexa', 'Api\Web\AlexaController@handle');
|
||||||
|
|
||||||
Route::get('/taxonomies/all', 'Api\Web\TaxonomiesController@getAll');
|
Route::get('/taxonomies/all', 'Api\Web\TaxonomiesController@getAll');
|
||||||
Route::get('/search', 'Api\Web\SearchController@getSearch');
|
Route::get('/search', 'Api\Web\SearchController@getSearch');
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue