Skip to content

Commit

Permalink
Fixes #1208 (#1212)
Browse files Browse the repository at this point in the history
* Fixed console command `takedate`.

* Sync @kamil4's latest front-end changes while we are on it.

* Modified console command as discussed during review.

* Fixed some bugs

* Made output prettier.
  • Loading branch information
nagmat84 authored Feb 7, 2022
1 parent 8c6b200 commit 373382b
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 71 deletions.
12 changes: 0 additions & 12 deletions app/Actions/Photo/Strategies/AddBaseStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,18 +149,6 @@ protected function putSourceIntoFinalDestination(string $targetPath): void
} else {
$targetFile->write($sourceFile->read());
$sourceFile->close();
// Set original date
if ($isTargetLocal && $this->photo->taken_at !== null) {
// I wonder if Flysystem is really the right choice for use
// given the fact, that it lacks many of the features we need
// such that we need to fall back to low-level PHP methods
// all the time (for symlinks, setting timestamps, etc.)
// Also, the head maintainer seem very reluctant of
// integrating new features:
// - For setting timestamps: https://github.com/thephpleague/flysystem/issues/920
// - For symlinks: https://github.com/thephpleague/flysystem/issues/599
touch($targetFile->getAbsolutePath(), $this->photo->taken_at->getTimestamp());
}
if ($this->parameters->importMode->shallDeleteImported()) {
// This may throw an exception, if the original has been
// readable, but is not writable
Expand Down
190 changes: 137 additions & 53 deletions app/Console/Commands/Takedate.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,31 @@
use App\Metadata\Extractor;
use App\Models\Photo;
use App\Models\SizeVariant;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\ConsoleSectionOutput;

class Takedate extends Command
{
private ConsoleSectionOutput $msgSection;
private ProgressBar $progressBar;

private const DATETIME_FORMAT = 'Y-m-d \a\t H:i:s (e)';

/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'lychee:takedate' .
'{from=0 : index of first record}' .
'{nb=5 : number of records to retrieve (0 to retrieve all)}' .
'{tm=600 : maximum execution time (in seconds)}' .
'{--timestamp : use timestamps of media files if exif data missing}' .
'{--force : force processing of all media files}';
'{offset=0 : offset of the first photo to process}' .
'{limit=50 : number of photos to process (0 means process all)}' .
'{time=600 : maximum execution time in seconds (0 means unlimited)}' .
'{--c|set-upload-time : additionally sets the upload time based on the creation time of the media file; ATTENTION: this option is rarely needed and potentially harmful}' .
'{--f|force : force processing of all media files}';

/**
* The console command description.
Expand All @@ -29,80 +38,155 @@ class Takedate extends Command
*/
protected $description = 'Update missing takedate entries from exif data';

public function __construct()
{
parent::__construct();
$output = new ConsoleOutput();
// Create an independent section for message _above_ the section
// which holds the progress bar.
// This way the progress bar remains on the bottom in case too
// many warning/errors are spit out.
$this->msgSection = $output->section();
$this->progressBar = new ProgressBar($output->section());
$this->progressBar->setFormat('Photo %current%/%max% [%bar%] %percent:3s%%');
}

/**
* Outputs an error message.
*
* @param string $msg the message
*
* @return void
*/
private function printError(Photo $photo, string $msg): void
{
$this->msgSection->writeln('<error>Error:</error> Photo "' . $photo->title . '" (ID=' . $photo->id . '): ' . $msg);
}

/**
* Outputs an warning.
*
* @param string $msg the message
*
* @return void
*/
private function printWarning(Photo $photo, string $msg): void
{
$this->msgSection->writeln('<comment>Warning:</comment> Photo "' . $photo->title . '" (ID=' . $photo->id . '): ' . $msg);
}

/**
* Outputs an informational message.
*
* @param string $msg the message
*
* @return void
*/
private function printInfo(Photo $photo, string $msg): void
{
$this->msgSection->writeln('<info>Info:</info> Photo "' . $photo->title . '" (ID=' . $photo->id . '): ' . $msg);
}

/**
* Execute the console command.
*
* @return mixed
* @return int
*/
public function handle(Extractor $metadataExtractor)
public function handle(Extractor $metadataExtractor): int
{
$argument = $this->argument('nb');
$from = $this->argument('from');
$timeout = $this->argument('tm');
$timestamps = $this->option('timestamp');
$force = $this->option('force');
$limit = intval($this->argument('limit'));
$offset = intval($this->argument('offset'));
$timeout = intval($this->argument('time'));
$setCreationTime = boolval($this->option('set-upload-time'));
$force = boolval($this->option('force'));
set_time_limit($timeout);

if ($argument == 0) {
$argument = PHP_INT_MAX;
}
// For faster iteration we eagerly load the original size variant,
// but only the original size variant
$photoQuery = Photo::with(['size_variants' => function (HasMany $r) {
$r->where('type', '=', SizeVariant::ORIGINAL);
}]);
if ($force) {
$photos = $photoQuery->offset($from)->limit($argument)->get();
} else {
$photos = $photoQuery->whereNull('taken_at')->offset($from)->limit($argument)->get();

if (!$force) {
$photoQuery->whereNull('taken_at');
}
if (count($photos) == 0) {

// ATTENTION: We must call `count` first, otherwise `offset` and
// `limit` won't have an effect.
$count = $photoQuery->count();
if ($count === 0) {
$this->line('No pictures require takedate updates.');

return false;
return -1;
}

$i = $from - 1;
// We must stipulate a particular order, otherwise `offset` and `limit` have random effects
$photoQuery->orderBy('id');

if ($offset !== 0) {
$photoQuery->offset($offset);
}

if ($limit !== 0) {
$photoQuery->limit($limit);
}

$this->progressBar->setMaxSteps($limit === 0 ? $count : min($count, $limit));

// Unfortunately, `->getLazy` ignores `offset` and `limit`, so we must
// use a regular collection which might run out of memory for large
// values of `limit`.
$photos = $photoQuery->get();
/* @var Photo $photo */
foreach ($photos as $photo) {
$fullPath = $photo->full_path;
$i++;
$this->progressBar->advance();
// TODO: As soon as we support AWS S3 storage, we must stop using absolute paths. However, first the EXIF extractor must be rewritten to use file streams.
$fullPath = $photo->size_variants->getOriginal()->getFile()->getAbsolutePath();

if (!file_exists($fullPath)) {
$this->line($i . ': File ' . $fullPath . ' not found for ' . $photo->title . '.');
$this->printError($photo, 'Media file ' . $fullPath . ' not found');
continue;
}
$info = $metadataExtractor->extract($fullPath, $photo->type);
/* @var \DateTime $stamp */

$kind = $photo->isRaw() ? 'raw' : ($photo->isVideo() ? 'video' : 'photo');
$info = $metadataExtractor->extract($fullPath, $kind);
/* @var Carbon $stamp */
$stamp = $info['taken_at'];
if ($stamp != null) {
if ($stamp == $photo->takestamp) {
$this->line($i . ': Takestamp up to date for ' . $photo->title);
continue;
}
$photo->taken_at = $stamp;
if ($photo->save()) {
$this->line($i . ': Takestamp updated to ' . $stamp->format('d M Y \a\t H:i') . ' for ' . $photo->title);
if ($stamp !== null) {
// Note: `equalTo` only checks if two times indicate the same
// instant of time on the universe's timeline, i.e. equality
// comparison is always done in UTC.
// For example "2022-01-31 20:50 CET" is deemed equal to
// "2022-01-31 19:50 GMT".
// So, we must check for equality of timezones separately.
if ($photo->taken_at->equalTo($stamp) && $photo->taken_at->timezoneName === $stamp->timezoneName) {
$this->printInfo($photo, 'Takestamp up-to-date.');
} else {
$this->line($i . ': Failed to update takestamp for ' . $photo->title);
$photo->taken_at = $stamp;
$this->printInfo($photo, 'Takestamp set to ' . $photo->taken_at->format(self::DATETIME_FORMAT) . '.');
}
continue;
}
if (!$timestamps) {
$this->line($i . ': Failed to get Takestamp data for ' . $photo->title . '.');
continue;
}
if (is_link($fullPath)) {
$fullPath = readlink($fullPath);
} else {
$this->printWarning($photo, 'Failed to extract takestamp data from media file.');
}
$created_at = filemtime($fullPath);
if ($created_at == $photo->created_at->timestamp) {
$this->line($i . ': Created_at up to date for ' . $photo->title);
continue;

if ($setCreationTime) {
if (is_link($fullPath)) {
$fullPath = readlink($fullPath);
}
$created_at = filemtime($fullPath);
if ($created_at == $photo->created_at->timestamp) {
$this->printInfo($photo, 'Upload time up-to-date.');
} else {
$photo->created_at = Carbon::createFromTimestamp($created_at);
$this->printInfo($photo, 'Upload time set to ' . $photo->created_at->format(self::DATETIME_FORMAT) . '.');
}
}
$photo->created_at->setTimestamp($created_at);
if ($photo->save()) {
$this->line($i . ': Created_at updated to ' . $photo->created_at->format('d M Y \a\t H:i') . ' for ' . $photo->title);
} else {
$this->line($i . ': Failed to update created_at for ' . $photo->title);

if (!$photo->save()) {
$this->printError($photo, 'Failed to save changes.');
}
}

return 0;
}
}
2 changes: 1 addition & 1 deletion public/Lychee-front
2 changes: 1 addition & 1 deletion public/dist/leaflet.markercluster.js.map
100755 → 100644

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions public/dist/main.js

Large diffs are not rendered by default.

0 comments on commit 373382b

Please sign in to comment.