#8: Implemented auto-publishing and lots of optional fields for the upload endpoint.

This commit is contained in:
Peter Deltchev 2015-12-26 03:40:47 -08:00
parent 378584261d
commit ac6ce4bbb4
16 changed files with 362 additions and 39 deletions

View file

@ -37,6 +37,7 @@ class Album extends Model
use SoftDeletes, SlugTrait, DispatchesJobs, TrackCollection, RevisionableTrait;
protected $dates = ['deleted_at'];
protected $fillable = ['user_id', 'title', 'slug'];
public static function summary()
{

View file

@ -20,16 +20,20 @@
namespace Poniverse\Ponyfm\Commands;
use Carbon\Carbon;
use Config;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Input;
use Poniverse\Ponyfm\Album;
use Poniverse\Ponyfm\Exceptions\InvalidEncodeOptionsException;
use Poniverse\Ponyfm\Genre;
use Poniverse\Ponyfm\Jobs\EncodeTrackFile;
use Poniverse\Ponyfm\Track;
use Poniverse\Ponyfm\TrackFile;
use AudioCache;
use File;
use Illuminate\Support\Str;
use Storage;
use Poniverse\Ponyfm\TrackType;
class UploadTrackCommand extends CommandBase
{
@ -38,6 +42,9 @@ class UploadTrackCommand extends CommandBase
private $_allowLossy;
private $_allowShortTrack;
private $_customTrackSource;
private $_autoPublishByDefault;
private $_losslessFormats = [
'flac',
'pcm_s16le ([1][0][0][0] / 0x0001)',
@ -49,10 +56,12 @@ class UploadTrackCommand extends CommandBase
'pcm_f32be (fl32 / 0x32336C66)'
];
public function __construct($allowLossy = false, $allowShortTrack = false)
public function __construct($allowLossy = false, $allowShortTrack = false, $customTrackSource = null, $autoPublishByDefault = false)
{
$this->_allowLossy = $allowLossy;
$this->_allowShortTrack = $allowShortTrack;
$this->_customTrackSource = $customTrackSource;
$this->_autoPublishByDefault = $autoPublishByDefault;
}
/**
@ -63,6 +72,30 @@ class UploadTrackCommand extends CommandBase
return \Auth::user() != null;
}
protected function getGenreId(string $genreName) {
return Genre::firstOrCreate(['name' => $genreName, 'slug' => Str::slug($genreName)])->id;
}
protected function getAlbumId(int $artistId, $albumName) {
if (null !== $albumName) {
$album = Album::firstOrNew([
'user_id' => $artistId,
'title' => $albumName
]);
if (null === $album->id) {
$album->description = '';
$album->track_count = 0;
$album->save();
return $album->id;
} else {
return $album->id;
}
} else {
return null;
}
}
/**
* @throws \Exception
* @return CommandResponse
@ -81,9 +114,9 @@ class UploadTrackCommand extends CommandBase
$track = new Track();
$track->user_id = $user->id;
$track->title = pathinfo($trackFile->getClientOriginalName(), PATHINFO_FILENAME);
$track->title = Input::get('title', pathinfo($trackFile->getClientOriginalName(), PATHINFO_FILENAME));
$track->duration = $audio->getDuration();
$track->is_listed = true;
$track->save();
$track->ensureDirectoryExists();
@ -93,13 +126,30 @@ class UploadTrackCommand extends CommandBase
}
$trackFile = $trackFile->move(Config::get('ponyfm.files_directory').'/queued-tracks', $track->id);
$input = Input::all();
$input['track'] = $trackFile;
$validator = \Validator::make(['track' => $trackFile], [
$validator = \Validator::make($input, [
'track' =>
'required|'
. ($this->_allowLossy ? '' : 'audio_format:'. implode(',', $this->_losslessFormats).'|')
. ($this->_allowShortTrack ? '' : 'min_duration:30|')
. 'audio_channels:1,2'
. 'audio_channels:1,2',
'auto_publish' => 'boolean',
'title' => 'string',
'track_type_id' => 'exists:track_types,id',
'genre' => 'string',
'album' => 'string',
'track_number' => 'integer',
'released_at' => 'date_format:'.Carbon::ISO8601,
'description' => 'string',
'lyrics' => 'string',
'is_vocal' => 'boolean',
'is_explicit' => 'boolean',
'is_downloadable' => 'boolean',
'is_listed' => 'boolean',
'metadata' => 'json',
]);
if ($validator->fails()) {
@ -108,6 +158,32 @@ class UploadTrackCommand extends CommandBase
}
// Process optional track fields
$autoPublish = (bool) Input::get('auto_publish', $this->_autoPublishByDefault);
$track->title = Input::get('title', $track->title);
$track->track_type_id = Input::get('track_type_id', TrackType::UNCLASSIFIED_TRACK);
$track->genre_id = $this->getGenreId(Input::get('genre', 'Unknown'));
$track->album_id = $this->getAlbumId($user->id, Input::get('album', null));
$track->track_number = $track->album_id !== null ? (int) Input::get('track_number', null) : null;
$track->released_at = Input::has('released_at') ? Carbon::createFromFormat(Carbon::ISO8601, Input::get('released_at')) : null;
$track->description = Input::get('description', '');
$track->lyrics = Input::get('lyrics', '');
$track->is_vocal = (bool) Input::get('is_vocal');
$track->is_explicit = (bool) Input::get('is_explicit');
$track->is_downloadable = (bool) Input::get('is_downloadable');
$track->is_listed = (bool) Input::get('is_listed', true);
$track->source = $this->_customTrackSource ?? 'direct_upload';
// If json_decode() isn't called here, Laravel will surround the JSON
// string with quotes when storing it in the database, which breaks things.
$track->metadata = json_decode(Input::get('metadata', null));
$track->save();
try {
$source = $trackFile->getPathname();
@ -172,7 +248,7 @@ class UploadTrackCommand extends CommandBase
try {
foreach($trackFiles as $trackFile) {
$this->dispatch(new EncodeTrackFile($trackFile, false, true));
$this->dispatch(new EncodeTrackFile($trackFile, false, true, $autoPublish));
}
} catch (InvalidEncodeOptionsException $e) {
@ -187,7 +263,10 @@ class UploadTrackCommand extends CommandBase
return CommandResponse::succeed([
'id' => $track->id,
'name' => $track->name
'name' => $track->name,
'title' => $track->title,
'slug' => $track->slug,
'autoPublish' => $autoPublish,
]);
}
}

View file

@ -31,7 +31,7 @@ class TracksController extends ApiControllerBase
public function postUploadTrack() {
session_write_close();
$response = $this->execute(new UploadTrackCommand());
$response = $this->execute(new UploadTrackCommand(true, true, session('api_client_id'), true));
$commandData = $response->getData(true);
if (200 !== $response->getStatusCode()) {
@ -39,9 +39,12 @@ class TracksController extends ApiControllerBase
}
$data = [
'id' => $commandData['id'],
'id' => (string) $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.",
'track_url' => action('TracksController@getTrack', ['id' => $commandData['id'], 'slug' => $commandData['slug']]),
'message' => $commandData['autoPublish']
? "This track has been accepted for processing! Poll the status_url to know when it has been published. It will be published at the track_url."
: "This track has been accepted for processing! Poll the status_url to know when it's ready to publish. It will be published at the track_url.",
];
$response->setData($data);
@ -59,13 +62,16 @@ class TracksController extends ApiControllerBase
} 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])
'message' => $track->published_at
? 'Processing complete! The track is live at the track_url. The artist can edit the track by visiting its edit_url.'
: 'Processing complete! The artist must publish the track by visiting its edit_url.',
'edit_url' => action('ContentController@getTracks', ['id' => $trackId]),
'track_url' => $track->url
], 201);
} else {
// something went wrong
return Response::json(['error' => 'Processing failed!'], 500);
return Response::json(['error' => 'Processing failed! Please contact feld0@poniverse.net to figure out what went wrong.'], 500);
}
}

View file

@ -41,12 +41,7 @@ class TracksController extends ApiControllerBase
{
session_write_close();
try {
return $this->execute(new UploadTrackCommand());
} catch (InvalidEncodeOptionsException $e) {
}
return $this->execute(new UploadTrackCommand());
}
public function getUploadStatus($trackId)

View file

@ -65,6 +65,8 @@ class AuthenticateOAuth
// Log in as the given user, creating the account if necessary.
$this->poniverse->setAccessToken($accessToken);
session()->put('api_client_id', $accessTokenInfo->getClientId());
$poniverseUser = $this->poniverse->getUser();
$user = User::findOrCreate($poniverseUser['username'], $poniverseUser['display_name'], $poniverseUser['email']);

View file

@ -39,7 +39,7 @@ Route::get('/tracks/popular', 'TracksController@getIndex');
Route::get('/tracks/random', 'TracksController@getIndex');
Route::get('tracks/{id}-{slug}', 'TracksController@getTrack');
Route::get('t{id}', 'TracksController@getShortlink' );
Route::get('t{id}', 'TracksController@getShortlink' )->where('id', '\d+');
Route::get('t{id}/embed', 'TracksController@getEmbed' );
Route::get('t{id}/stream.{extension}', 'TracksController@getStream' );
Route::get('t{id}/dl.{extension}', 'TracksController@getDownload' );

View file

@ -22,6 +22,7 @@
namespace Poniverse\Ponyfm\Jobs;
use Carbon\Carbon;
use DB;
use File;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
@ -52,14 +53,19 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue
* @var bool
*/
private $isForUpload;
/**
* @var bool
*/
private $autoPublishWhenComplete;
/**
* Create a new job instance.
* @param TrackFile $trackFile
* @param bool $isExpirable
* @param bool $isForUpload indicates whether this encode job is for an upload
* @param bool $autoPublish
*/
public function __construct(TrackFile $trackFile, $isExpirable, $isForUpload = false)
public function __construct(TrackFile $trackFile, $isExpirable, $isForUpload = false, $autoPublish = false)
{
if(
(!$isForUpload && $trackFile->is_master) ||
@ -71,6 +77,7 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue
$this->trackFile = $trackFile;
$this->isExpirable = $isExpirable;
$this->isForUpload = $isForUpload;
$this->autoPublishWhenComplete = $autoPublish;
}
/**
@ -140,7 +147,16 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue
File::delete($this->trackFile->getFile());
}
// This was the final TrackFile for this track!
if ($this->trackFile->track->status === Track::STATUS_COMPLETE) {
if ($this->autoPublishWhenComplete) {
$this->trackFile->track->published_at = Carbon::now();
DB::table('tracks')->whereUserId($this->trackFile->track->user_id)->update(['is_latest' => false]);
$this->trackFile->track->is_latest = true;
$this->trackFile->track->save();
}
File::delete($this->trackFile->track->getTemporarySourceFile());
}
}

View file

@ -122,7 +122,7 @@ class Poniverse {
*
* @param $accessTokenToIntrospect
* @return \Poniverse\AccessTokenInfo
* @throws \Poniverse\Ponyfm\InvalidAccessTokenException
* @throws InvalidAccessTokenException
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
*/
public function getAccessTokenInfo($accessTokenToIntrospect)
@ -154,7 +154,8 @@ class Poniverse {
$tokenInfo = new \Poniverse\AccessTokenInfo($accessTokenToIntrospect);
$tokenInfo
->setIsActive($data['active'])
->setScopes($data['scope']);
->setScopes($data['scope'])
->setClientId($data['client_id']);
return $tokenInfo;
}

View file

@ -41,6 +41,7 @@ class Track extends Model
use SoftDeletes;
protected $dates = ['deleted_at', 'published_at', 'released_at'];
protected $hidden = ['original_tags', 'metadata'];
protected $casts = [
'id' => 'integer',
'user_id' => 'integer',
@ -53,6 +54,8 @@ class Track extends Model
'is_downloadable' => 'boolean',
'is_latest' => 'boolean',
'is_listed' => 'boolean',
'original_tags' => 'array',
'metadata' => 'array',
];
use SlugTrait {

View file

@ -5,7 +5,7 @@
"license": "AGPL",
"type": "project",
"require": {
"php": ">=5.5.9",
"php": ">=7.0.1",
"laravel/framework": "5.1.*",
"codescale/ffmpeg-php": "2.7.0",
"kriswallsmith/assetic": "1.2.*@dev",

View file

@ -29,15 +29,39 @@
|
*/
$factory->define(Poniverse\Ponyfm\User::class, function ($faker) {
use Poniverse\Ponyfm\User;
$factory->define(Poniverse\Ponyfm\User::class, function (\Faker\Generator $faker) {
return [
'username' => $faker->userName,
'display_name' => $faker->userName,
'slug' => $faker->slug,
'email' => $faker->email,
'can_see_explicit_content' => true,
'uses_gravatar' => true,
'bio' => $faker->paragraph,
'track_count' => 0,
'comment_count' => 0,
];
});
$factory->define(\Poniverse\Ponyfm\Track::class, function(\Faker\Generator $faker) {
$user = factory(User::class)->create();
return [
'user_id' => $user->id,
'hash' => $faker->md5,
'title' => $faker->sentence(5),
'track_type_id' => \Poniverse\Ponyfm\TrackType::UNCLASSIFIED_TRACK,
'genre' => $faker->word,
'album' => $faker->sentence(5),
'track_number' => null,
'description' => $faker->paragraph(5),
'lyrics' => $faker->paragraph(5),
'is_vocal' => true,
'is_explicit' => false,
'is_downloadable' => true,
'is_listed' => true,
'metadata' => '{"this":{"is":["very","random","metadata"]}}'
];
});

View file

@ -0,0 +1,52 @@
<?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/>.
*/
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddMoreDefaults extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('albums', function (Blueprint $table) {
$table->text('description')->default('')->change();
$table->unsignedInteger('view_count')->default(0)->change();
$table->unsignedInteger('download_count')->default(0)->change();
$table->unsignedInteger('favourite_count')->default(0)->change();
$table->unsignedInteger('comment_count')->default(0)->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
// This migration is not reversible.
}
}

View file

@ -0,0 +1,50 @@
<?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/>.
*/
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddMetadataColumns extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('tracks', function(Blueprint $table) {
$table->longText('metadata')->nullable();
$table->longText('original_tags')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('tracks', function(Blueprint $table) {
$table->dropColumn(['original_tags', 'metadata']);
});
}
}

View file

@ -37,7 +37,6 @@ class ApiAuthTest extends TestCase {
$accessTokenInfo->setIsActive(true);
$accessTokenInfo->setScopes(['basic', 'ponyfm:tracks:upload']);
$poniverse = Mockery::mock('overload:Poniverse');
$poniverse->shouldReceive('getUser')
->andReturn([
@ -56,4 +55,30 @@ class ApiAuthTest extends TestCase {
$this->post('/api/v1/tracks', ['access_token' => 'nonsense-token']);
$this->seeInDatabase('users', ['username' => $user->username]);
}
public function testApiClientIdIsRecordedWhenUploadingTrack() {
$user = factory(User::class)->make();
$accessTokenInfo = new \Poniverse\AccessTokenInfo('nonsense-token');
$accessTokenInfo->setIsActive(true);
$accessTokenInfo->setClientId('Unicorns and rainbows');
$accessTokenInfo->setScopes(['basic', 'ponyfm:tracks:upload']);
$poniverse = Mockery::mock('overload:Poniverse');
$poniverse->shouldReceive('getUser')
->andReturn([
'username' => $user->username,
'display_name' => $user->display_name,
'email' => $user->email,
]);
$poniverse->shouldReceive('setAccessToken');
$poniverse
->shouldReceive('getAccessTokenInfo')
->andReturn($accessTokenInfo);
$this->callUploadWithParameters(['access_token' => $accessTokenInfo->getToken()]);
$this->assertSessionHas('api_client_id', $accessTokenInfo->getClientId());
$this->seeInDatabase('tracks', ['source' => $accessTokenInfo->getClientId()]);
}
}

View file

@ -20,6 +20,7 @@
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Poniverse\Ponyfm\Track;
use Poniverse\Ponyfm\User;
class ApiTest extends TestCase {
@ -40,20 +41,70 @@ class ApiTest extends TestCase {
$this->assertResponseStatus(400);
}
public function testUploadWithFile() {
$this->expectsJobs(Poniverse\Ponyfm\Jobs\EncodeTrackFile::class);
public function testUploadWithFileWithoutAutoPublish() {
$this->callUploadWithParameters([
'auto_publish' => false
]);
$user = factory(User::class)->create();
$file = $this->getTestFileForUpload('ponyfm-test.flac');
$this->actingAs($user)
->call('POST', '/api/v1/tracks', [], [], ['track' => $file]);
$this->assertResponseStatus(202);
$this->seeJsonEquals([
'message' => "This track has been accepted for processing! Poll the status_url to know when it's ready to publish.",
'id' => 1,
'status_url' => "http://ponyfm-testing.poni/api/v1/tracks/1/upload-status"
'message' => "This track has been accepted for processing! Poll the status_url to know when it's ready to publish. It will be published at the track_url.",
'id' => "1",
'status_url' => "http://ponyfm-testing.poni/api/v1/tracks/1/upload-status",
'track_url' => "http://ponyfm-testing.poni/tracks/1-ponyfm-test",
]);
}
public function testUploadWithFileWithAutoPublish() {
$this->callUploadWithParameters([]);
$this->seeJsonEquals([
'message' => "This track has been accepted for processing! Poll the status_url to know when it has been published. It will be published at the track_url.",
'id' => "1",
'status_url' => "http://ponyfm-testing.poni/api/v1/tracks/1/upload-status",
'track_url' => "http://ponyfm-testing.poni/tracks/1-ponyfm-test",
]);
$this->visit('/tracks/1-ponyfm-test');
$this->assertResponseStatus(200);
}
public function testUploadWithOptionalData() {
$track = factory(Track::class)->make();
$this->callUploadWithParameters([
'title' => $track->title,
'track_type_id' => $track->track_type_id,
'genre' => $track->genre,
'album' => $track->album,
'released_at' => \Carbon\Carbon::create(2015, 1, 1, 1, 1, 1)->toIso8601String(),
'description' => $track->description,
'lyrics' => $track->lyrics,
'is_vocal' => true,
'is_explicit' => true,
'is_downloadable' => false,
'is_listed' => false,
'metadata' => $track->metadata
]);
$this->seeInDatabase('genres', [
'name' => $track->genre
]);
$this->seeInDatabase('albums', [
'title' => $track->album
]);
$this->seeInDatabase('tracks', [
'title' => $track->title,
'track_type_id' => $track->track_type_id,
'released_at' => "2015-01-01 01:01:01",
'description' => $track->description,
'lyrics' => $track->lyrics,
'is_vocal' => true,
'is_explicit' => true,
'is_downloadable' => false,
'is_listed' => false,
'metadata' => $track->metadata
]);
}
}

View file

@ -1,4 +1,5 @@
<?php
use Poniverse\Ponyfm\User;
/**
* Pony.fm - A community for pony fan music.
@ -115,4 +116,21 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
return new \Symfony\Component\HttpFoundation\File\UploadedFile(storage_path("app/testing-datastore/tmp/${filename}"), $filename, null, null, null, true);
}
/**
* Helper function for testing file uploads to the API.
*
* @param array $parameters
*/
protected function callUploadWithParameters(array $parameters) {
$this->expectsJobs(Poniverse\Ponyfm\Jobs\EncodeTrackFile::class);
$user = factory(User::class)->create();
$file = $this->getTestFileForUpload('ponyfm-test.flac');
$this->actingAs($user)
->call('POST', '/api/v1/tracks', $parameters, [], ['track' => $file]);
$this->assertResponseStatus(202);
}
}