diff --git a/app/Metadata/Extractor.php b/app/Metadata/Extractor.php index d3eb137a17..9d8e3d045a 100644 --- a/app/Metadata/Extractor.php +++ b/app/Metadata/Extractor.php @@ -64,11 +64,11 @@ public function size(array &$metadata, string $filename) * Extracts metadata from an image file. * * @param string $filename - * @param string mime type + * @param string file kind * * @return array */ - public function extract(string $filename, string $type): array + public function extract(string $filename, string $kind): array { $reader = null; @@ -82,7 +82,7 @@ public function extract(string $filename, string $type): array $is_raw = true; } - if (strpos($type, 'video') !== 0) { + if ($kind !== 'video') { // It's a photo if (Configs::hasExiftool()) { // reader with Exiftool adapter @@ -164,7 +164,6 @@ public function extract(string $filename, string $type): array $metadata['make'] = ($exif->getMake() !== false) ? $exif->getMake() : ''; $metadata['model'] = ($exif->getCamera() !== false) ? $exif->getCamera() : ''; $metadata['shutter'] = ($exif->getExposure() !== false) ? $exif->getExposure() : ''; - $metadata['takestamp'] = ($exif->getCreationDate() !== false) ? $exif->getCreationDate()->format('Y-m-d H:i:s') : null; $metadata['lens'] = ($exif->getLens() !== false) ? $exif->getLens() : ''; $metadata['tags'] = ($exif->getKeywords() !== false) ? (is_array($exif->getKeywords()) ? implode(',', $exif->getKeywords()) : $exif->getKeywords()) : ''; $metadata['latitude'] = ($exif->getLatitude() !== false) ? $exif->getLatitude() : null; @@ -175,16 +174,60 @@ public function extract(string $filename, string $type): array $metadata['livePhotoContentID'] = ($exif->getContentIdentifier() !== false) ? $exif->getContentIdentifier() : null; $metadata['MicroVideoOffset'] = ($exif->getMicroVideoOffset() !== false) ? $exif->getMicroVideoOffset() : null; - // We need to make sure, takestamp is between '1970-01-01 00:00:01' UTC to '2038-01-19 03:14:07' UTC. - // We set value to null in case we're out of bounds - if ($metadata['takestamp'] !== null) { + $takestamp = $exif->getCreationDate(); + if ($takestamp !== false) { + // Some videos store creation time in local time while others use + // UTC and it's often impossible to tell, especially since the + // metadata extractors are not consistent either. We rely here on + // a simple filetype-based heuristics and, for a timestamp we + // suspect to be in UTC, we covert it to the local time of the + // Lychee server so that it's displayed to users as local time. + // + // Other possible approaches would include deriving local time from + // the file name or from other objects in the same album, as well + // as extracting the time zone from the location data if present. + if ($kind === 'video') { + $locals = strtolower(Configs::get_value('local_takestamp_video_formats', '')); + if (!in_array(strtolower($extension), explode('|', $locals), true)) { + // This is a video format where we expect the takestamp + // to be provided in UTC. + if ($takestamp->getTimezone()->getName() === date_default_timezone_get()) { + // Most likely the time zone info was missing so the + // system default was used instead, which is wrong, + // because the takestamp is actually in UTC. This will + // trigger, e.g., for mp4 files with the Exiftool + // extractor. We recreate the takestamp as a UTC + // timestamp and _then_ change the time zone to local. + $takestamp = new \DateTime($takestamp->format('Y-m-d H:i:s'), new \DateTimeZone('UTC')); + $takestamp->setTimezone(new \DateTimeZone(date_default_timezone_get())); + } elseif ($takestamp->getTimezone()->getName() === 'Z') { + // This one is correctly in Zulu (UTC). We just need + // to change the time zone to local. + $takestamp->setTimezone(new \DateTimeZone(date_default_timezone_get())); + } else { + // In the remaining case the time zone information was + // extracted and the takestamp is assumed to be in local + // time, so we don't need to do anything. + // + // The only known example are the mov files from Apple + // devices; the time zone will be formatted as "+01:00" + // so neither of the two conditions above should trigger. + } + } + } + + // We need to make sure that the takestamp is between '1970-01-01 00:00:01' UTC and '2038-01-19 03:14:07' UTC. + // We set the value to null in case we're out of bounds $min_date = new \DateTime('1970-01-01 00:00:01', new \DateTimeZone('UTC')); $max_date = new \DateTime('2038-01-19 03:14:07', new \DateTimeZone('UTC')); - $takestamp = new \DateTime($metadata['takestamp']); if ($takestamp < $min_date || $takestamp > $max_date) { $metadata['takestamp'] = null; Logs::notice(__METHOD__, __LINE__, 'Takestamp (' . $takestamp->format('Y-m-d H:i:s') . ') out of bounds (needs to be between 1970-01-01 00:00:01 and 2038-01-19 03:14:07)'); + } else { + $metadata['takestamp'] = $takestamp->format('Y-m-d H:i:s'); } + } else { + $metadata['takestamp'] = null; } // We need to make sure, latitude is between -90/90 and longitude is between -180/180 @@ -237,7 +280,7 @@ public function extract(string $filename, string $type): array $metadata['position'] = implode(', ', $fields); } - if (strpos($type, 'video') !== 0) { + if ($kind !== 'video') { $metadata['aperture'] = ($exif->getAperture() !== false) ? $exif->getAperture() : ''; $metadata['focal'] = ($exif->getFocalLength() !== false) ? $exif->getFocalLength() : ''; if ($metadata['focal'] !== '') { diff --git a/app/ModelFunctions/PhotoFunctions.php b/app/ModelFunctions/PhotoFunctions.php index d40e09f77a..cfb97f5983 100644 --- a/app/ModelFunctions/PhotoFunctions.php +++ b/app/ModelFunctions/PhotoFunctions.php @@ -51,10 +51,12 @@ class PhotoFunctions public $validVideoTypes = [ 'video/mp4', 'video/mpeg', + 'image/x-tga', // mpg 'video/ogg', 'video/webm', 'video/quicktime', 'video/x-ms-asf', // wmv file + 'video/x-ms-wmv', // wmv file 'video/x-msvideo', // Avi 'video/x-m4v', // Avi ]; @@ -111,37 +113,35 @@ public function file_type($file, string $extension) return 'raw'; } - if (!in_array(strtolower($extension), $this->validExtensions, true)) { + if (in_array(strtolower($extension), $this->validExtensions, true)) { $mimeType = $file['type']; - if (!in_array($mimeType, $this->validVideoTypes, true)) { - // let's check for the mimetype - // maybe we don't have a photo - if (!function_exists('exif_imagetype')) { - Logs::error( - __METHOD__, - __LINE__, - 'EXIF library not loaded. Make sure exif is enabled in php.ini' - ); - - return 'EXIF library not loaded on the server!'; - } + if (in_array($mimeType, $this->validVideoTypes, true)) { + return 'video'; + } - $type = @exif_imagetype($file['tmp_name']); - if (!in_array($type, $this->validTypes, true)) { - Logs::error(__METHOD__, __LINE__, 'Photo type not supported: ' . $file['name']); + return 'photo'; + } - return 'Photo type not supported!'; - } - // we have maybe a raw file - Logs::error(__METHOD__, __LINE__, 'Photo format not supported: ' . $file['name']); + // let's check for the mimetype + // maybe we don't have a photo + if (!function_exists('exif_imagetype')) { + Logs::error( + __METHOD__, + __LINE__, + 'EXIF library not loaded. Make sure exif is enabled in php.ini' + ); - return 'Photo format not supported!'; - } - // we have a video - return 'video'; + return 'EXIF library not loaded on the server!'; + } + + $type = @exif_imagetype($file['tmp_name']); + if (in_array($type, $this->validTypes, true)) { + return 'photo'; } - // we have a normal photo - return 'photo'; + + Logs::error(__METHOD__, __LINE__, 'Photo type not supported: ' . $file['name']); + + return 'Photo type not supported!'; } /** @@ -285,7 +285,7 @@ public function add(array $file, $albumID_in = 0, bool $delete_imported = false, // Before we skip entirely, check if there is a sidecar file and if the metadata needs to be updated (from a sidecar) if ($resync_metadata === true) { - $info = $this->getFileMetadata($file, $path, $kind, $mimeType, $extension); + $info = $this->getFileMetadata($file, $path, $kind, $extension); foreach ($info as $key => $value) { if ($existing->$key !== null && $value !== $existing->$key) { $metadataChanged = true; @@ -307,7 +307,7 @@ public function add(array $file, $albumID_in = 0, bool $delete_imported = false, } } - $info = $this->getFileMetadata($file, $path, $kind, $mimeType, $extension); + $info = $this->getFileMetadata($file, $path, $kind, $extension); // TODO: move this elsewhere $photo->title = $info['title']; @@ -540,7 +540,7 @@ public function createJpgFromRaw(Photo $photo): string */ public function extractVideoFrame(Photo $photo): string { - if ($photo->aperture === '') { + if ($photo->aperture === '' || !Configs::hasFFmpeg()) { return ''; } @@ -890,14 +890,13 @@ public function getValidExtensions(): array * @param array $file * @param string $path * @param string $kind - * @param string $mimeType * @param string $extension * * @return array */ - private function getFileMetadata($file, $path, $kind, $mimeType, $extension): array + private function getFileMetadata($file, $path, $kind, $extension): array { - $info = $this->metadataExtractor->extract($path, $mimeType); + $info = $this->metadataExtractor->extract($path, $kind); if ($kind == 'raw') { $info['type'] = 'raw'; } diff --git a/database/migrations/2020_07_29_132731_config_local_takestamp.php b/database/migrations/2020_07_29_132731_config_local_takestamp.php new file mode 100644 index 0000000000..3913810e85 --- /dev/null +++ b/database/migrations/2020_07_29_132731_config_local_takestamp.php @@ -0,0 +1,37 @@ +insert([ + [ + 'key' => 'local_takestamp_video_formats', + 'value' => '.avi|.mov', + 'confidentiality' => '2', + 'cat' => 'Image Processing', + 'type_range' => DISABLED, + ], + ]); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Configs::where('key', '=', 'local_takestamp_video_formats')->delete(); + } +}