diff --git a/app/Commands/CommandResponse.php b/app/Commands/CommandResponse.php index f0ab2729..ddb5e62f 100644 --- a/app/Commands/CommandResponse.php +++ b/app/Commands/CommandResponse.php @@ -24,11 +24,25 @@ use Illuminate\Validation\Validator; class CommandResponse { - public static function fail($validator) + /** + * @var Validator + */ + private $_validator; + private $_response; + private $_didFail; + + public static function fail($validatorOrMessages) { $response = new CommandResponse(); $response->_didFail = true; - $response->_validator = $validator; + + if (is_array($validatorOrMessages)) { + $response->_messages = $validatorOrMessages; + $response->_validator = null; + + } else { + $response->_validator = $validatorOrMessages; + } return $response; } @@ -42,10 +56,6 @@ class CommandResponse return $cmdResponse; } - private $_validator; - private $_response; - private $_didFail; - private function __construct() { } @@ -73,4 +83,14 @@ class CommandResponse { return $this->_validator; } + + public function getMessages() + { + if ($this->_validator !== null) { + return $this->_validator->messages()->getMessages(); + + } else { + return $this->_messages; + } + } } diff --git a/app/Commands/UploadTrackCommand.php b/app/Commands/UploadTrackCommand.php index 8f80a510..f801ff84 100644 --- a/app/Commands/UploadTrackCommand.php +++ b/app/Commands/UploadTrackCommand.php @@ -20,17 +20,22 @@ namespace Poniverse\Ponyfm\Commands; +use Config; +use Illuminate\Foundation\Bus\DispatchesJobs; +use Poniverse\Ponyfm\Exceptions\InvalidEncodeOptionsException; +use Poniverse\Ponyfm\Jobs\EncodeTrackFile; use Poniverse\Ponyfm\Track; use Poniverse\Ponyfm\TrackFile; use AudioCache; use File; -use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; -use Symfony\Component\Process\Exception\ProcessFailedException; -use Symfony\Component\Process\Process; +use Storage; class UploadTrackCommand extends CommandBase { + use DispatchesJobs; + + private $_allowLossy; private $_allowShortTrack; private $_losslessFormats = [ @@ -68,6 +73,21 @@ class UploadTrackCommand extends CommandBase $trackFile = \Input::file('track'); $audio = \AudioCache::get($trackFile->getPathname()); + + $track = new Track(); + $track->user_id = $user->id; + $track->title = pathinfo($trackFile->getClientOriginalName(), PATHINFO_FILENAME); + $track->duration = $audio->getDuration(); + $track->is_listed = true; + + $track->save(); + $track->ensureDirectoryExists(); + + Storage::makeDirectory(Config::get('ponyfm.files_directory') . '/queued-tracks', 0755, false, true); + $trackFile = $trackFile->move(Config::get('ponyfm.files_directory').'/queued-tracks', $track->id); + + + $validator = \Validator::make(['track' => $trackFile], [ 'track' => 'required|' @@ -77,24 +97,13 @@ class UploadTrackCommand extends CommandBase ]); if ($validator->fails()) { + $track->delete(); return CommandResponse::fail($validator); } - $track = new Track(); try { - $track->user_id = $user->id; - $track->title = pathinfo($trackFile->getClientOriginalName(), PATHINFO_FILENAME); - $track->duration = $audio->getDuration(); - $track->is_listed = true; - - $track->save(); - - $destination = $track->getDirectory(); - $track->ensureDirectoryExists(); - $source = $trackFile->getPathname(); - $index = 0; // Lossy uploads need to be identified and set as the master file // without being re-encoded. @@ -113,6 +122,7 @@ class UploadTrackCommand extends CommandBase } else { $validator->messages()->add('track', 'The track does not contain audio in a known lossy format.'); + $track->delete(); return CommandResponse::fail($validator); } @@ -126,6 +136,9 @@ class UploadTrackCommand extends CommandBase File::copy($source, $trackFile->getFile()); } + + $trackFiles = []; + foreach (Track::$Formats as $name => $format) { // Don't bother with lossless transcodes of lossy uploads, and // don't re-encode the lossy master. @@ -136,6 +149,7 @@ class UploadTrackCommand extends CommandBase $trackFile = new TrackFile(); $trackFile->is_master = $name === 'FLAC' ? true : false; $trackFile->format = $name; + $trackFile->status = TrackFile::STATUS_PROCESSING; if (in_array($name, Track::$CacheableFormats) && $trackFile->is_master == false) { $trackFile->is_cacheable = true; @@ -144,29 +158,21 @@ class UploadTrackCommand extends CommandBase } $track->trackFiles()->save($trackFile); - // Encode track file - $target = $trackFile->getFile(); - - $command = $format['command']; - $command = str_replace('{$source}', '"' . $source . '"', $command); - $command = str_replace('{$target}', '"' . $target . '"', $command); - - Log::info('Encoding ' . $track->id . ' into ' . $target); - $this->notify('Encoding ' . $name, $index / count(Track::$Formats) * 100); - - $process = new Process($command); - $process->mustRun(); - - // Update file size for track file - $trackFile->updateFilesize(); - - // Delete track file if it is cacheable - if ($trackFile->is_cacheable == true) { - File::delete($trackFile->getFile()); - } + // All TrackFile records we need are synchronously created + // before kicking off the encode jobs in order to avoid a race + // condition with the "temporary" source file getting deleted. + $trackFiles[] = $trackFile; } - $track->updateTags(); + try { + foreach($trackFiles as $trackFile) { + $this->dispatch(new EncodeTrackFile($trackFile, false, true)); + } + + } catch (InvalidEncodeOptionsException $e) { + $track->delete(); + return CommandResponse::fail(['track' => [$e->getMessage()]]); + } } catch (\Exception $e) { $track->delete(); diff --git a/app/Console/Commands/ImportMLPMA.php b/app/Console/Commands/ImportMLPMA.php index 52a03e89..d38cf454 100644 --- a/app/Console/Commands/ImportMLPMA.php +++ b/app/Console/Commands/ImportMLPMA.php @@ -365,7 +365,7 @@ class ImportMLPMA extends Command $result = $upload->execute(); if ($result->didFail()) { - $this->error(json_encode($result->getValidator()->messages()->getMessages(), JSON_PRETTY_PRINT)); + $this->error(json_encode($result->getMessages(), JSON_PRETTY_PRINT)); } else { // Save metadata. diff --git a/app/Exceptions/InvalidEncodeOptionsException.php b/app/Exceptions/InvalidEncodeOptionsException.php new file mode 100644 index 00000000..64181efe --- /dev/null +++ b/app/Exceptions/InvalidEncodeOptionsException.php @@ -0,0 +1,25 @@ +. + */ + +namespace Poniverse\Ponyfm\Exceptions; + +use InvalidArgumentException; + +class InvalidEncodeOptionsException extends InvalidArgumentException {} diff --git a/app/Http/Controllers/Api/Web/TracksController.php b/app/Http/Controllers/Api/Web/TracksController.php index 81fefdce..b509fe80 100644 --- a/app/Http/Controllers/Api/Web/TracksController.php +++ b/app/Http/Controllers/Api/Web/TracksController.php @@ -33,6 +33,7 @@ use Cover; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Input; use Illuminate\Support\Facades\Response; +use Poniverse\Ponyfm\TrackFile; class TracksController extends ApiControllerBase { @@ -40,7 +41,31 @@ class TracksController extends ApiControllerBase { session_write_close(); - return $this->execute(new UploadTrackCommand()); + try { + return $this->execute(new UploadTrackCommand()); + + } catch (\InvalidEncodeOptions $e) { + + } + } + + public function getUploadStatus($trackId) + { + // TODO: authorize this + + $track = Track::findOrFail($trackId); + + if ($track->status === Track::STATUS_PROCESSING){ + return Response::json(['message' => 'Processing...'], 202); + + } elseif ($track->status === Track::STATUS_COMPLETE) { + return Response::json(['message' => 'Processing complete!'], 201); + + } else { + // something went wrong + return Response::json(['error' => 'Processing failed!'], 500); + } + } public function postDelete($id) @@ -101,7 +126,7 @@ class TracksController extends ApiControllerBase // Return URL or begin encoding if ($trackFile->expires_at != null && File::exists($trackFile->getFile())) { $url = $track->getUrlFor($format); - } elseif ($trackFile->is_in_progress === true) { + } elseif ($trackFile->status === TrackFile::STATUS_PROCESSING) { $url = null; } else { $this->dispatch(new EncodeTrackFile($trackFile, true)); diff --git a/app/Http/Controllers/ApiControllerBase.php b/app/Http/Controllers/ApiControllerBase.php index c8420442..4f814fa6 100644 --- a/app/Http/Controllers/ApiControllerBase.php +++ b/app/Http/Controllers/ApiControllerBase.php @@ -35,7 +35,7 @@ abstract class ApiControllerBase extends Controller if ($result->didFail()) { return Response::json([ 'message' => 'Validation failed', - 'errors' => $result->getValidator()->messages()->getMessages() + 'errors' => $result->getMessages() ], 400); } diff --git a/app/Http/routes.php b/app/Http/routes.php index 48f53bd2..afa12b27 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -101,6 +101,7 @@ Route::group(['prefix' => 'api/web'], function() { Route::group(['middleware' => 'auth'], function() { Route::post('/tracks/upload', 'Api\Web\TracksController@postUpload'); + Route::get('/tracks/{id}/upload-status', 'Api\Web\TracksController@getUploadStatus'); Route::post('/tracks/delete/{id}', 'Api\Web\TracksController@postDelete'); Route::post('/tracks/edit/{id}', 'Api\Web\TracksController@postEdit'); diff --git a/app/Jobs/EncodeTrackFile.php b/app/Jobs/EncodeTrackFile.php index 2b135556..fc53c34a 100644 --- a/app/Jobs/EncodeTrackFile.php +++ b/app/Jobs/EncodeTrackFile.php @@ -2,6 +2,7 @@ /** * Pony.fm - A community for pony fan music. + * Copyright (C) 2015 Peter Deltchev * Copyright (C) 2015 Kelvin Zhang * * This program is free software: you can redistribute it and/or modify @@ -21,9 +22,11 @@ namespace Poniverse\Ponyfm\Jobs; use Carbon\Carbon; +use File; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Log; use OAuth2\Exception; +use Poniverse\Ponyfm\Exceptions\InvalidEncodeOptionsException; use Poniverse\Ponyfm\Jobs\Job; use Illuminate\Queue\SerializesModels; use Illuminate\Queue\InteractsWithQueue; @@ -45,16 +48,29 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue * @var */ private $isExpirable; + /** + * @var bool + */ + private $isForUpload; /** * Create a new job instance. * @param TrackFile $trackFile - * @param $isExpirable + * @param bool $isExpirable + * @param bool $isForUpload indicates whether this encode job is for an upload */ - public function __construct(TrackFile $trackFile, $isExpirable) + public function __construct(TrackFile $trackFile, $isExpirable, $isForUpload = false) { + if( + (!$isForUpload && $trackFile->is_master) || + ($isForUpload && $trackFile->is_master && !$trackFile->getFormat()['is_lossless']) + ) { + throw new InvalidEncodeOptionsException("Master files cannot be encoded unless we're generating a lossless master file during the upload process."); + } + $this->trackFile = $trackFile; $this->isExpirable = $isExpirable; + $this->isForUpload = $isForUpload; } /** @@ -65,14 +81,19 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue public function handle() { // Start the job - $this->trackFile->is_in_progress = true; + $this->trackFile->status = TrackFile::STATUS_PROCESSING; $this->trackFile->update(); // Use the track's master file as the source - $source = TrackFile::where('track_id', $this->trackFile->track_id) - ->where('is_master', true) - ->first() - ->getFile(); + if ($this->isForUpload) { + $source = $this->trackFile->track->getTemporarySourceFile(); + + } else { + $source = TrackFile::where('track_id', $this->trackFile->track_id) + ->where('is_master', true) + ->first() + ->getFile(); + } // Assign the target $this->trackFile->track->ensureDirectoryExists(); @@ -111,8 +132,18 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue $this->trackFile->updateFilesize(); // Complete the job - $this->trackFile->is_in_progress = false; + $this->trackFile->status = TrackFile::STATUS_NOT_BEING_PROCESSED; $this->trackFile->update(); + + if ($this->isForUpload) { + if (!$this->trackFile->is_master && $this->trackFile->is_cacheable) { + File::delete($this->trackFile->getFile()); + } + + if ($this->trackFile->track->status === Track::STATUS_COMPLETE) { + File::delete($this->trackFile->track->getTemporarySourceFile()); + } + } } /** @@ -122,7 +153,7 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue */ public function failed() { - $this->trackFile->is_in_progress = false; + $this->trackFile->status = TrackFile::STATUS_PROCESSING_ERROR; $this->trackFile->expires_at = null; $this->trackFile->update(); } diff --git a/app/Track.php b/app/Track.php index 57e43298..a9afdd3d 100644 --- a/app/Track.php +++ b/app/Track.php @@ -48,6 +48,12 @@ class Track extends Model use RevisionableTrait; + // Used for the track's upload status. + const STATUS_COMPLETE = 0; + const STATUS_PROCESSING = 1; + const STATUS_ERROR = 2; + + public static $Formats = [ 'FLAC' => [ 'index' => 0, @@ -579,6 +585,18 @@ class Track extends Model return "{$this->getDirectory()}/{$this->id}.{$format['extension']}"; } + /** + * Returns the path to the "temporary" master file uploaded by the user. + * This file is used during the upload process to generate the actual master + * file stored by Pony.fm. + * + * @return string + */ + public function getTemporarySourceFile() { + return Config::get('ponyfm.files_directory') . '/queued-tracks/' . $this->id; + } + + public function getUrlFor($format) { if (!isset(self::$Formats[$format])) { @@ -590,6 +608,33 @@ class Track extends Model return URL::to('/t' . $this->id . '/dl.' . $format['extension']); } + + /** + * @return string one of the Track::STATUS_* values, indicating whether this track is currently being processed + */ + public function getStatusAttribute(){ + return $this->trackFiles->reduce(function($carry, $trackFile){ + if($trackFile->status === TrackFile::STATUS_PROCESSING_ERROR) { + return static::STATUS_ERROR; + + } elseif ( + $carry !== static::STATUS_ERROR && + $trackFile->status === TrackFile::STATUS_PROCESSING) { + return static::STATUS_PROCESSING; + + } elseif ( + !in_array($carry, [static::STATUS_ERROR, static::STATUS_PROCESSING]) && + $trackFile->status === TrackFile::STATUS_NOT_BEING_PROCESSED + ) { + return static::STATUS_COMPLETE; + + } else { + return $carry; + } + }, static::STATUS_COMPLETE); + } + + public function updateHash() { $this->hash = md5(Helpers::sanitizeInputForHashing($this->user->display_name) . ' - ' . Helpers::sanitizeInputForHashing($this->title)); diff --git a/app/TrackFile.php b/app/TrackFile.php index 63556f4e..22e9d779 100644 --- a/app/TrackFile.php +++ b/app/TrackFile.php @@ -20,6 +20,7 @@ namespace Poniverse\Ponyfm; +use Config; use Helpers; use Illuminate\Database\Eloquent\Model; use App; @@ -28,6 +29,12 @@ use URL; class TrackFile extends Model { + // used for the "status" property + const STATUS_NOT_BEING_PROCESSED = 0; + const STATUS_PROCESSING = 1; + const STATUS_PROCESSING_ERROR = 2; + + public function track() { return $this->belongsTo('Poniverse\Ponyfm\Track')->withTrashed(); diff --git a/app/Traits/TrackCollection.php b/app/Traits/TrackCollection.php index 2be06db0..ec8f15c9 100644 --- a/app/Traits/TrackCollection.php +++ b/app/Traits/TrackCollection.php @@ -103,7 +103,7 @@ trait TrackCollection foreach ($trackFiles as $trackFile) { /** @var TrackFile $trackFile */ - if (!File::exists($trackFile->getFile()) && $trackFile->is_in_progress != true) { + if (!File::exists($trackFile->getFile()) && $trackFile->status == TrackFile::STATUS_NOT_BEING_PROCESSED) { $this->dispatch(new EncodeTrackFile($trackFile, true)); } } diff --git a/database/migrations/2015_10_26_192855_update_track_files_with_cache.php b/database/migrations/2015_10_26_192855_update_track_files_with_cache.php index 5352f319..8b27c22f 100644 --- a/database/migrations/2015_10_26_192855_update_track_files_with_cache.php +++ b/database/migrations/2015_10_26_192855_update_track_files_with_cache.php @@ -2,6 +2,7 @@ /** * Pony.fm - A community for pony fan music. + * Copyright (C) 2015 Peter Deltchev * Copyright (C) 2015 Kelvin Zhang * * This program is free software: you can redistribute it and/or modify @@ -32,7 +33,7 @@ class UpdateTrackFilesWithCache extends Migration { Schema::table('track_files', function (Blueprint $table) { $table->boolean('is_cacheable')->default(false)->index(); - $table->boolean('is_in_progress')->default(false); + $table->tinyInteger('is_in_progress')->default(false); $table->dateTime('expires_at')->nullable()->index(); }); } diff --git a/database/migrations/2015_12_18_025953_convert_track_file_in_progress_to_status.php b/database/migrations/2015_12_18_025953_convert_track_file_in_progress_to_status.php new file mode 100644 index 00000000..ff116691 --- /dev/null +++ b/database/migrations/2015_12_18_025953_convert_track_file_in_progress_to_status.php @@ -0,0 +1,51 @@ +. + */ + +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Migrations\Migration; + +class ConvertTrackFileInProgressToStatus extends Migration +{ + /** + * Run the migrations. + * + * @return void + */ + public function up() + { + // + Schema::table('track_files', function(Blueprint $table) { + $table->renameColumn('is_in_progress', 'status'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + Schema::table('track_files', function (Blueprint $table) { + $table->renameColumn('status', 'is_in_progress'); + }); + } +} diff --git a/public/templates/uploader/index.html b/public/templates/uploader/index.html index 6139d8bd..31acaec6 100644 --- a/public/templates/uploader/index.html +++ b/public/templates/uploader/index.html @@ -10,15 +10,17 @@

Please note that you need to publish your tracks after uploading them before they will become available to the public.