[#9] Add functionality to add/change versions of tracks

This commit is contained in:
Kelvin Zhang 2016-08-24 12:48:02 +01:00 committed by Peter Deltchev
parent cf7bd8b9e6
commit 775de16bfe
14 changed files with 489 additions and 89 deletions

View file

@ -20,15 +20,15 @@
namespace Poniverse\Ponyfm\Commands; namespace Poniverse\Ponyfm\Commands;
use AudioCache;
use FFmpegMovie; use FFmpegMovie;
use File;
use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Support\Str;
use Poniverse\Ponyfm\Exceptions\InvalidEncodeOptionsException; use Poniverse\Ponyfm\Exceptions\InvalidEncodeOptionsException;
use Poniverse\Ponyfm\Jobs\EncodeTrackFile; use Poniverse\Ponyfm\Jobs\EncodeTrackFile;
use Poniverse\Ponyfm\Models\Track; use Poniverse\Ponyfm\Models\Track;
use Poniverse\Ponyfm\Models\TrackFile; use Poniverse\Ponyfm\Models\TrackFile;
use AudioCache;
use File;
use Illuminate\Support\Str;
use SplFileInfo; use SplFileInfo;
/** /**
@ -45,6 +45,9 @@ class GenerateTrackFilesCommand extends CommandBase
private $track; private $track;
private $autoPublish; private $autoPublish;
private $sourceFile; private $sourceFile;
private $isForUpload;
private $isReplacingTrack;
private $version;
protected static $_losslessFormats = [ protected static $_losslessFormats = [
'flac', 'flac',
@ -53,11 +56,14 @@ class GenerateTrackFilesCommand extends CommandBase
'alac' '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->track = $track;
$this->autoPublish = $autoPublish; $this->autoPublish = $autoPublish;
$this->sourceFile = $sourceFile; $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->is_master = true;
$trackFile->format = $masterFormat; $trackFile->format = $masterFormat;
$trackFile->track_id = $this->track->id; $trackFile->track_id = $this->track->id;
$trackFile->version = $this->version;
$trackFile->save(); $trackFile->save();
} }
@ -126,13 +133,14 @@ class GenerateTrackFilesCommand extends CommandBase
$trackFile->is_master = $name === 'FLAC' ? true : false; $trackFile->is_master = $name === 'FLAC' ? true : false;
$trackFile->format = $name; $trackFile->format = $name;
$trackFile->status = TrackFile::STATUS_PROCESSING_PENDING; $trackFile->status = TrackFile::STATUS_PROCESSING_PENDING;
$trackFile->version = $this->version;
if (in_array($name, Track::$CacheableFormats) && !$trackFile->is_master) { if (in_array($name, Track::$CacheableFormats) && !$trackFile->is_master) {
$trackFile->is_cacheable = true; $trackFile->is_cacheable = true;
} else { } else {
$trackFile->is_cacheable = false; $trackFile->is_cacheable = false;
} }
$this->track->trackFiles()->save($trackFile); $this->track->trackFilesForAllVersions()->save($trackFile);
// All TrackFile records we need are synchronously created // All TrackFile records we need are synchronously created
// before kicking off the encode jobs in order to avoid a race // before kicking off the encode jobs in order to avoid a race
@ -142,16 +150,31 @@ class GenerateTrackFilesCommand extends CommandBase
try { try {
foreach ($trackFiles as $trackFile) { 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) { } 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()]]); return CommandResponse::fail(['track' => [$e->getMessage()]]);
} }
} catch (\Exception $e) { } 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; throw $e;
} }
@ -168,7 +191,8 @@ class GenerateTrackFilesCommand extends CommandBase
* @param FFmpegMovie|string $file object or full path of the file we're checking * @param FFmpegMovie|string $file object or full path of the file we're checking
* @return bool whether the given file is lossless * @return bool whether the given file is lossless
*/ */
private function isLosslessFile($file) { private function isLosslessFile($file)
{
if (is_string($file)) { if (is_string($file)) {
$file = AudioCache::get($file); $file = AudioCache::get($file);
} }
@ -180,7 +204,8 @@ class GenerateTrackFilesCommand extends CommandBase
* @param string $format * @param string $format
* @return TrackFile|null * @return TrackFile|null
*/ */
private function trackFileExists(string $format) { private function trackFileExists(string $format)
return $this->track->trackFiles()->where('format', $format)->first(); {
return $this->track->trackFilesForAllVersions()->where('format', $format)->where('version', $this->version)->first();
} }
} }

View file

@ -27,7 +27,6 @@ use Gate;
use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Bus\DispatchesJobs;
use Input; use Input;
use Poniverse\Ponyfm\Models\Track; use Poniverse\Ponyfm\Models\Track;
use AudioCache;
use Poniverse\Ponyfm\Models\User; use Poniverse\Ponyfm\Models\User;
use Validator; use Validator;
@ -40,6 +39,21 @@ class UploadTrackCommand extends CommandBase
private $_allowShortTrack; private $_allowShortTrack;
private $_customTrackSource; private $_customTrackSource;
private $_autoPublishByDefault; 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. * UploadTrackCommand constructor.
@ -48,12 +62,16 @@ class UploadTrackCommand extends CommandBase
* @param bool $allowShortTrack allow tracks shorter than 30 seconds * @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 string|null $customTrackSource value to set in the track's "source" field; if left blank, "direct_upload" is used
* @param bool $autoPublishByDefault * @param bool $autoPublishByDefault
* @param int $version
* @param Track $track | null
*/ */
public function __construct( public function __construct(
bool $allowLossy = false, bool $allowLossy = false,
bool $allowShortTrack = false, bool $allowShortTrack = false,
string $customTrackSource = null, string $customTrackSource = null,
bool $autoPublishByDefault = false bool $autoPublishByDefault = false,
int $version = 1,
$track = null
) { ) {
$userSlug = Input::get('user_slug', null); $userSlug = Input::get('user_slug', null);
$this->_artist = $this->_artist =
@ -65,14 +83,9 @@ class UploadTrackCommand extends CommandBase
$this->_allowShortTrack = $allowShortTrack; $this->_allowShortTrack = $allowShortTrack;
$this->_customTrackSource = $customTrackSource; $this->_customTrackSource = $customTrackSource;
$this->_autoPublishByDefault = $autoPublishByDefault; $this->_autoPublishByDefault = $autoPublishByDefault;
} $this->_version = $version;
$this->_track = $track;
/** $this->_isReplacingTrack = $this->_track !== null && $version > 1;
* @return bool
*/
public function authorize()
{
return Gate::allows('create-track', $this->_artist);
} }
/** /**
@ -82,22 +95,32 @@ class UploadTrackCommand extends CommandBase
public function execute() public function execute()
{ {
$trackFile = Input::file('track', null); $trackFile = Input::file('track', null);
$coverFile = Input::file('cover', null); if (!$this->_isReplacingTrack) {
$coverFile = Input::file('cover', null);
}
if (null === $trackFile) { 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!']]); return CommandResponse::fail(['track' => ['You must upload an audio file!']]);
} }
$audio = \AudioCache::get($trackFile->getPathname()); $audio = \AudioCache::get($trackFile->getPathname());
$track = new Track(); if (!$this->_isReplacingTrack) {
$track->user_id = $this->_artist->id; $this->_track = new Track();
// The title set here is a placeholder; it'll be replaced by ParseTrackTagsCommand $this->_track->user_id = $this->_artist->id;
// if the file contains a title tag. // The title set here is a placeholder; it'll be replaced by ParseTrackTagsCommand
$track->title = Input::get('title', pathinfo($trackFile->getClientOriginalName(), PATHINFO_FILENAME)); // if the file contains a title tag.
$track->duration = $audio->getDuration(); $this->_track->title = Input::get('title', pathinfo($trackFile->getClientOriginalName(), PATHINFO_FILENAME));
$track->save(); // The duration/version of the track cannot be changed until the encoding is successful
$track->ensureDirectoryExists(); $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')) { if (!is_dir(Config::get('ponyfm.files_directory').'/tmp')) {
mkdir(Config::get('ponyfm.files_directory').'/tmp', 0755, true); 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')) { if (!is_dir(Config::get('ponyfm.files_directory').'/queued-tracks')) {
mkdir(Config::get('ponyfm.files_directory').'/queued-tracks', 0755, true); 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 = Input::all();
$input['track'] = $trackFile; $input['track'] = $trackFile;
$input['cover'] = $coverFile; if (!$this->_isReplacingTrack) {
$input['cover'] = $coverFile;
}
$validator = \Validator::make($input, [ $rules = [
'track' => 'track' =>
'required|' 'required|'
. ($this->_allowLossy . ($this->_allowLossy
@ -120,44 +145,61 @@ class UploadTrackCommand extends CommandBase
: 'audio_format:flac,alac,pcm,adpcm|') : 'audio_format:flac,alac,pcm,adpcm|')
. ($this->_allowShortTrack ? '' : 'min_duration:30|') . ($this->_allowShortTrack ? '' : 'min_duration:30|')
. 'audio_channels:1,2', . 'audio_channels:1,2',
];
'auto_publish' => 'boolean', if (!$this->_isReplacingTrack) {
'title' => 'string', array_push($rules, [
'track_type_id' => 'exists:track_types,id', 'cover' => 'image|mimes:png,jpeg|min_width:350|min_height:350',
'genre' => 'string', 'auto_publish' => 'boolean',
'album' => 'string', 'title' => 'string',
'track_number' => 'integer', 'track_type_id' => 'exists:track_types,id',
'released_at' => 'date_format:'.Carbon::ISO8601, 'genre' => 'string',
'description' => 'string', 'album' => 'string',
'lyrics' => 'string', 'track_number' => 'integer',
'is_vocal' => 'boolean', 'released_at' => 'date_format:'.Carbon::ISO8601,
'is_explicit' => 'boolean', 'description' => 'string',
'is_downloadable' => 'boolean', 'lyrics' => 'string',
'is_listed' => 'boolean', 'is_vocal' => 'boolean',
'cover' => 'image|mimes:png,jpeg|min_width:350|min_height:350', 'is_explicit' => 'boolean',
'metadata' => 'json', 'is_downloadable' => 'boolean',
]); 'is_listed' => 'boolean',
'metadata' => 'json'
]);
}
$validator = \Validator::make($input, $rules);
if ($validator->fails()) { 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); 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 if (!$this->_isReplacingTrack) {
// string with quotes when storing it in the database, which breaks things. // If json_decode() isn't called here, Laravel will surround the JSON
$track->metadata = json_decode(Input::get('metadata', null)); // string with quotes when storing it in the database, which breaks things.
$track->save(); $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. if (!$this->_isReplacingTrack) {
$parseTagsCommand = new ParseTrackTagsCommand($track, $trackFile, $input); // Parse any tags in the uploaded files.
$result = $parseTagsCommand->execute(); $parseTagsCommand = new ParseTrackTagsCommand($this->_track, $trackFile, $input);
if ($result->didFail()) { $result = $parseTagsCommand->execute();
return $result; 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(); return $generateTrackFiles->execute();
} }
} }

View file

@ -72,7 +72,7 @@ class RebuildTrack extends Command
$track->restore(); $track->restore();
$this->info("Attempting to finish this track's upload..."); $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); $generateTrackFiles = new GenerateTrackFilesCommand($track, $sourceFile, false);
$result = $generateTrackFiles->execute(); $result = $generateTrackFiles->execute();
// The GenerateTrackFiles command will re-encode all TrackFiles. // The GenerateTrackFiles command will re-encode all TrackFiles.

View file

@ -0,0 +1,97 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Kelvin Zhang
*
* 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\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.');
}
}
}

View file

@ -45,6 +45,7 @@ class Kernel extends ConsoleKernel
\Poniverse\Ponyfm\Console\Commands\RebuildSearchIndex::class, \Poniverse\Ponyfm\Console\Commands\RebuildSearchIndex::class,
\Poniverse\Ponyfm\Console\Commands\MergeAccounts::class, \Poniverse\Ponyfm\Console\Commands\MergeAccounts::class,
\Poniverse\Ponyfm\Console\Commands\FixMLPMAImages::class, \Poniverse\Ponyfm\Console\Commands\FixMLPMAImages::class,
\Poniverse\Ponyfm\Console\Commands\VersionFiles::class
]; ];
/** /**

View file

@ -20,23 +20,24 @@
namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; namespace Poniverse\Ponyfm\Http\Controllers\Api\Web;
use Illuminate\Database\Eloquent\ModelNotFoundException; use Auth;
use File; use File;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Input;
use Poniverse\Ponyfm\Commands\DeleteTrackCommand; use Poniverse\Ponyfm\Commands\DeleteTrackCommand;
use Poniverse\Ponyfm\Commands\EditTrackCommand; use Poniverse\Ponyfm\Commands\EditTrackCommand;
use Poniverse\Ponyfm\Commands\GenerateTrackFilesCommand;
use Poniverse\Ponyfm\Commands\UploadTrackCommand; use Poniverse\Ponyfm\Commands\UploadTrackCommand;
use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase;
use Poniverse\Ponyfm\Jobs\EncodeTrackFile; use Poniverse\Ponyfm\Jobs\EncodeTrackFile;
use Poniverse\Ponyfm\Models\Genre; use Poniverse\Ponyfm\Models\Genre;
use Poniverse\Ponyfm\Models\ResourceLogItem; use Poniverse\Ponyfm\Models\ResourceLogItem;
use Poniverse\Ponyfm\Models\TrackFile;
use Poniverse\Ponyfm\Models\Track; use Poniverse\Ponyfm\Models\Track;
use Poniverse\Ponyfm\Models\TrackType; use Poniverse\Ponyfm\Models\TrackType;
use Poniverse\Ponyfm\Models\TrackTypes; use Poniverse\Ponyfm\Models\TrackFile;
use Auth;
use Input;
use Poniverse\Ponyfm\Models\User; use Poniverse\Ponyfm\Models\User;
use Response; use Response;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class TracksController extends ApiControllerBase class TracksController extends ApiControllerBase
{ {
@ -74,6 +75,75 @@ class TracksController extends ApiControllerBase
return $this->execute(new EditTrackCommand($id, Input::all())); 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) public function getShow($id)
{ {
$track = Track::userDetails()->withComments()->find($id); $track = Track::userDetails()->withComments()->find($id);

View file

@ -114,6 +114,11 @@ Route::group(['prefix' => 'api/web'], function() {
Route::post('/tracks/delete/{id}', 'Api\Web\TracksController@postDelete'); Route::post('/tracks/delete/{id}', 'Api\Web\TracksController@postDelete');
Route::post('/tracks/edit/{id}', 'Api\Web\TracksController@postEdit'); 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/create', 'Api\Web\AlbumsController@postCreate');
Route::post('/albums/delete/{id}', 'Api\Web\AlbumsController@postDelete'); Route::post('/albums/delete/{id}', 'Api\Web\AlbumsController@postDelete');
Route::post('/albums/edit/{id}', 'Api\Web\AlbumsController@postEdit'); Route::post('/albums/edit/{id}', 'Api\Web\AlbumsController@postEdit');

View file

@ -22,15 +22,15 @@
namespace Poniverse\Ponyfm\Jobs; namespace Poniverse\Ponyfm\Jobs;
use Carbon\Carbon; use Carbon\Carbon;
use Config;
use DB; use DB;
use File; 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\Bus\SelfHandling;
use Illuminate\Contracts\Queue\ShouldQueue; 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\Track;
use Poniverse\Ponyfm\Models\TrackFile; use Poniverse\Ponyfm\Models\TrackFile;
use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Exception\ProcessFailedException;
@ -47,6 +47,10 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue
* @var * @var
*/ */
protected $isExpirable; protected $isExpirable;
/**
* @var bool
*/
protected $autoPublishWhenComplete;
/** /**
* @var bool * @var bool
*/ */
@ -54,16 +58,17 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue
/** /**
* @var bool * @var bool
*/ */
protected $autoPublishWhenComplete; protected $isReplacingTrack;
/** /**
* Create a new job instance. * Create a new job instance.
* @param TrackFile $trackFile * @param TrackFile $trackFile
* @param bool $isExpirable * @param bool $isExpirable
* @param bool $isForUpload indicates whether this encode job is for an upload
* @param bool $autoPublish * @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 ( if (
(!$isForUpload && $trackFile->is_master) || (!$isForUpload && $trackFile->is_master) ||
@ -74,8 +79,9 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue
$this->trackFile = $trackFile; $this->trackFile = $trackFile;
$this->isExpirable = $isExpirable; $this->isExpirable = $isExpirable;
$this->isForUpload = $isForUpload;
$this->autoPublishWhenComplete = $autoPublish; $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!'); Log::warning('Track file #'.$this->trackFile->id.' (track #'.$this->trackFile->track_id.') is already being processed!');
return; 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.'); Log::warning('Track file #'.$this->trackFile->id.' (track #'.$this->trackFile->track_id.') is still valid! No need to re-encode it.');
return; return;
} }
@ -103,11 +109,11 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue
// Use the track's master file as the source // Use the track's master file as the source
if ($this->isForUpload) { if ($this->isForUpload) {
$source = $this->trackFile->track->getTemporarySourceFile(); $source = $this->trackFile->track->getTemporarySourceFileForVersion($this->trackFile->version);
} else { } else {
$source = TrackFile::where('track_id', $this->trackFile->track_id) $source = TrackFile::where('track_id', $this->trackFile->track_id)
->where('is_master', true) ->where('is_master', true)
->where('version', $this->trackFile->version)
->first() ->first()
->getFile(); ->getFile();
} }
@ -151,7 +157,7 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue
$this->trackFile->status = TrackFile::STATUS_NOT_BEING_PROCESSED; $this->trackFile->status = TrackFile::STATUS_NOT_BEING_PROCESSED;
$this->trackFile->save(); $this->trackFile->save();
if ($this->isForUpload) { if ($this->isForUpload || $this->isReplacingTrack) {
if (!$this->trackFile->is_master && $this->trackFile->is_cacheable) { if (!$this->trackFile->is_master && $this->trackFile->is_cacheable) {
File::delete($this->trackFile->getFile()); File::delete($this->trackFile->getFile());
} }
@ -166,7 +172,29 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue
$this->trackFile->track->save(); $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->status = TrackFile::STATUS_PROCESSING_ERROR;
$this->trackFile->expires_at = null; $this->trackFile->expires_at = null;
$this->trackFile->save(); $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();
}
} }
} }

View file

@ -123,7 +123,8 @@ class Album extends Model implements Searchable, Commentable, Favouritable
} }
public function trackFiles() { 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 public function comments():HasMany

View file

@ -226,7 +226,7 @@ class Playlist extends Model implements Searchable, Commentable, Favouritable
public function trackFiles() public function trackFiles()
{ {
$trackIds = $this->tracks->lists('id'); $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() public function users()

View file

@ -536,10 +536,21 @@ class Track extends Model implements Searchable, Commentable, Favouritable
} }
public function trackFiles() public function trackFiles()
{
return $this->hasMany(TrackFile::class)->where('version', $this->current_version);
}
public function trackFilesForAllVersions()
{ {
return $this->hasMany(TrackFile::class); return $this->hasMany(TrackFile::class);
} }
public function trackFilesForVersion(int $version)
{
return $this->hasMany(TrackFile::class)->where('track_files.version', $version);
}
public function notifications() public function notifications()
{ {
return $this->morphMany(Activity::class, 'notification_type'); return $this->morphMany(Activity::class, 'notification_type');
@ -688,7 +699,7 @@ class Track extends Model implements Searchable, Commentable, Favouritable
$format = self::$Formats[$format]; $format = self::$Formats[$format];
return "{$this->id}.{$format['extension']}"; return "{$this->id}-v{$this->current_version}.{$format['extension']}";
} }
public function getDownloadFilenameFor($format) public function getDownloadFilenameFor($format)
@ -715,7 +726,7 @@ class Track extends Model implements Searchable, Commentable, Favouritable
$format = self::$Formats[$format]; $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 * This file is used during the upload process to generate the actual master
* file stored by Pony.fm. * file stored by Pony.fm.
* *
* @param int $version
* @return string * @return string
*/ */
public function getTemporarySourceFile():string { public function getTemporarySourceFileForVersion(int $version):string {
return Config::get('ponyfm.files_directory').'/queued-tracks/'.$this->id; 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 { public function getResourceType():string {
return 'track'; return 'track';
} }
public function getNextVersion()
{
return $this->current_version + 1;
}
} }

View file

@ -80,6 +80,11 @@ class TrackFile extends Model
*/ */
public static function findOrFailByExtension($trackId, $extension) public static function findOrFailByExtension($trackId, $extension)
{ {
$track = Track::find($trackId);
if (!$track) {
App::abort(404);
}
// find the extension's format // find the extension's format
$requestedFormatName = null; $requestedFormatName = null;
foreach (Track::$Formats as $name => $format) { foreach (Track::$Formats as $name => $format) {
@ -96,6 +101,7 @@ class TrackFile extends Model
with('track') with('track')
->where('track_id', $trackId) ->where('track_id', $trackId)
->where('format', $requestedFormatName) ->where('format', $requestedFormatName)
->where('version', $track->current_version)
->first(); ->first();
if ($trackFile === null) { if ($trackFile === null) {
@ -148,11 +154,21 @@ class TrackFile extends Model
} }
public function getFile() 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}"; return "{$this->getDirectory()}/{$this->track_id}.{$this->extension}";
} }
public function getFilename() public function getFilename()
{
return "{$this->track_id}-v{$this->track->current_version}.{$this->extension}";
}
public function getUnversionedFilename()
{ {
return "{$this->track_id}.{$this->extension}"; return "{$this->track_id}.{$this->extension}";
} }

View file

@ -0,0 +1,49 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Kelvin Zhang
*
* 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 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');
});
}
}

View file

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddVersionColumnToTracksTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('tracks', function (Blueprint $table) {
$table->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');
});
}
}