diff --git a/.gitignore b/.gitignore index eb512dfd..ed4f76ef 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ _ide_helper.php resources/views/emails/html npm-debug.log yarn-error.log +/composer.phar diff --git a/app/Commands/UploadTrackCommand.php b/app/Commands/UploadTrackCommand.php index 2ccee33c..c93fc742 100644 --- a/app/Commands/UploadTrackCommand.php +++ b/app/Commands/UploadTrackCommand.php @@ -145,7 +145,9 @@ class UploadTrackCommand extends CommandBase $input = Request::all(); $input['track'] = $trackFile; - if (!$this->_isReplacingTrack) { + + // Prevent the setting of the cover index for validation + if (!$this->_isReplacingTrack && isset($coverFile)) { $input['cover'] = $coverFile; } @@ -159,7 +161,7 @@ class UploadTrackCommand extends CommandBase . 'audio_channels:1,2', ]; if (!$this->_isReplacingTrack) { - array_push($rules, [ + array_merge($rules, [ 'cover' => 'image|mimes:png,jpeg|min_width:350|min_height:350', 'auto_publish' => 'boolean', 'title' => 'string', @@ -198,6 +200,12 @@ class UploadTrackCommand extends CommandBase $this->_track->source = $this->_customTrackSource ?? $source; $this->_track->save(); + // If the cover was null, and not included, add it back in as null so that + // other commands do not encounter a undefined index. + if (! isset($input['cover'])) { + $input['cover'] = null; + } + if (!$this->_isReplacingTrack) { // Parse any tags in the uploaded files. $parseTagsCommand = new ParseTrackTagsCommand($this->_track, $trackFile, $input); diff --git a/app/Console/Commands/RebuildImages.php b/app/Console/Commands/RebuildImages.php new file mode 100644 index 00000000..408af47a --- /dev/null +++ b/app/Console/Commands/RebuildImages.php @@ -0,0 +1,78 @@ +. + */ + +namespace Poniverse\Ponyfm\Console\Commands; + +use Illuminate\Console\Command; +use Poniverse\Ponyfm\Models\Image; +use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; +use Symfony\Component\HttpFoundation\File\File; + +class RebuildImages extends Command +{ + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'rebuild:images'; + + /** + * The console command description. + * + * @var string + */ + protected $description = 'Resizes all images to fit the specifications in Models/Image'; + + /** + * Create a new command instance. + * + * @return void + */ + public function __construct() + { + parent::__construct(); + } + + /** + * Execute the console command. + * + * @return void + */ + public function handle() + { + $this->info("Regenerating Images"); + $progressBar = $this->output->createProgressBar(Image::count()); + + Image::chunk(1000, function($images) use ($progressBar) { + foreach ($images as $image) { + try { + $image->buildCovers(); + } catch (FileNotFoundException $e) { + $name = $image->filename; + $id = $image->id; + + $this->error("Unable to process image $name (id: $id): ".$e->getMessage()); + } + + $progressBar->advance(); + } + }); + } +} diff --git a/app/Models/Image.php b/app/Models/Image.php index 0bf18079..f1feef37 100644 --- a/app/Models/Image.php +++ b/app/Models/Image.php @@ -24,6 +24,8 @@ use External; use Illuminate\Database\Eloquent\Model; use Config; use Illuminate\Support\Str; +use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; +use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\HttpFoundation\File\UploadedFile; /** @@ -57,12 +59,14 @@ class Image extends Model const SMALL = 4; public static $ImageTypes = [ - self::NORMAL => ['id' => self::NORMAL, 'name' => 'normal', 'width' => 350, 'height' => 350], - self::ORIGINAL => ['id' => self::ORIGINAL, 'name' => 'original', 'width' => null, 'height' => null], - self::SMALL => ['id' => self::SMALL, 'name' => 'small', 'width' => 100, 'height' => 100], - self::THUMBNAIL => ['id' => self::THUMBNAIL, 'name' => 'thumbnail', 'width' => 50, 'height' => 50] + self::NORMAL => ['id' => self::NORMAL, 'name' => 'normal', 'width' => 350, 'height' => 350, 'geometry' => '350'], + self::ORIGINAL => ['id' => self::ORIGINAL, 'name' => 'original', 'width' => null, 'height' => null, 'geometry' => null], + self::SMALL => ['id' => self::SMALL, 'name' => 'small', 'width' => 100, 'height' => 100, 'geometry' => '100x100^'], + self::THUMBNAIL => ['id' => self::THUMBNAIL, 'name' => 'thumbnail', 'width' => 50, 'height' => 50, 'geometry' => '50x50^'] ]; + const MIME_JPEG = 'image/jpeg'; + public static function getImageTypeFromName($name) { foreach (self::$ImageTypes as $cover) { @@ -95,17 +99,7 @@ class Image extends Model if ($image) { if ($forceReupload) { - // delete existing versions of the image - $filenames = scandir($image->getDirectory()); - $imagePrefix = $image->id.'_'; - - $filenames = array_filter($filenames, function (string $filename) use ($imagePrefix) { - return Str::startsWith($filename, $imagePrefix); - }); - - foreach ($filenames as $filename) { - unlink($image->getDirectory().'/'.$filename); - } + $image->clearExisting(true); } else { return $image; } @@ -124,27 +118,7 @@ class Image extends Model $image->ensureDirectoryExists(); foreach (self::$ImageTypes as $coverType) { - if ($coverType['id'] === self::ORIGINAL && $image->mime === 'image/jpeg') { - $command = 'cp "'.$file->getPathname().'" '.$image->getFile($coverType['id']); - } else { - // ImageMagick options reference: http://www.imagemagick.org/script/command-line-options.php - $command = 'convert 2>&1 "'.$file->getPathname().'" -background white -alpha remove -alpha off -strip'; - - if ($image->mime === 'image/jpeg') { - $command .= ' -quality 100 -format jpeg'; - } else { - $command .= ' -quality 95 -format png'; - } - - if (isset($coverType['width']) && isset($coverType['height'])) { - $command .= " -thumbnail ${coverType['width']}x${coverType['height']}^ -gravity center -extent ${coverType['width']}x${coverType['height']}"; - } - - $command .= ' "'.$image->getFile($coverType['id']).'"'; - } - - External::execute($command); - chmod($image->getFile($coverType['id']), 0644); + self::processFile($file, $image->getFile($coverType['id']), $coverType); } return $image; @@ -154,6 +128,37 @@ class Image extends Model } } + /** + * Converts the image into the specified cover type to the specified path. + * + * @param File $image The image file to be processed + * @param string $path The path to save the processed image file + * @param array $coverType The type to process the image to + */ + private static function processFile(File $image, string $path, $coverType) { + if ($coverType['id'] === self::ORIGINAL && $image->getMimeType() === self::MIME_JPEG) { + $command = 'cp "'.$image->getPathname().'" '.$path; + } else { + // ImageMagick options reference: http://www.imagemagick.org/script/command-line-options.php + $command = 'convert 2>&1 "'.$image->getPathname().'" -background white -alpha remove -alpha off -strip'; + + if ($image->getMimeType() === self::MIME_JPEG) { + $command .= ' -quality 100 -format jpeg'; + } else { + $command .= ' -quality 95 -format png'; + } + + if (isset($coverType['geometry'])) { + $command .= " -gravity center -thumbnail ${coverType['geometry']} -extent ${coverType['geometry']}"; + } + + $command .= ' "'.$path.'"'; + } + + External::execute($command); + chmod($path, 0644); + } + protected $table = 'images'; public function getUrl($type = self::NORMAL) @@ -191,4 +196,40 @@ class Image extends Model mkdir($destination, 0777, true); } } + + /** + * Deletes any generated files if they exist + * @param bool $includeOriginal Set to true if the original image should be deleted as well. + */ + public function clearExisting(bool $includeOriginal = false) { + $files = scandir($this->getDirectory()); + $filePrefix = $this->id.'_'; + $originalName = $filePrefix.Image::$ImageTypes[Image::ORIGINAL]['name']; + + $files = array_filter($files, function($file) use ($originalName, $includeOriginal, $filePrefix) { + if (Str::startsWith($file,$originalName) && !$includeOriginal) { + return false; + } + else { + return (Str::startsWith($file, $filePrefix)); + } + }); + + foreach ($files as $file) { + unlink($this->getDirectory().'/'.$file); + } + } + + /** + * Builds the cover images for the image, overwriting if needed. + * + * @throws FileNotFoundException If the original file cannot be found. + */ + public function buildCovers() { + $originalFile = new File($this->getFile(self::ORIGINAL)); + + foreach (self::$ImageTypes as $imageType) { + self::processFile($originalFile, $this->getFile($imageType['id']), $imageType); + } + } }