diff --git a/app/Commands/GenerateTrackFilesCommand.php b/app/Commands/GenerateTrackFilesCommand.php index f61ea3c8..b7a3eb8c 100644 --- a/app/Commands/GenerateTrackFilesCommand.php +++ b/app/Commands/GenerateTrackFilesCommand.php @@ -20,15 +20,15 @@ namespace Poniverse\Ponyfm\Commands; +use AudioCache; use FFmpegMovie; +use File; use Illuminate\Foundation\Bus\DispatchesJobs; +use Illuminate\Support\Str; use Poniverse\Ponyfm\Exceptions\InvalidEncodeOptionsException; use Poniverse\Ponyfm\Jobs\EncodeTrackFile; use Poniverse\Ponyfm\Models\Track; use Poniverse\Ponyfm\Models\TrackFile; -use AudioCache; -use File; -use Illuminate\Support\Str; use SplFileInfo; /** @@ -45,6 +45,9 @@ class GenerateTrackFilesCommand extends CommandBase private $track; private $autoPublish; private $sourceFile; + private $isForUpload; + private $isReplacingTrack; + private $version; protected static $_losslessFormats = [ 'flac', @@ -53,11 +56,14 @@ class GenerateTrackFilesCommand extends CommandBase 'alac' ]; - public function __construct(Track $track, SplFileInfo $sourceFile, bool $autoPublish = false) + public function __construct(Track $track, SplFileInfo $sourceFile, bool $autoPublish = false, bool $isForUpload = false, bool $isReplacingTrack = false, int $version = 1) { $this->track = $track; $this->autoPublish = $autoPublish; $this->sourceFile = $sourceFile; + $this->isForUpload = $isForUpload; + $this->isReplacingTrack = $isReplacingTrack; + $this->version = $version; } /** @@ -98,6 +104,7 @@ class GenerateTrackFilesCommand extends CommandBase $trackFile->is_master = true; $trackFile->format = $masterFormat; $trackFile->track_id = $this->track->id; + $trackFile->version = $this->version; $trackFile->save(); } @@ -126,13 +133,14 @@ class GenerateTrackFilesCommand extends CommandBase $trackFile->is_master = $name === 'FLAC' ? true : false; $trackFile->format = $name; $trackFile->status = TrackFile::STATUS_PROCESSING_PENDING; + $trackFile->version = $this->version; if (in_array($name, Track::$CacheableFormats) && !$trackFile->is_master) { $trackFile->is_cacheable = true; } else { $trackFile->is_cacheable = false; } - $this->track->trackFiles()->save($trackFile); + $this->track->trackFilesForAllVersions()->save($trackFile); // All TrackFile records we need are synchronously created // before kicking off the encode jobs in order to avoid a race @@ -142,16 +150,31 @@ class GenerateTrackFilesCommand extends CommandBase try { foreach ($trackFiles as $trackFile) { - $this->dispatch(new EncodeTrackFile($trackFile, false, true, $this->autoPublish)); + // Don't re-encode master files when replacing tracks with an already-uploaded version + if ($trackFile->is_master && !$this->isForUpload && $this->isReplacingTrack) { + continue; + } + $this->dispatch(new EncodeTrackFile($trackFile, false, false, $this->isForUpload, $this->isReplacingTrack)); } } catch (InvalidEncodeOptionsException $e) { - $this->track->delete(); + // Only delete the track if the track is not being replaced + if ($this->isReplacingTrack) { + $this->track->version_upload_status = Track::STATUS_ERROR; + $this->track->update(); + } else { + $this->track->delete(); + } return CommandResponse::fail(['track' => [$e->getMessage()]]); } } catch (\Exception $e) { - $this->track->delete(); + if ($this->isReplacingTrack) { + $this->track->version_upload_status = Track::STATUS_ERROR; + $this->track->update(); + } else { + $this->track->delete(); + } throw $e; } @@ -168,7 +191,8 @@ class GenerateTrackFilesCommand extends CommandBase * @param FFmpegMovie|string $file object or full path of the file we're checking * @return bool whether the given file is lossless */ - private function isLosslessFile($file) { + private function isLosslessFile($file) + { if (is_string($file)) { $file = AudioCache::get($file); } @@ -180,7 +204,8 @@ class GenerateTrackFilesCommand extends CommandBase * @param string $format * @return TrackFile|null */ - private function trackFileExists(string $format) { - return $this->track->trackFiles()->where('format', $format)->first(); + private function trackFileExists(string $format) + { + return $this->track->trackFilesForAllVersions()->where('format', $format)->where('version', $this->version)->first(); } } diff --git a/app/Commands/UploadTrackCommand.php b/app/Commands/UploadTrackCommand.php index 85ed4100..a4ea3340 100644 --- a/app/Commands/UploadTrackCommand.php +++ b/app/Commands/UploadTrackCommand.php @@ -27,7 +27,6 @@ use Gate; use Illuminate\Foundation\Bus\DispatchesJobs; use Input; use Poniverse\Ponyfm\Models\Track; -use AudioCache; use Poniverse\Ponyfm\Models\User; use Validator; @@ -40,6 +39,21 @@ class UploadTrackCommand extends CommandBase private $_allowShortTrack; private $_customTrackSource; private $_autoPublishByDefault; + private $_track; + private $_version; + private $_isReplacingTrack; + + /** + * @return bool + */ + public function authorize() + { + if ($this->_isReplacingTrack) { + return $this->_track && Gate::allows('edit', $this->_track); + } else { + return Gate::allows('create-track', $this->_artist); + } + } /** * UploadTrackCommand constructor. @@ -48,12 +62,16 @@ class UploadTrackCommand extends CommandBase * @param bool $allowShortTrack allow tracks shorter than 30 seconds * @param string|null $customTrackSource value to set in the track's "source" field; if left blank, "direct_upload" is used * @param bool $autoPublishByDefault + * @param int $version + * @param Track $track | null */ public function __construct( bool $allowLossy = false, bool $allowShortTrack = false, string $customTrackSource = null, - bool $autoPublishByDefault = false + bool $autoPublishByDefault = false, + int $version = 1, + $track = null ) { $userSlug = Input::get('user_slug', null); $this->_artist = @@ -65,14 +83,9 @@ class UploadTrackCommand extends CommandBase $this->_allowShortTrack = $allowShortTrack; $this->_customTrackSource = $customTrackSource; $this->_autoPublishByDefault = $autoPublishByDefault; - } - - /** - * @return bool - */ - public function authorize() - { - return Gate::allows('create-track', $this->_artist); + $this->_version = $version; + $this->_track = $track; + $this->_isReplacingTrack = $this->_track !== null && $version > 1; } /** @@ -82,22 +95,32 @@ class UploadTrackCommand extends CommandBase public function execute() { $trackFile = Input::file('track', null); - $coverFile = Input::file('cover', null); + if (!$this->_isReplacingTrack) { + $coverFile = Input::file('cover', null); + } if (null === $trackFile) { + if ($this->_isReplacingTrack) { + $this->_track->version_upload_status = Track::STATUS_ERROR; + $this->_track->update(); + } return CommandResponse::fail(['track' => ['You must upload an audio file!']]); } $audio = \AudioCache::get($trackFile->getPathname()); - $track = new Track(); - $track->user_id = $this->_artist->id; - // The title set here is a placeholder; it'll be replaced by ParseTrackTagsCommand - // if the file contains a title tag. - $track->title = Input::get('title', pathinfo($trackFile->getClientOriginalName(), PATHINFO_FILENAME)); - $track->duration = $audio->getDuration(); - $track->save(); - $track->ensureDirectoryExists(); + if (!$this->_isReplacingTrack) { + $this->_track = new Track(); + $this->_track->user_id = $this->_artist->id; + // The title set here is a placeholder; it'll be replaced by ParseTrackTagsCommand + // if the file contains a title tag. + $this->_track->title = Input::get('title', pathinfo($trackFile->getClientOriginalName(), PATHINFO_FILENAME)); + // The duration/version of the track cannot be changed until the encoding is successful + $this->_track->duration = $audio->getDuration(); + $this->_track->current_version = $this->_version; + $this->_track->save(); + } + $this->_track->ensureDirectoryExists(); if (!is_dir(Config::get('ponyfm.files_directory').'/tmp')) { mkdir(Config::get('ponyfm.files_directory').'/tmp', 0755, true); @@ -106,13 +129,15 @@ class UploadTrackCommand extends CommandBase if (!is_dir(Config::get('ponyfm.files_directory').'/queued-tracks')) { mkdir(Config::get('ponyfm.files_directory').'/queued-tracks', 0755, true); } - $trackFile = $trackFile->move(Config::get('ponyfm.files_directory').'/queued-tracks', $track->id); + $trackFile = $trackFile->move(Config::get('ponyfm.files_directory').'/queued-tracks', $this->_track->id . 'v' . $this->_version); $input = Input::all(); $input['track'] = $trackFile; - $input['cover'] = $coverFile; + if (!$this->_isReplacingTrack) { + $input['cover'] = $coverFile; + } - $validator = \Validator::make($input, [ + $rules = [ 'track' => 'required|' . ($this->_allowLossy @@ -120,44 +145,61 @@ class UploadTrackCommand extends CommandBase : 'audio_format:flac,alac,pcm,adpcm|') . ($this->_allowShortTrack ? '' : 'min_duration:30|') . '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', - 'cover' => 'image|mimes:png,jpeg|min_width:350|min_height:350', - 'metadata' => 'json', - ]); + ]; + if (!$this->_isReplacingTrack) { + array_push($rules, [ + 'cover' => 'image|mimes:png,jpeg|min_width:350|min_height:350', + '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' + ]); + } + $validator = \Validator::make($input, $rules); if ($validator->fails()) { - $track->delete(); + if ($this->_isReplacingTrack) { + $this->_track->version_upload_status = Track::STATUS_ERROR; + $this->_track->update(); + } else { + $this->_track->delete(); + } return CommandResponse::fail($validator); } - $autoPublish = (bool) ($input['auto_publish'] ?? $this->_autoPublishByDefault); - $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(); + if (!$this->_isReplacingTrack) { + // If json_decode() isn't called here, Laravel will surround the JSON + // string with quotes when storing it in the database, which breaks things. + $this->_track->metadata = json_decode(Input::get('metadata', null)); + } + $autoPublish = (bool)($input['auto_publish'] ?? $this->_autoPublishByDefault); + $this->_track->source = $this->_customTrackSource ?? 'direct_upload'; + $this->_track->save(); - // Parse any tags in the uploaded files. - $parseTagsCommand = new ParseTrackTagsCommand($track, $trackFile, $input); - $result = $parseTagsCommand->execute(); - if ($result->didFail()) { - return $result; + if (!$this->_isReplacingTrack) { + // Parse any tags in the uploaded files. + $parseTagsCommand = new ParseTrackTagsCommand($this->_track, $trackFile, $input); + $result = $parseTagsCommand->execute(); + if ($result->didFail()) { + if ($this->_isReplacingTrack) { + $this->_track->version_upload_status = Track::STATUS_ERROR; + $this->_track->update(); + } + return $result; + } } - $generateTrackFiles = new GenerateTrackFilesCommand($track, $trackFile, $autoPublish); + $generateTrackFiles = new GenerateTrackFilesCommand($this->_track, $trackFile, $autoPublish, true, $this->_isReplacingTrack, $this->_version); return $generateTrackFiles->execute(); } } diff --git a/app/Console/Commands/RebuildTrack.php b/app/Console/Commands/RebuildTrack.php index f1569636..a849df74 100644 --- a/app/Console/Commands/RebuildTrack.php +++ b/app/Console/Commands/RebuildTrack.php @@ -72,7 +72,7 @@ class RebuildTrack extends Command $track->restore(); $this->info("Attempting to finish this track's upload..."); - $sourceFile = new \SplFileInfo($track->getTemporarySourceFile()); + $sourceFile = new \SplFileInfo($track->getTemporarySourceFileForVersion($track->current_version)); $generateTrackFiles = new GenerateTrackFilesCommand($track, $sourceFile, false); $result = $generateTrackFiles->execute(); // The GenerateTrackFiles command will re-encode all TrackFiles. diff --git a/app/Console/Commands/VersionFiles.php b/app/Console/Commands/VersionFiles.php new file mode 100644 index 00000000..0489263f --- /dev/null +++ b/app/Console/Commands/VersionFiles.php @@ -0,0 +1,97 @@ +. + */ + +namespace Poniverse\Ponyfm\Console\Commands; + +use File; +use Illuminate\Console\Command; +use Poniverse\Ponyfm\Models\TrackFile; + +class VersionFiles extends Command +{ + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'version-files + {--force : Skip all prompts.}'; + + /** + * The console command description. + * + * @var string + */ + protected $description = 'Replaces track files of format [name].[ext] with [name]-v[version].[ext]'; + + /** + * Create a new command instance. + * + * @return void + */ + public function __construct() + { + parent::__construct(); + } + + /** + * Execute the console command. + * + * @return mixed + */ + public function handle() + { + $this->info('This will only version track files which exist on disk; non-existent files will be skipped.'); + + if ($this->option('force') || $this->confirm('Are you sure you want to rename all unversioned track files? [y|N]', false)) { + + TrackFile::chunk(200, function ($trackFiles) { + + $this->info('========== Start Chunk =========='); + + foreach ($trackFiles as $trackFile) { + /** @var TrackFile $trackFile */ + + // Check whether the unversioned file exists + if (!File::exists($trackFile->getUnversionedFile())) { + $this->info('ID ' . $trackFile->id . ' skipped - file not found'); + continue; + } + + // Version the file and check the outcome + if (File::move($trackFile->getUnversionedFile(), $trackFile->getFile())) { + $this->info('ID ' . $trackFile->id . ' processed'); + } else { + $this->error('ID ' . $trackFile->id . ' was unable to be renamed'); + } + + } + + $this->info('=========== End Chunk ==========='); + + }); + + $this->info('Rebuild complete. Exiting.'); + + } else { + $this->info('Rebuild cancelled. Exiting.'); + } + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index ade0d41f..66fc547e 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -45,6 +45,7 @@ class Kernel extends ConsoleKernel \Poniverse\Ponyfm\Console\Commands\RebuildSearchIndex::class, \Poniverse\Ponyfm\Console\Commands\MergeAccounts::class, \Poniverse\Ponyfm\Console\Commands\FixMLPMAImages::class, + \Poniverse\Ponyfm\Console\Commands\VersionFiles::class ]; /** diff --git a/app/Http/Controllers/Api/Web/TracksController.php b/app/Http/Controllers/Api/Web/TracksController.php index aae95438..f4106a6a 100644 --- a/app/Http/Controllers/Api/Web/TracksController.php +++ b/app/Http/Controllers/Api/Web/TracksController.php @@ -20,23 +20,24 @@ namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; -use Illuminate\Database\Eloquent\ModelNotFoundException; +use Auth; use File; +use Illuminate\Database\Eloquent\ModelNotFoundException; +use Input; use Poniverse\Ponyfm\Commands\DeleteTrackCommand; use Poniverse\Ponyfm\Commands\EditTrackCommand; +use Poniverse\Ponyfm\Commands\GenerateTrackFilesCommand; use Poniverse\Ponyfm\Commands\UploadTrackCommand; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; use Poniverse\Ponyfm\Jobs\EncodeTrackFile; use Poniverse\Ponyfm\Models\Genre; use Poniverse\Ponyfm\Models\ResourceLogItem; -use Poniverse\Ponyfm\Models\TrackFile; use Poniverse\Ponyfm\Models\Track; use Poniverse\Ponyfm\Models\TrackType; -use Poniverse\Ponyfm\Models\TrackTypes; -use Auth; -use Input; +use Poniverse\Ponyfm\Models\TrackFile; use Poniverse\Ponyfm\Models\User; use Response; +use Symfony\Component\HttpFoundation\File\UploadedFile; class TracksController extends ApiControllerBase { @@ -74,6 +75,75 @@ class TracksController extends ApiControllerBase return $this->execute(new EditTrackCommand($id, Input::all())); } + public function postUploadNewVersion($trackId) + { + session_write_close(); + + $track = Track::find($trackId); + if (!$track) { + return $this->notFound('Track not found!'); + } + $this->authorize('edit', $track); + + $track->version_upload_status = Track::STATUS_PROCESSING; + $track->update(); + return $this->execute(new UploadTrackCommand(true, false, null, false, $track->getNextVersion(), $track)); + } + + public function getVersionUploadStatus($trackId) + { + $track = Track::findOrFail($trackId); + $this->authorize('edit', $track); + + if ($track->version_upload_status === Track::STATUS_PROCESSING) { + return Response::json(['message' => 'Processing...'], 202); + + } elseif ($track->version_upload_status === Track::STATUS_COMPLETE) { + return Response::json(['message' => 'Processing complete!'], 201); + + } else { + // something went wrong + return Response::json(['error' => 'Processing failed!'], 500); + } + } + + public function getVersionList($trackId) + { + $track = Track::findOrFail($trackId); + $this->authorize('edit', $track); + + $versions = []; + $trackFiles = $track->trackFilesForAllVersions()->where('is_master', 'true')->get(); + foreach($trackFiles as $trackFile) { + $versions[] = [ + 'version' => $trackFile->version, + 'url' => '/tracks/' . $track->id . '/version-change/' . $trackFile->version, + 'created_at' => $trackFile->created_at->timestamp + ]; + } + + return Response::json(['current_version' => $track->current_version, 'versions' => $versions], 200); + } + + public function getChangeVersion($trackId, $newVersion) + { + $track = Track::find($trackId); + if (!$track) { + return $this->notFound('Track not found!'); + } + $this->authorize('edit', $track); + + $masterTrackFile = $track->trackFilesForVersion($newVersion)->where('is_master', true)->first(); + if (!$masterTrackFile) { + return $this->notFound('Version not found!'); + } + + $track->version_upload_status = Track::STATUS_PROCESSING; + $track->update(); + $sourceFile = new UploadedFile($masterTrackFile->getFile(), $masterTrackFile->getFilename()); + return $this->execute(new GenerateTrackFilesCommand($track, $sourceFile, false, false, true, $newVersion)); + } + public function getShow($id) { $track = Track::userDetails()->withComments()->find($id); diff --git a/app/Http/routes.php b/app/Http/routes.php index 4539cae2..3dc0271c 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -114,6 +114,11 @@ Route::group(['prefix' => 'api/web'], function() { Route::post('/tracks/delete/{id}', 'Api\Web\TracksController@postDelete'); Route::post('/tracks/edit/{id}', 'Api\Web\TracksController@postEdit'); + Route::post('/tracks/{id}/version-upload', 'Api\Web\TracksController@postUploadNewVersion'); + Route::get('/tracks/{id}/version-change/{version}', 'Api\Web\TracksController@getChangeVersion'); + Route::get('/tracks/{id}/version-upload-status', 'Api\Web\TracksController@getVersionUploadStatus'); + Route::get('/tracks/{id}/versions', 'Api\Web\TracksController@getVersionList'); + Route::post('/albums/create', 'Api\Web\AlbumsController@postCreate'); Route::post('/albums/delete/{id}', 'Api\Web\AlbumsController@postDelete'); Route::post('/albums/edit/{id}', 'Api\Web\AlbumsController@postEdit'); diff --git a/app/Jobs/EncodeTrackFile.php b/app/Jobs/EncodeTrackFile.php index aba8f1b7..ad3e88e0 100644 --- a/app/Jobs/EncodeTrackFile.php +++ b/app/Jobs/EncodeTrackFile.php @@ -22,15 +22,15 @@ namespace Poniverse\Ponyfm\Jobs; use Carbon\Carbon; +use Config; use DB; use File; -use Config; -use Log; -use Poniverse\Ponyfm\Exceptions\InvalidEncodeOptionsException; -use Illuminate\Queue\SerializesModels; -use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Bus\SelfHandling; use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Queue\SerializesModels; +use Log; +use Poniverse\Ponyfm\Exceptions\InvalidEncodeOptionsException; use Poniverse\Ponyfm\Models\Track; use Poniverse\Ponyfm\Models\TrackFile; use Symfony\Component\Process\Exception\ProcessFailedException; @@ -47,6 +47,10 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue * @var */ protected $isExpirable; + /** + * @var bool + */ + protected $autoPublishWhenComplete; /** * @var bool */ @@ -54,16 +58,17 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue /** * @var bool */ - protected $autoPublishWhenComplete; + protected $isReplacingTrack; /** * 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 + * @param bool $isForUpload indicates whether this encode job is for an upload + * @param bool $isReplacingTrack */ - public function __construct(TrackFile $trackFile, $isExpirable, $isForUpload = false, $autoPublish = false) + public function __construct(TrackFile $trackFile, $isExpirable, $autoPublish = false, $isForUpload = false, $isReplacingTrack = false) { if ( (!$isForUpload && $trackFile->is_master) || @@ -74,8 +79,9 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue $this->trackFile = $trackFile; $this->isExpirable = $isExpirable; - $this->isForUpload = $isForUpload; $this->autoPublishWhenComplete = $autoPublish; + $this->isForUpload = $isForUpload; + $this->isReplacingTrack = $isReplacingTrack; } /** @@ -92,7 +98,7 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue Log::warning('Track file #'.$this->trackFile->id.' (track #'.$this->trackFile->track_id.') is already being processed!'); return; - } elseif (!$this->trackFile->is_expired) { + } elseif (!$this->trackFile->is_expired && File::exists($this->trackFile->getFile())) { Log::warning('Track file #'.$this->trackFile->id.' (track #'.$this->trackFile->track_id.') is still valid! No need to re-encode it.'); return; } @@ -103,11 +109,11 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue // Use the track's master file as the source if ($this->isForUpload) { - $source = $this->trackFile->track->getTemporarySourceFile(); - + $source = $this->trackFile->track->getTemporarySourceFileForVersion($this->trackFile->version); } else { $source = TrackFile::where('track_id', $this->trackFile->track_id) ->where('is_master', true) + ->where('version', $this->trackFile->version) ->first() ->getFile(); } @@ -151,7 +157,7 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue $this->trackFile->status = TrackFile::STATUS_NOT_BEING_PROCESSED; $this->trackFile->save(); - if ($this->isForUpload) { + if ($this->isForUpload || $this->isReplacingTrack) { if (!$this->trackFile->is_master && $this->trackFile->is_cacheable) { File::delete($this->trackFile->getFile()); } @@ -166,7 +172,29 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue $this->trackFile->track->save(); } - File::delete($this->trackFile->track->getTemporarySourceFile()); + if ($this->isReplacingTrack) { + $oldVersion = $this->trackFile->track->current_version; + + // Update the version of the track being uploaded + $this->trackFile->track->duration = \AudioCache::get($source)->getDuration(); + $this->trackFile->track->current_version = $this->trackFile->version; + $this->trackFile->track->version_upload_status = Track::STATUS_COMPLETE; + $this->trackFile->track->update(); + + // Delete the non-master files for the old version + if ($oldVersion !== $this->trackFile->version) { + $trackFilesToDelete = $this->trackFile->track->trackFilesForVersion($oldVersion)->where('is_master', false)->get(); + foreach ($trackFilesToDelete as $trackFileToDelete) { + if (File::exists($trackFileToDelete->getFile())) { + File::delete($trackFileToDelete->getFile()); + } + } + } + } + + if ($this->isForUpload) { + File::delete($this->trackFile->track->getTemporarySourceFileForVersion($this->trackFile->version)); + } } } } @@ -181,5 +209,21 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue $this->trackFile->status = TrackFile::STATUS_PROCESSING_ERROR; $this->trackFile->expires_at = null; $this->trackFile->save(); + + if ($this->isReplacingTrack) { + // If a new version is being uploaded to replace a file, yet the upload fails, + // all track files for that version should be deleted as it would other clutter the version + if ($this->isForUpload) { + $trackFiles = $this->trackFile->track->trackFilesForVersion($this->trackFile->version)->get(); + foreach ($trackFiles as $trackFile) { + if (File::exists($trackFile->getFile())) { + File::delete($trackFile->getFile()); + } + $trackFile->delete(); + } + } + $this->trackFile->track->version_upload_status = Track::STATUS_ERROR; + $this->trackFile->track->update(); + } } } diff --git a/app/Models/Album.php b/app/Models/Album.php index 9b90b900..c91e097c 100644 --- a/app/Models/Album.php +++ b/app/Models/Album.php @@ -123,7 +123,8 @@ class Album extends Model implements Searchable, Commentable, Favouritable } public function trackFiles() { - return $this->hasManyThrough(TrackFile::class, Track::class, 'album_id', 'track_id'); + $trackIds = $this->tracks->lists('id'); + return TrackFile::join('tracks', 'tracks.current_version', '=', 'track_files.version')->whereIn('track_id', $trackIds); } public function comments():HasMany diff --git a/app/Models/Playlist.php b/app/Models/Playlist.php index a210457a..d869fb38 100644 --- a/app/Models/Playlist.php +++ b/app/Models/Playlist.php @@ -226,7 +226,7 @@ class Playlist extends Model implements Searchable, Commentable, Favouritable public function trackFiles() { $trackIds = $this->tracks->lists('id'); - return TrackFile::whereIn('track_id', $trackIds); + return TrackFile::join('tracks', 'tracks.current_version', '=', 'track_files.version')->whereIn('track_id', $trackIds); } public function users() diff --git a/app/Models/Track.php b/app/Models/Track.php index 1a2886d0..cd489ae3 100644 --- a/app/Models/Track.php +++ b/app/Models/Track.php @@ -536,10 +536,21 @@ class Track extends Model implements Searchable, Commentable, Favouritable } public function trackFiles() + { + return $this->hasMany(TrackFile::class)->where('version', $this->current_version); + } + + + public function trackFilesForAllVersions() { return $this->hasMany(TrackFile::class); } + public function trackFilesForVersion(int $version) + { + return $this->hasMany(TrackFile::class)->where('track_files.version', $version); + } + public function notifications() { return $this->morphMany(Activity::class, 'notification_type'); @@ -688,7 +699,7 @@ class Track extends Model implements Searchable, Commentable, Favouritable $format = self::$Formats[$format]; - return "{$this->id}.{$format['extension']}"; + return "{$this->id}-v{$this->current_version}.{$format['extension']}"; } public function getDownloadFilenameFor($format) @@ -715,7 +726,7 @@ class Track extends Model implements Searchable, Commentable, Favouritable $format = self::$Formats[$format]; - return "{$this->getDirectory()}/{$this->id}.{$format['extension']}"; + return "{$this->getDirectory()}/{$this->id}-v{$this->current_version}.{$format['extension']}"; } /** @@ -723,10 +734,11 @@ class Track extends Model implements Searchable, Commentable, Favouritable * This file is used during the upload process to generate the actual master * file stored by Pony.fm. * + * @param int $version * @return string */ - public function getTemporarySourceFile():string { - return Config::get('ponyfm.files_directory').'/queued-tracks/'.$this->id; + public function getTemporarySourceFileForVersion(int $version):string { + return Config::get('ponyfm.files_directory').'/queued-tracks/'.$this->id.'v'.$version; } @@ -914,4 +926,9 @@ class Track extends Model implements Searchable, Commentable, Favouritable public function getResourceType():string { return 'track'; } + + public function getNextVersion() + { + return $this->current_version + 1; + } } diff --git a/app/Models/TrackFile.php b/app/Models/TrackFile.php index a983f2b3..96073622 100644 --- a/app/Models/TrackFile.php +++ b/app/Models/TrackFile.php @@ -80,6 +80,11 @@ class TrackFile extends Model */ public static function findOrFailByExtension($trackId, $extension) { + $track = Track::find($trackId); + if (!$track) { + App::abort(404); + } + // find the extension's format $requestedFormatName = null; foreach (Track::$Formats as $name => $format) { @@ -96,6 +101,7 @@ class TrackFile extends Model with('track') ->where('track_id', $trackId) ->where('format', $requestedFormatName) + ->where('version', $track->current_version) ->first(); if ($trackFile === null) { @@ -148,11 +154,21 @@ class TrackFile extends Model } public function getFile() + { + return "{$this->getDirectory()}/{$this->track_id}-v{$this->version}.{$this->extension}"; + } + + public function getUnversionedFile() { return "{$this->getDirectory()}/{$this->track_id}.{$this->extension}"; } public function getFilename() + { + return "{$this->track_id}-v{$this->track->current_version}.{$this->extension}"; + } + + public function getUnversionedFilename() { return "{$this->track_id}.{$this->extension}"; } diff --git a/database/migrations/2016_08_07_185426_add_version_column_to_track_files_table.php b/database/migrations/2016_08_07_185426_add_version_column_to_track_files_table.php new file mode 100644 index 00000000..f9e3e2c2 --- /dev/null +++ b/database/migrations/2016_08_07_185426_add_version_column_to_track_files_table.php @@ -0,0 +1,49 @@ +. + */ + +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Migrations\Migration; + +class AddVersionColumnToTrackFilesTable extends Migration +{ + /** + * Run the migrations. + * + * @return void + */ + public function up() + { + Schema::table('track_files', function (Blueprint $table) { + $table->integer('version')->unsigned()->default(1); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('track_files', function (Blueprint $table) { + $table->dropColumn('version'); + }); + } +} diff --git a/database/migrations/2016_08_15_103625_add_version_column_to_tracks_table.php b/database/migrations/2016_08_15_103625_add_version_column_to_tracks_table.php new file mode 100644 index 00000000..9577b082 --- /dev/null +++ b/database/migrations/2016_08_15_103625_add_version_column_to_tracks_table.php @@ -0,0 +1,33 @@ +integer('current_version')->unsigned()->default(1); + $table->integer('version_upload_status')->unsigned()->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('tracks', function (Blueprint $table) { + $table->dropColumn('current_version'); + $table->dropColumn('version_upload_status'); + }); + } +}