#8: Implemented the track upload API.

This commit is contained in:
Peter Deltchev 2015-11-09 11:35:30 -08:00
parent cf7ec3b6cf
commit 4d119ff758
22 changed files with 647 additions and 54 deletions

View file

@ -70,7 +70,12 @@ class UploadTrackCommand extends CommandBase
public function execute()
{
$user = \Auth::user();
$trackFile = \Input::file('track');
$trackFile = \Input::file('track', null);
if (null === $trackFile) {
return CommandResponse::fail(['track' => ['You must upload an audio file!']]);
}
$audio = \AudioCache::get($trackFile->getPathname());

View file

@ -1,14 +0,0 @@
<?php namespace Poniverse\Ponyfm;
use Illuminate\Database\Eloquent\ModelNotFoundException;
/**
* Class TrackFileNotFoundException
* @package Poniverse\Ponyfm
*
* This exception is used to indicate that the requested `TrackFile` object
* does not exist. This is useful when dealing with albums or playlists that
* contain tracks for which no lossless master is available (and thus, lossless
* `TrackFiles` don't exist for).
*/
class TrackFileNotFoundException extends ModelNotFoundException {}

View file

@ -0,0 +1,32 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2015 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\Exceptions;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* Class InvalidAccessTokenException
* @package Poniverse\Ponyfm
*
* This exception indicates that an access token we attempted to introspect
* through the Poniverse API is expired or otherwise unusable.
*/
class InvalidAccessTokenException extends AccessDeniedHttpException {};

View file

@ -0,0 +1,34 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2015 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\Exceptions;
use Illuminate\Database\Eloquent\ModelNotFoundException;
/**
* Class TrackFileNotFoundException
* @package Poniverse\Ponyfm
*
* This exception is used to indicate that the requested `TrackFile` object
* does not exist. This is useful when dealing with albums or playlists that
* contain tracks for which no lossless master is available (and thus, lossless
* `TrackFiles` don't exist for).
*/
class TrackFileNotFoundException extends ModelNotFoundException {}

View file

@ -20,13 +20,56 @@
namespace Poniverse\Ponyfm\Http\Controllers\Api\V1;
use Poniverse\Ponyfm\Commands\UploadTrackCommand;
use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase;
use Poniverse\Ponyfm\Image;
use Poniverse\Ponyfm\Track;
use Cover;
use Illuminate\Support\Facades\Response;
use Response;
class TracksController extends \ApiControllerBase
class TracksController extends ApiControllerBase
{
public function postUploadTrack() {
session_write_close();
$response = $this->execute(new UploadTrackCommand());
$commandData = $response->getData(true);
if (200 !== $response->getStatusCode()) {
return $response;
}
$data = [
'id' => $commandData['id'],
'status_url' => action('Api\V1\TracksController@getUploadStatus', ['id' => $commandData['id']]),
'message' => "This track has been accepted for processing! Poll the status_url to know when it's ready to publish.",
];
$response->setData($data);
$response->setStatusCode(202);
return $response;
}
public function getUploadStatus($trackId) {
$track = Track::findOrFail($trackId);
$this->authorize('edit', $track);
if ($track->status === Track::STATUS_PROCESSING) {
return Response::json(['message' => 'Processing...'], 202);
} elseif ($track->status === Track::STATUS_COMPLETE) {
return Response::json([
'message' => 'Processing complete! The artist must publish the track by visiting its edit_url.',
'edit_url' => action('ContentController@getTracks', ['id' => $trackId])
], 201);
} else {
// something went wrong
return Response::json(['error' => 'Processing failed!'], 500);
}
}
public function getTrackRadioDetails($hash)
{
$track = Track

View file

@ -21,19 +21,19 @@
namespace Poniverse\Ponyfm\Http\Controllers\Api\Web;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\File;
use File;
use Poniverse\Ponyfm\Exceptions\InvalidEncodeOptionsException;
use Poniverse\Ponyfm\Commands\DeleteTrackCommand;
use Poniverse\Ponyfm\Commands\EditTrackCommand;
use Poniverse\Ponyfm\Commands\UploadTrackCommand;
use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase;
use Poniverse\Ponyfm\Jobs\EncodeTrackFile;
use Poniverse\Ponyfm\ResourceLogItem;
use Poniverse\Ponyfm\Track;
use Cover;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Input;
use Illuminate\Support\Facades\Response;
use Poniverse\Ponyfm\TrackFile;
use Poniverse\Ponyfm\Track;
use Auth;
use Input;
use Response;
class TracksController extends ApiControllerBase
{
@ -44,16 +44,15 @@ class TracksController extends ApiControllerBase
try {
return $this->execute(new UploadTrackCommand());
} catch (\InvalidEncodeOptions $e) {
} catch (InvalidEncodeOptionsException $e) {
}
}
public function getUploadStatus($trackId)
{
// TODO: authorize this
$track = Track::findOrFail($trackId);
$this->authorize('edit', $track);
if ($track->status === Track::STATUS_PROCESSING){
return Response::json(['message' => 'Processing...'], 202);
@ -65,7 +64,6 @@ class TracksController extends ApiControllerBase
// something went wrong
return Response::json(['error' => 'Processing failed!'], 500);
}
}
public function postDelete($id)

View file

@ -25,6 +25,13 @@ use Response;
abstract class ApiControllerBase extends Controller
{
/**
* NOTE: This function is used by the v1 API. If the response JSON format
* it returns changes, don't break the API!
*
* @param CommandBase $command
* @return \Illuminate\Http\JsonResponse
*/
protected function execute(CommandBase $command)
{
if (!$command->authorize()) {

View file

@ -96,25 +96,13 @@ class AuthController extends Controller
}
// Check by login name to see if they already have an account
$localMember = User::where('username', '=', $poniverseUser['username'])
->where('is_archived', false)
->first();
$user = User::findOrCreate($poniverseUser['username'], $poniverseUser['display_name'], $poniverseUser['email']);
if ($localMember) {
return $this->loginRedirect($localMember);
if ($user->wasRecentlyCreated) {
return $this->loginRedirect($user);
}
$user = new User;
$user->username = $poniverseUser['username'];
$user->display_name = $poniverseUser['display_name'];
$user->email = $poniverseUser['email'];
$user->created_at = gmdate("Y-m-d H:i:s", time());
$user->uses_gravatar = 1;
$user->save();
//We need to insert a new token row :O
// We need to insert a new token row :O
$setData['user_id'] = $user->id;
$setData['external_user_id'] = $poniverseUser['id'];

View file

@ -47,7 +47,9 @@ class Kernel extends HttpKernel
protected $routeMiddleware = [
'auth' => \Poniverse\Ponyfm\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.oauth' => \Poniverse\Ponyfm\Http\Middleware\AuthenticateOAuth::class,
'can' => \Poniverse\Ponyfm\Http\Middleware\Authorize::class,
'json-exceptions' => \Poniverse\Ponyfm\Http\Middleware\JsonExceptions::class,
'guest' => \Poniverse\Ponyfm\Http\Middleware\RedirectIfAuthenticated::class,
];
}

View file

@ -0,0 +1,75 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2015 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\Http\Middleware;
use Auth;
use Closure;
use GuzzleHttp;
use Poniverse;
use Poniverse\Ponyfm\User;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class AuthenticateOAuth
{
/**
* @var Poniverse
*/
private $poniverse;
public function __construct(Poniverse $poniverse) {
$this->poniverse = $poniverse;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string $requiredScope
* @return mixed
* @throws \OAuth2\Exception
*/
public function handle($request, Closure $next, $requiredScope)
{
// Ensure this is a valid OAuth client.
$accessToken = $request->get('access_token');
// check that access token is valid at Poniverse.net
$accessTokenInfo = $this->poniverse->getAccessTokenInfo($accessToken);
if (!$accessTokenInfo->getIsActive()) {
throw new AccessDeniedHttpException('This access token is expired or invalid!');
}
if (!in_array($requiredScope, $accessTokenInfo->getScopes())) {
throw new AccessDeniedHttpException("This access token lacks the '${requiredScope}' scope!");
}
// Log in as the given user, creating the account if necessary.
$this->poniverse->setAccessToken($accessToken);
$poniverseUser = $this->poniverse->getUser();
$user = User::findOrCreate($poniverseUser['username'], $poniverseUser['display_name'], $poniverseUser['email']);
Auth::login($user);
return $next($request);
}
}

View file

@ -0,0 +1,55 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2015 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\Http\Middleware;
use Closure;
use Symfony\Component\HttpKernel\Exception\HttpException;
/**
* Class JsonExceptions
* @package Poniverse\Ponyfm\Http\Middleware
*
* This middleware turns any HTTP exceptions thrown during the request
* into a JSON response. To be used when implementing the API!
*/
class JsonExceptions
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
try {
$response = $next($request);
} catch (HttpException $e) {
return \Response::json([
'message' => $e->getMessage()
], $e->getStatusCode());
}
return $response;
}
}

View file

@ -50,8 +50,9 @@ class Profiler
} catch (\Exception $e) {
$response = \Response::make([
'message' => $e->getMessage(),
'lineNumber' => $e->getLine(),
'exception' => $e->getTrace()
], 500);
], method_exists($e, 'getStatusCode') ? $e->getStatusCode() : 500);
$profiler->log('error', $e->__toString(), []);
}

View file

@ -30,6 +30,6 @@ class VerifyCsrfToken extends BaseVerifier
* @var array
*/
protected $except = [
//
'api/*'
];
}

View file

@ -66,11 +66,19 @@ Route::get('playlist/{id}-{slug}', 'PlaylistsController@getPlaylist');
Route::get('p{id}', 'PlaylistsController@getShortlink')->where('id', '\d+');
Route::get('p{id}/dl.{extension}', 'PlaylistsController@getDownload' );
Route::group(['prefix' => 'api/v1'], function() {
Route::group(['prefix' => 'api/v1', 'middleware' => 'json-exceptions'], function() {
Route::get('/tracks/radio-details/{hash}', 'Api\V1\TracksController@getTrackRadioDetails');
Route::post('/tracks/radio-details/{hash}', 'Api\V1\TracksController@getTrackRadioDetails');
Route::group(['middleware' => 'auth.oauth:ponyfm-upload-track'], function() {
Route::post('tracks', 'Api\V1\TracksController@postUploadTrack');
Route::get('/tracks/{id}/upload-status', 'Api\V1\TracksController@getUploadStatus');
});
});
Route::group(['prefix' => 'api/web'], function() {
Route::get('/taxonomies/all', 'Api\Web\TaxonomiesController@getAll');

View file

@ -0,0 +1,220 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2015 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse;
/**
* Class AccessTokenInfo
*
* A container for the fields in the draft OAuth Token Introspection proposal.
*
* @link https://tools.ietf.org/html/draft-richer-oauth-introspection-06
* @package Poniverse
*/
class AccessTokenInfo {
protected $token;
protected $isActive;
protected $expiresAt;
protected $issuedAt;
protected $scopes;
protected $clientId;
protected $sub;
protected $userId;
protected $intendedAudience;
protected $issuer;
protected $tokenType;
public function __construct($accessToken) {
$this->token = $accessToken;
}
/**
* @return mixed
*/
public function getToken() {
return $this->token;
}
/**
* @return bool
*/
public function getIsActive() {
return $this->isActive;
}
/**
* @param bool $isActive
* @return AccessTokenInfo
*/
public function setIsActive($isActive) {
$this->isActive = $isActive;
return $this;
}
/**
* @return mixed
*/
public function getExpiresAt() {
return $this->expiresAt;
}
/**
* @param mixed $expiresAt
* @return AccessTokenInfo
*/
public function setExpiresAt($expiresAt) {
$this->expiresAt = $expiresAt;
return $this;
}
/**
* @return mixed
*/
public function getIssuedAt() {
return $this->issuedAt;
}
/**
* @param mixed $issuedAt
* @return AccessTokenInfo
*/
public function setIssuedAt($issuedAt) {
$this->issuedAt = $issuedAt;
return $this;
}
/**
* @return array
*/
public function getScopes() {
return $this->scopes;
}
/**
* @param array|string $scopes
* @return AccessTokenInfo
*/
public function setScopes($scopes) {
if (is_array($scopes)) {
$this->scopes = $scopes;
} else {
$this->scopes = mb_split(' ', $scopes);
}
return $this;
}
/**
* @return mixed
*/
public function getClientId() {
return $this->clientId;
}
/**
* @param mixed $clientId
* @return AccessTokenInfo
*/
public function setClientId($clientId) {
$this->clientId = $clientId;
return $this;
}
/**
* @return mixed
*/
public function getSub() {
return $this->sub;
}
/**
* @param mixed $sub
* @return AccessTokenInfo
*/
public function setSub($sub) {
$this->sub = $sub;
return $this;
}
/**
* @return mixed
*/
public function getUserId() {
return $this->userId;
}
/**
* @param mixed $userId
* @return AccessTokenInfo
*/
public function setUserId($userId) {
$this->userId = $userId;
return $this;
}
/**
* @return mixed
*/
public function getIntendedAudience() {
return $this->intendedAudience;
}
/**
* @param mixed $intendedAudience
* @return AccessTokenInfo
*/
public function setIntendedAudience($intendedAudience) {
$this->intendedAudience = $intendedAudience;
return $this;
}
/**
* @return mixed
*/
public function getIssuer() {
return $this->issuer;
}
/**
* @param mixed $issuer
* @return AccessTokenInfo
*/
public function setIssuer($issuer) {
$this->issuer = $issuer;
return $this;
}
/**
* @return mixed
*/
public function getTokenType() {
return $this->tokenType;
}
/**
* @param mixed $tokenType
* @return AccessTokenInfo
*/
public function setTokenType($tokenType) {
$this->tokenType = $tokenType;
return $this;
}
}

View file

@ -18,6 +18,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
use OAuth2\Client;
use Poniverse\Ponyfm\Exceptions\InvalidAccessTokenException;
/**
* Class Poniverse
*
@ -111,4 +114,48 @@ class Poniverse {
return json_decode($result, true);
}
/**
* Gets information about the given access token.
*
* @link https://tools.ietf.org/html/draft-richer-oauth-introspection-06
*
* @param $accessTokenToIntrospect
* @return \Poniverse\AccessTokenInfo
* @throws \Poniverse\Ponyfm\InvalidAccessTokenException
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
*/
public function getAccessTokenInfo($accessTokenToIntrospect)
{
$token = $this->client->getAccessToken(
Config::get('poniverse.urls.token'),
Client::GRANT_TYPE_CLIENT_CREDENTIALS,
[]
)['result']['access_token'];
$request = \Httpful\Request::post($this->urls['api']. 'meta/introspect?token='.$accessTokenToIntrospect);
/** @var Httpful\Response $result */
$result = $request
->addHeader('Accept', 'application/json')
->addHeader('Authorization', 'Bearer '.$token)
->send();
$data = json_decode($result, true);
if (404 === $result->code) {
throw new InvalidAccessTokenException('This access token is expired or invalid!');
}
if (200 !== $result->code) {
throw new \Symfony\Component\HttpKernel\Exception\HttpException(500, 'An unknown error occurred while contacting the Poniverse API.');
}
$tokenInfo = new \Poniverse\AccessTokenInfo($accessTokenToIntrospect);
$tokenInfo
->setIsActive($data['active'])
->setScopes($data['scope']);
return $tokenInfo;
}
}

View file

@ -206,6 +206,7 @@ class Client
* @param int $grant_type Grant Type ('authorization_code', 'password', 'client_credentials', 'refresh_token', or a custom code (@see GrantType Classes)
* @param array $parameters Array sent to the server (depend on which grant type you're using)
* @return array Array of parameters required by the grant_type (CF SPEC)
* @throws Exception
*/
public function getAccessToken($token_endpoint, $grant_type, array $parameters)
{

View file

@ -0,0 +1,35 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2015 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\Policies;
use Poniverse\Ponyfm\Track;
use Poniverse\Ponyfm\User;
class TrackPolicy
{
public function edit(User $user, Track $track) {
return $user->id === $track->user_id || $user->hasRole('admin');
}
public function delete(User $user, Track $track) {
return $user->id === $track->user_id || $user->hasRole('admin');
}
}

View file

@ -20,8 +20,10 @@
namespace Poniverse\Ponyfm\Providers;
use Illuminate\Foundation\Application;
use Illuminate\Support\ServiceProvider;
use PfmValidator;
use Poniverse;
use Validator;
class AppServiceProvider extends ServiceProvider
@ -46,6 +48,8 @@ class AppServiceProvider extends ServiceProvider
*/
public function register()
{
//
$this->app->bind(Poniverse::class, function(Application $app) {
return new Poniverse($app['config']->get('poniverse.client_id'), $app['config']->get('poniverse.secret'));
});
}
}

View file

@ -24,6 +24,8 @@ use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Poniverse\Ponyfm\Genre;
use Poniverse\Ponyfm\Policies\GenrePolicy;
use Poniverse\Ponyfm\Policies\TrackPolicy;
use Poniverse\Ponyfm\Track;
use Poniverse\Ponyfm\User;
class AuthServiceProvider extends ServiceProvider
@ -34,7 +36,8 @@ class AuthServiceProvider extends ServiceProvider
* @var array
*/
protected $policies = [
Genre::class => GenrePolicy::class
Genre::class => GenrePolicy::class,
Track::class => TrackPolicy::class,
];
/**

View file

@ -39,7 +39,20 @@ class Track extends Model
{
use SoftDeletes;
protected $dates = ['deleted_at'];
protected $dates = ['deleted_at', 'published_at', 'released_at'];
protected $casts = [
'id' => 'integer',
'user_id' => 'integer',
'license_id' => 'integer',
'genre_id' => 'integer',
'track_type_id' => 'integer',
'is_vocal' => 'boolean',
'is_explicit' => 'boolean',
'cover_id' => 'integer',
'is_downloadable' => 'boolean',
'is_latest' => 'boolean',
'is_listed' => 'boolean',
];
use SlugTrait {
SlugTrait::setTitleAttribute as setTitleAttributeSlug;

View file

@ -38,7 +38,16 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
use Authenticatable, CanResetPassword, Authorizable, RevisionableTrait;
protected $table = 'users';
protected $hidden1 = ['password_hash', 'password_salt', 'bio'];
protected $casts = [
'id' => 'integer',
'sync_names' => 'boolean',
'uses_gravatar' => 'boolean',
'can_see_explicit_content' => 'boolean',
'track_count' => 'integer',
'comment_count' => 'integer',
'avatar_id' => 'integer',
'is_archived' => 'boolean',
];
public function scopeUserDetails($query)
{
@ -53,6 +62,33 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
return !$query;
}
/**
* @param string $username
* @param string $displayName
* @param string $email
* @return User
*/
public static function findOrCreate(string $username, string $displayName, string $email) {
$user = static::where('username', $username)
->where('is_archived', false)
->first();
if (null !== $user) {
return $user;
} else {
$user = new User;
$user->username = $username;
$user->display_name = $displayName;
$user->email = $email;
$user->uses_gravatar = true;
$user->save();
return $user;
}
}
public function avatar()
{
return $this->belongsTo('Poniverse\Ponyfm\Image');