mirror of https://github.com/nextcloud/photos
Use new metadata API for providers
Signed-off-by: Louis Chemineau <louis@chmn.me>
This commit is contained in:
parent
5edd0b76d0
commit
a87c19ee24
|
@ -32,7 +32,6 @@
|
|||
|
||||
<commands>
|
||||
<command>OCA\Photos\Command\UpdateReverseGeocodingFilesCommand</command>
|
||||
<command>OCA\Photos\Command\MapMediaToPlaceCommand</command>
|
||||
</commands>
|
||||
|
||||
<sabre>
|
||||
|
@ -48,4 +47,4 @@
|
|||
<background-jobs>
|
||||
<job>OCA\Photos\Jobs\AutomaticPlaceMapperJob</job>
|
||||
</background-jobs>
|
||||
</info>
|
||||
</info>
|
|
@ -35,7 +35,7 @@ describe('Manage places', () => {
|
|||
cy.createRandomUser()
|
||||
.then((user) => {
|
||||
uploadTestMedia(user)
|
||||
runOccCommand(`photos:map-media-to-place --user ${user.userId}`)
|
||||
runOccCommand('files:scan --all --generate-metadata')
|
||||
cy.login(user)
|
||||
cy.visit('/apps/photos')
|
||||
})
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -28,15 +28,19 @@ namespace OCA\Photos\AppInfo;
|
|||
use OCA\DAV\Connector\Sabre\Principal;
|
||||
use OCA\DAV\Events\SabrePluginAuthInitEvent;
|
||||
use OCA\Photos\Listener\AlbumsManagementEventListener;
|
||||
use OCA\Photos\Listener\PlaceManagerEventListener;
|
||||
use OCA\Photos\Listener\SabrePluginAuthInitListener;
|
||||
use OCA\Photos\Listener\TagListener;
|
||||
use OCA\Photos\MetadataProvider\ExifMetadataProvider;
|
||||
use OCA\Photos\MetadataProvider\OriginalDateTimeMetadataProvider;
|
||||
use OCA\Photos\MetadataProvider\PlaceMetadataProvider;
|
||||
use OCA\Photos\MetadataProvider\SizeMetadataProvider;
|
||||
use OCP\AppFramework\App;
|
||||
use OCP\AppFramework\Bootstrap\IBootContext;
|
||||
use OCP\AppFramework\Bootstrap\IBootstrap;
|
||||
use OCP\AppFramework\Bootstrap\IRegistrationContext;
|
||||
use OCP\Files\Events\Node\NodeDeletedEvent;
|
||||
use OCP\Files\Events\Node\NodeWrittenEvent;
|
||||
use OCP\FilesMetadata\Event\MetadataBackgroundEvent;
|
||||
use OCP\FilesMetadata\Event\MetadataLiveEvent;
|
||||
use OCP\Group\Events\GroupDeletedEvent;
|
||||
use OCP\Group\Events\UserRemovedEvent;
|
||||
use OCP\Share\Events\ShareDeletedEvent;
|
||||
|
@ -75,8 +79,12 @@ class Application extends App implements IBootstrap {
|
|||
/** Register $principalBackend for the DAV collection */
|
||||
$context->registerServiceAlias('principalBackend', Principal::class);
|
||||
|
||||
// Priority of -1 to be triggered after event listeners populating metadata.
|
||||
$context->registerEventListener(NodeWrittenEvent::class, PlaceManagerEventListener::class, -1);
|
||||
// Metadata
|
||||
$context->registerEventListener(MetadataLiveEvent::class, ExifMetadataProvider::class);
|
||||
$context->registerEventListener(MetadataLiveEvent::class, SizeMetadataProvider::class);
|
||||
$context->registerEventListener(MetadataLiveEvent::class, OriginalDateTimeMetadataProvider::class);
|
||||
$context->registerEventListener(MetadataLiveEvent::class, PlaceMetadataProvider::class);
|
||||
$context->registerEventListener(MetadataBackgroundEvent::class, PlaceMetadataProvider::class);
|
||||
|
||||
$context->registerEventListener(NodeDeletedEvent::class, AlbumsManagementEventListener::class);
|
||||
$context->registerEventListener(UserRemovedEvent::class, AlbumsManagementEventListener::class);
|
||||
|
|
|
@ -1,116 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Louis Chemineau <louis@chmn.me>
|
||||
*
|
||||
* @author Louis Chemineau <louis@chmn.me>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* 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 OCA\Photos\Command;
|
||||
|
||||
use OCA\Photos\Service\MediaPlaceManager;
|
||||
use OCP\Files\Folder;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\IConfig;
|
||||
use OCP\IUserManager;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class MapMediaToPlaceCommand extends Command {
|
||||
public function __construct(
|
||||
private IRootFolder $rootFolder,
|
||||
private MediaPlaceManager $mediaPlaceManager,
|
||||
private IConfig $config,
|
||||
private IUserManager $userManager,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the command
|
||||
*/
|
||||
protected function configure(): void {
|
||||
$this->setName('photos:map-media-to-place')
|
||||
->setDescription('Reverse geocode media coordinates.')
|
||||
->addOption('user', 'u', InputOption::VALUE_REQUIRED, 'Limit the mapping to a user.', null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the command
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
if (!$this->config->getSystemValueBool('enable_file_metadata', true)) {
|
||||
throw new \Exception('File metadata is not enabled.');
|
||||
}
|
||||
|
||||
$userId = $input->getOption('user');
|
||||
if ($userId === null) {
|
||||
$this->scanForAllUsers($output);
|
||||
} else {
|
||||
$this->scanFilesForUser($userId, $output);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function scanForAllUsers(OutputInterface $output): void {
|
||||
$users = $this->userManager->search('');
|
||||
|
||||
$output->writeln("Scanning all users:");
|
||||
foreach ($users as $user) {
|
||||
$this->scanFilesForUser($user->getUID(), $output);
|
||||
}
|
||||
}
|
||||
|
||||
private function scanFilesForUser(string $userId, OutputInterface $output): void {
|
||||
$userFolder = $this->rootFolder->getUserFolder($userId);
|
||||
$output->write(" - Scanning files for $userId");
|
||||
$startTime = time();
|
||||
$count = $this->scanFolder($userFolder);
|
||||
$timeElapse = time() - $startTime;
|
||||
$output->writeln(" - $count files, $timeElapse sec");
|
||||
}
|
||||
|
||||
private function scanFolder(Folder $folder): int {
|
||||
$count = 0;
|
||||
|
||||
// Do not scan share and other moveable mounts.
|
||||
if ($folder->getMountPoint() instanceof \OC\Files\Mount\MoveableMount) {
|
||||
return $count;
|
||||
}
|
||||
|
||||
foreach ($folder->getDirectoryListing() as $node) {
|
||||
if ($node instanceof Folder) {
|
||||
$count += $this->scanFolder($node);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!str_starts_with($node->getMimeType(), 'image')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->mediaPlaceManager->setPlaceForFile($node->getId());
|
||||
$count++;
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
}
|
|
@ -25,12 +25,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace OCA\Photos\DB;
|
||||
|
||||
use OC\Metadata\FileMetadata;
|
||||
|
||||
class PhotosFile {
|
||||
/** @var array<string, FileMetadata> */
|
||||
private array $metaData = [];
|
||||
|
||||
public function __construct(
|
||||
private int $fileId,
|
||||
private string $name,
|
||||
|
@ -64,16 +59,4 @@ class PhotosFile {
|
|||
public function getEtag(): string {
|
||||
return $this->etag;
|
||||
}
|
||||
|
||||
public function setMetadata(string $key, FileMetadata $value): void {
|
||||
$this->metaData[$key] = $value;
|
||||
}
|
||||
|
||||
public function hasMetadata(string $key): bool {
|
||||
return isset($this->metaData[$key]);
|
||||
}
|
||||
|
||||
public function getMetadata(string $key): FileMetadata {
|
||||
return $this->metaData[$key];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,20 +25,22 @@ declare(strict_types=1);
|
|||
|
||||
namespace OCA\Photos\DB\Place;
|
||||
|
||||
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
|
||||
use OCA\Photos\AppInfo\Application;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\Files\IMimeTypeLoader;
|
||||
use OCP\Files\IRootFolder;
|
||||
use OCP\Files\NotFoundException;
|
||||
use OCP\FilesMetadata\IFilesMetadataManager;
|
||||
use OCP\IDBConnection;
|
||||
|
||||
class PlaceMapper {
|
||||
public const METADATA_GROUP = 'photos_place';
|
||||
public const METADATA_KEY = 'photos-place';
|
||||
|
||||
public function __construct(
|
||||
private IDBConnection $connection,
|
||||
private IMimeTypeLoader $mimeTypeLoader,
|
||||
private IRootFolder $rootFolder,
|
||||
private IFilesMetadataManager $filesMetadataManager,
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -49,20 +51,22 @@ class PlaceMapper {
|
|||
->getMountPoint()
|
||||
->getNumericStorageId();
|
||||
|
||||
$mimepart = $this->mimeTypeLoader->getId('image');
|
||||
|
||||
$mimetypes = array_map(fn ($mimetype) => $this->mimeTypeLoader->getId($mimetype), Application::IMAGE_MIMES);
|
||||
|
||||
$qb = $this->connection->getQueryBuilder();
|
||||
|
||||
$rows = $qb->selectDistinct('meta.value')
|
||||
->from('file_metadata', 'meta')
|
||||
->join('meta', 'filecache', 'file', $qb->expr()->eq('file.fileid', 'meta.id', IQueryBuilder::PARAM_INT))
|
||||
->where($qb->expr()->eq('file.storage', $qb->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->eq('file.mimepart', $qb->createNamedParameter($mimepart, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->eq('meta.group_name', $qb->createNamedParameter(self::METADATA_GROUP)))
|
||||
$qb->selectDistinct('meta_value_string')
|
||||
->from('filecache', 'file');
|
||||
$metadataQuery = $this->filesMetadataManager->getMetadataQuery($qb, 'file', 'fileid');
|
||||
$metadataQuery->joinIndex(self::METADATA_KEY);
|
||||
$rows = $qb->where($qb->expr()->eq('file.storage', $qb->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->in('file.mimetype', $qb->createNamedParameter($mimetypes, IQueryBuilder::PARAM_INT_ARRAY)))
|
||||
->andWhere('meta_value_string IS NOT NULL')
|
||||
->executeQuery()
|
||||
->fetchAll();
|
||||
|
||||
return array_map(fn ($row) => new PlaceInfo($userId, $row['value']), $rows);
|
||||
return array_map(fn ($row) => new PlaceInfo($userId, $row['meta_value_string']), $rows);
|
||||
}
|
||||
|
||||
/** @return PlaceInfo */
|
||||
|
@ -72,17 +76,18 @@ class PlaceMapper {
|
|||
->getMountPoint()
|
||||
->getNumericStorageId();
|
||||
|
||||
$mimepart = $this->mimeTypeLoader->getId('image');
|
||||
$mimetypes = array_map(fn ($mimetype) => $this->mimeTypeLoader->getId($mimetype), Application::IMAGE_MIMES);
|
||||
|
||||
$qb = $this->connection->getQueryBuilder();
|
||||
|
||||
$rows = $qb->selectDistinct('meta.value')
|
||||
->from('file_metadata', 'meta')
|
||||
->join('meta', 'filecache', 'file', $qb->expr()->eq('file.fileid', 'meta.id', IQueryBuilder::PARAM_INT))
|
||||
->where($qb->expr()->eq('file.storage', $qb->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->eq('file.mimepart', $qb->createNamedParameter($mimepart, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->eq('meta.group_name', $qb->createNamedParameter(self::METADATA_GROUP)))
|
||||
->andWhere($qb->expr()->eq('meta.value', $qb->createNamedParameter($place)))
|
||||
$qb->selectDistinct('meta_value_string')
|
||||
->from('filecache', 'file');
|
||||
$metadataQuery = $this->filesMetadataManager->getMetadataQuery($qb, 'file', 'fileid');
|
||||
$metadataQuery->joinIndex(self::METADATA_KEY);
|
||||
$rows = $qb->where($qb->expr()->eq('file.storage', $qb->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->in('file.mimetype', $qb->createNamedParameter($mimetypes, IQueryBuilder::PARAM_INT_ARRAY)))
|
||||
->andWhere($qb->expr()->eq('meta_value_string', $qb->createNamedParameter($place)))
|
||||
->andWhere('meta_value_string IS NOT NULL')
|
||||
->executeQuery()
|
||||
->fetchAll();
|
||||
|
||||
|
@ -90,7 +95,7 @@ class PlaceMapper {
|
|||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return new PlaceInfo($userId, $rows[0]['value']);
|
||||
return new PlaceInfo($userId, $rows[0]['meta_value_string']);
|
||||
}
|
||||
|
||||
/** @return PlaceFile[] */
|
||||
|
@ -100,17 +105,18 @@ class PlaceMapper {
|
|||
->getMountPoint()
|
||||
->getNumericStorageId();
|
||||
|
||||
$mimepart = $this->mimeTypeLoader->getId('image');
|
||||
$mimetypes = array_map(fn ($mimetype) => $this->mimeTypeLoader->getId($mimetype), Application::IMAGE_MIMES);
|
||||
|
||||
$qb = $this->connection->getQueryBuilder();
|
||||
|
||||
$rows = $qb->select('file.fileid', 'file.name', 'file.mimetype', 'file.size', 'file.mtime', 'file.etag', 'meta.value')
|
||||
->from('file_metadata', 'meta')
|
||||
->join('meta', 'filecache', 'file', $qb->expr()->eq('file.fileid', 'meta.id', IQueryBuilder::PARAM_INT))
|
||||
->where($qb->expr()->eq('file.storage', $qb->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->eq('file.mimepart', $qb->createNamedParameter($mimepart, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->eq('meta.group_name', $qb->createNamedParameter(self::METADATA_GROUP)))
|
||||
->andWhere($qb->expr()->eq('meta.value', $qb->createNamedParameter($place)))
|
||||
$rows = $qb->select('file.fileid', 'file.name', 'file.mimetype', 'file.size', 'file.mtime', 'file.etag', 'meta_value_string')
|
||||
->from('filecache', 'file');
|
||||
$metadataQuery = $this->filesMetadataManager->getMetadataQuery($qb, 'file', 'fileid');
|
||||
$metadataQuery->joinIndex(self::METADATA_KEY);
|
||||
$rows = $qb->where($qb->expr()->eq('file.storage', $qb->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->in('file.mimetype', $qb->createNamedParameter($mimetypes, IQueryBuilder::PARAM_INT_ARRAY)))
|
||||
->andWhere($qb->expr()->eq('meta_value_string', $qb->createNamedParameter($place)))
|
||||
->andWhere('meta_value_string IS NOT NULL')
|
||||
->executeQuery()
|
||||
->fetchAll();
|
||||
|
||||
|
@ -122,7 +128,7 @@ class PlaceMapper {
|
|||
(int)$row['size'],
|
||||
(int)$row['mtime'],
|
||||
$row['etag'],
|
||||
$row['value']
|
||||
$row['meta_value_string']
|
||||
),
|
||||
$rows,
|
||||
);
|
||||
|
@ -134,19 +140,20 @@ class PlaceMapper {
|
|||
->getMountPoint()
|
||||
->getNumericStorageId();
|
||||
|
||||
$mimepart = $this->mimeTypeLoader->getId('image');
|
||||
$mimetypes = array_map(fn ($mimetype) => $this->mimeTypeLoader->getId($mimetype), Application::IMAGE_MIMES);
|
||||
|
||||
$qb = $this->connection->getQueryBuilder();
|
||||
|
||||
$rows = $qb->select('file.fileid', 'file.name', 'file.mimetype', 'file.size', 'file.mtime', 'file.etag', 'meta.value')
|
||||
->from('file_metadata', 'meta')
|
||||
->join('meta', 'filecache', 'file', $qb->expr()->eq('file.fileid', 'meta.id', IQueryBuilder::PARAM_INT))
|
||||
->where($qb->expr()->eq('file.storage', $qb->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->eq('file.mimepart', $qb->createNamedParameter($mimepart, IQueryBuilder::PARAM_INT)))
|
||||
$rows = $qb->select('file.fileid', 'file.name', 'file.mimetype', 'file.size', 'file.mtime', 'file.etag', 'meta_value_string')
|
||||
->from('filecache', 'file');
|
||||
$metadataQuery = $this->filesMetadataManager->getMetadataQuery($qb, 'file', 'fileid');
|
||||
$metadataQuery->joinIndex(self::METADATA_KEY);
|
||||
$rows = $qb->where($qb->expr()->eq('file.storage', $qb->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->eq('file.fileid', $qb->createNamedParameter($fileId)))
|
||||
->andWhere($qb->expr()->eq('file.name', $qb->createNamedParameter($fileName)))
|
||||
->andWhere($qb->expr()->eq('meta.group_name', $qb->createNamedParameter(self::METADATA_GROUP)))
|
||||
->andWhere($qb->expr()->eq('meta.value', $qb->createNamedParameter($place)))
|
||||
->andWhere($qb->expr()->in('file.mimetype', $qb->createNamedParameter($mimetypes, IQueryBuilder::PARAM_INT_ARRAY)))
|
||||
->andWhere($qb->expr()->eq('meta_value_string', $qb->createNamedParameter($place)))
|
||||
->andWhere('meta_value_string IS NOT NULL')
|
||||
->executeQuery()
|
||||
->fetchAll();
|
||||
|
||||
|
@ -161,35 +168,12 @@ class PlaceMapper {
|
|||
(int)$rows[0]['size'],
|
||||
(int)$rows[0]['mtime'],
|
||||
$rows[0]['etag'],
|
||||
$rows[0]['value']
|
||||
$rows[0]['meta_value_string']
|
||||
);
|
||||
}
|
||||
|
||||
public function setPlaceForFile(string $place, int $fileId): void {
|
||||
try {
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->insert('file_metadata')
|
||||
->values([
|
||||
"id" => $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT),
|
||||
"group_name" => $query->createNamedParameter(self::METADATA_GROUP),
|
||||
"value" => $query->createNamedParameter($place),
|
||||
])
|
||||
->executeStatement();
|
||||
} catch (\Exception $ex) {
|
||||
if ($ex->getPrevious() instanceof UniqueConstraintViolationException) {
|
||||
$this->updatePlaceForFile($place, $fileId);
|
||||
} else {
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function updatePlaceForFile(string $place, int $fileId): void {
|
||||
$query = $this->connection->getQueryBuilder();
|
||||
$query->update('file_metadata')
|
||||
->set("value", $query->createNamedParameter($place))
|
||||
->where($query->expr()->eq('id', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($query->expr()->eq('group_name', $query->createNamedParameter(self::METADATA_GROUP)))
|
||||
->executeStatement();
|
||||
$metadata = $this->filesMetadataManager->getMetadata($fileId, true);
|
||||
$metadata->setString('gps', $place, true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Louis Chemineau <louis@chmn.me>
|
||||
*
|
||||
* @author Louis Chemineau <louis@chmn.me>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* 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 OCA\Photos\Jobs;
|
||||
|
||||
use OCA\Photos\Service\MediaPlaceManager;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\BackgroundJob\QueuedJob;
|
||||
|
||||
class MapMediaToPlaceJob extends QueuedJob {
|
||||
private MediaPlaceManager $mediaPlaceManager;
|
||||
|
||||
public function __construct(
|
||||
ITimeFactory $time,
|
||||
MediaPlaceManager $mediaPlaceManager
|
||||
) {
|
||||
parent::__construct($time);
|
||||
$this->mediaPlaceManager = $mediaPlaceManager;
|
||||
}
|
||||
|
||||
protected function run($argument) {
|
||||
[$fileId] = $argument;
|
||||
|
||||
$this->mediaPlaceManager->setPlaceForFile($fileId);
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Louis Chemineau <louis@chmn.me>
|
||||
*
|
||||
* @author Louis Chemineau <louis@chmn.me>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* 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 OCA\Photos\Listener;
|
||||
|
||||
use OCA\Photos\Jobs\MapMediaToPlaceJob;
|
||||
use OCA\Photos\Service\MediaPlaceManager;
|
||||
use OCP\BackgroundJob\IJobList;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
use OCP\Files\Events\Node\NodeWrittenEvent;
|
||||
use OCP\IConfig;
|
||||
|
||||
/**
|
||||
* Listener to add place info from the database.
|
||||
*/
|
||||
class PlaceManagerEventListener implements IEventListener {
|
||||
public function __construct(
|
||||
private MediaPlaceManager $mediaPlaceManager,
|
||||
private IConfig $config,
|
||||
private IJobList $jobList,
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(Event $event): void {
|
||||
if (!$this->config->getSystemValueBool('enable_file_metadata', true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($event instanceof NodeWrittenEvent) {
|
||||
if (!$this->isCorrectPath($event->getNode()->getPath())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!str_starts_with($event->getNode()->getMimeType(), 'image')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fileId = $event->getNode()->getId();
|
||||
|
||||
$this->jobList->add(MapMediaToPlaceJob::class, [$fileId]);
|
||||
}
|
||||
}
|
||||
|
||||
private function isCorrectPath(string $path): bool {
|
||||
// TODO make this more dynamic, we have the same issue in other places
|
||||
return !str_starts_with($path, 'appdata_') && !str_starts_with($path, 'files_versions/');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* @copyright Copyright 2022 Carl Schwan <carl@carlschwan.eu>
|
||||
* @copyright Copyright 2022 Louis Chmn <louis@chmn.me>
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This code is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* 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, version 3,
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Photos\MetadataProvider;
|
||||
|
||||
use OCA\Photos\AppInfo\Application;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
use OCP\Files\File;
|
||||
use OCP\FilesMetadata\Event\MetadataLiveEvent;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* Extract EXIF, IFD0, and GPS data from a picture file.
|
||||
* EXIF data reference: https://web.archive.org/web/20220428165430/exif.org/Exif2-2.PDF
|
||||
*
|
||||
* @template-implements IEventListener
|
||||
*/
|
||||
class ExifMetadataProvider implements IEventListener {
|
||||
public function __construct(
|
||||
private LoggerInterface $logger
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(Event $event): void {
|
||||
if (!($event instanceof MetadataLiveEvent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$node = $event->getNode();
|
||||
|
||||
if (!$node instanceof File) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!in_array($node->getMimeType(), Application::IMAGE_MIMES)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!extension_loaded('exif')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fileDescriptor = $node->fopen('rb');
|
||||
if ($fileDescriptor === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$rawExifData = null;
|
||||
|
||||
try {
|
||||
// HACK: The stream_set_chunk_size call is needed to make reading exif data reliable.
|
||||
// This is to trigger this condition: https://github.com/php/php-src/blob/d64aa6f646a7b5e58359dc79479860164239580a/main/streams/streams.c#L710
|
||||
// But I don't understand yet why 1 as a special meaning.
|
||||
$oldBufferSize = stream_set_chunk_size($fileDescriptor, 1);
|
||||
$rawExifData = @exif_read_data($fileDescriptor, 'EXIF, GPS', true);
|
||||
// We then revert the change after having read the exif data.
|
||||
stream_set_chunk_size($fileDescriptor, $oldBufferSize);
|
||||
} catch (\Exception $ex) {
|
||||
$this->logger->info("Failed to extract metadata for " . $node->getId(), ['exception' => $ex]);
|
||||
}
|
||||
|
||||
if ($rawExifData && array_key_exists('EXIF', $rawExifData)) {
|
||||
$event->getMetadata()->setArray('photos-exif', $this->base64Encode($rawExifData['EXIF']));
|
||||
}
|
||||
|
||||
if ($rawExifData && array_key_exists('IFD0', $rawExifData)) {
|
||||
$event->getMetadata()->setArray('photos-ifd0', $this->base64Encode($rawExifData['IFD0']));
|
||||
}
|
||||
|
||||
if (
|
||||
$rawExifData &&
|
||||
array_key_exists('GPS', $rawExifData)
|
||||
) {
|
||||
$gps = [];
|
||||
|
||||
if (
|
||||
array_key_exists('GPSLatitude', $rawExifData['GPS']) && array_key_exists('GPSLatitudeRef', $rawExifData['GPS']) &&
|
||||
array_key_exists('GPSLongitude', $rawExifData['GPS']) && array_key_exists('GPSLongitudeRef', $rawExifData['GPS'])
|
||||
) {
|
||||
$gps['latitude'] = $this->gpsDegreesToDecimal($rawExifData['GPS']['GPSLatitude'], $rawExifData['GPS']['GPSLatitudeRef']);
|
||||
$gps['longitude'] = $this->gpsDegreesToDecimal($rawExifData['GPS']['GPSLongitude'], $rawExifData['GPS']['GPSLongitudeRef']);
|
||||
}
|
||||
|
||||
if (array_key_exists('GPSAltitude', $rawExifData['GPS']) && array_key_exists('GPSAltitudeRef', $rawExifData['GPS'])) {
|
||||
$gps['altitude'] = ($rawExifData['GPS']['GPSAltitudeRef'] === "\u{0000}" ? 1 : -1) * $this->parseGPSData($rawExifData['GPS']['GPSAltitude']);
|
||||
}
|
||||
|
||||
if (!empty($gps)) {
|
||||
$event->getMetadata()->setArray('photos-gps', $gps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|string $coordinates
|
||||
*/
|
||||
private function gpsDegreesToDecimal($coordinates, ?string $hemisphere): float {
|
||||
if (is_string($coordinates)) {
|
||||
$coordinates = array_map("trim", explode(",", $coordinates));
|
||||
}
|
||||
|
||||
if (count($coordinates) !== 3) {
|
||||
throw new \Exception('Invalid coordinate format: ' . json_encode($coordinates));
|
||||
}
|
||||
|
||||
[$degrees, $minutes, $seconds] = array_map(fn ($rawDegree) => $this->parseGPSData($rawDegree), $coordinates);
|
||||
|
||||
$sign = ($hemisphere === 'W' || $hemisphere === 'S') ? -1 : 1;
|
||||
return $sign * ($degrees + $minutes / 60 + $seconds / 3600);
|
||||
}
|
||||
|
||||
private function parseGPSData(string $rawData): float {
|
||||
$parts = explode('/', $rawData);
|
||||
|
||||
if ($parts[1] === '0') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return floatval($parts[0]) / floatval($parts[1] ?? 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exif data can contain anything.
|
||||
* This method will base 64 encode any non UTF-8 string in an array.
|
||||
*/
|
||||
private function base64Encode(array $data): array {
|
||||
foreach ($data as $key => $value) {
|
||||
if (is_string($value) && !mb_check_encoding($value, 'UTF-8')) {
|
||||
$data[$key] = 'base64:'.base64_encode($value);
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* @copyright Copyright 2022 Carl Schwan <carl@carlschwan.eu>
|
||||
* @copyright Copyright 2022 Louis Chmn <louis@chmn.me>
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This code is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* 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, version 3,
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Photos\MetadataProvider;
|
||||
|
||||
use DateTime;
|
||||
use OCA\Photos\AppInfo\Application;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
use OCP\Files\File;
|
||||
use OCP\FilesMetadata\Event\MetadataLiveEvent;
|
||||
|
||||
/**
|
||||
* @template-implements IEventListener
|
||||
*/
|
||||
class OriginalDateTimeMetadataProvider implements IEventListener {
|
||||
public function __construct() {
|
||||
}
|
||||
|
||||
public array $regexpToDateFormatMap = [
|
||||
"/^IMG_([0-9]{8}_[0-9]{6})/" => "Ymd_Gis",
|
||||
"/^PANO_([0-9]{8}_[0-9]{6})/" => "Ymd_Gis",
|
||||
"/^PXL_([0-9]{8}_[0-9]{6})/" => "Ymd_Gis",
|
||||
"/^([0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{4})/" => "Y-m-d-G-i-s",
|
||||
];
|
||||
|
||||
public function handle(Event $event): void {
|
||||
if (!($event instanceof MetadataLiveEvent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$node = $event->getNode();
|
||||
|
||||
if (!$node instanceof File) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!in_array($node->getMimeType(), Application::IMAGE_MIMES)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$metadata = $event->getMetadata();
|
||||
|
||||
// Try to use EXIF data.
|
||||
if ($metadata->hasKey('photos-exif') && array_key_exists('DateTimeOriginal', $metadata->getArray('photos-exif'))) {
|
||||
$rawDateTimeOriginal = $metadata->getArray('photos-exif')['DateTimeOriginal'];
|
||||
$dateTimeOriginal = DateTime::createFromFormat("Y:m:d G:i:s", $rawDateTimeOriginal);
|
||||
$metadata->setInt('photos-original_date_time', $dateTimeOriginal->getTimestamp(), true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to parse the date out of the name.
|
||||
$name = $node->getName();
|
||||
$matches = [];
|
||||
|
||||
foreach ($this->regexpToDateFormatMap as $regexp => $format) {
|
||||
$matchesCount = preg_match($regexp, $name, $matches);
|
||||
if ($matchesCount === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$dateTimeOriginal = DateTime::createFromFormat($format, $matches[1]);
|
||||
$metadata->setInt('photos-original_date_time', $dateTimeOriginal->getTimestamp(), true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to the mtime.
|
||||
$metadata->setInt('photos-original_date_time', $node->getMTime(), true);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* @copyright Copyright 2023 Louis Chmn <louis@chmn.me>
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This code is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* 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, version 3,
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
namespace OCA\Photos\MetadataProvider;
|
||||
|
||||
use OCA\Photos\AppInfo\Application;
|
||||
use OCA\Photos\Service\MediaPlaceManager;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
use OCP\Files\File;
|
||||
use OCP\FilesMetadata\Event\MetadataBackgroundEvent;
|
||||
use OCP\FilesMetadata\Event\MetadataLiveEvent;
|
||||
|
||||
/**
|
||||
* @template-implements IEventListener
|
||||
*/
|
||||
class PlaceMetadataProvider implements IEventListener {
|
||||
public function __construct(
|
||||
private MediaPlaceManager $mediaPlaceManager
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(Event $event): void {
|
||||
if ($event instanceof MetadataLiveEvent) {
|
||||
$node = $event->getNode();
|
||||
|
||||
if (!$node instanceof File) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!in_array($node->getMimeType(), Application::IMAGE_MIMES)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event->requestBackgroundJob();
|
||||
}
|
||||
|
||||
if ($event instanceof MetadataBackgroundEvent) {
|
||||
$metadata = $event->getMetadata();
|
||||
$place = $this->mediaPlaceManager->getPlaceForFile($event->getNode()->getId());
|
||||
if ($place !== null) {
|
||||
$metadata->setString('photos-place', $place, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* @copyright Copyright 2022 Carl Schwan <carl@carlschwan.eu>
|
||||
* @copyright Copyright 2022 Louis Chmn <louis@chmn.me>
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This code is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* 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, version 3,
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
*/
|
||||
|
||||
namespace OCA\Photos\MetadataProvider;
|
||||
|
||||
use OCA\Photos\AppInfo\Application;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
use OCP\Files\File;
|
||||
use OCP\FilesMetadata\Event\MetadataLiveEvent;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* @template-implements IEventListener
|
||||
*/
|
||||
class SizeMetadataProvider implements IEventListener {
|
||||
public function __construct(
|
||||
private LoggerInterface $logger
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(Event $event): void {
|
||||
if (!($event instanceof MetadataLiveEvent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$node = $event->getNode();
|
||||
|
||||
if (!$node instanceof File) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!in_array($node->getMimeType(), Application::IMAGE_MIMES)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$size = getimagesizefromstring($node->getContent());
|
||||
|
||||
if ($size === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event->getMetadata()->setArray('photos-size', [
|
||||
'width' => $size[0],
|
||||
'height' => $size[1],
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -23,7 +23,6 @@ declare(strict_types=1);
|
|||
|
||||
namespace OCA\Photos\Sabre;
|
||||
|
||||
use OC\Metadata\IMetadataManager;
|
||||
use OCA\DAV\Connector\Sabre\FilesPlugin;
|
||||
use OCA\Photos\Album\AlbumMapper;
|
||||
use OCA\Photos\Sabre\Album\AlbumPhoto;
|
||||
|
@ -54,23 +53,18 @@ class PropFindPlugin extends ServerPlugin {
|
|||
public const PERMISSIONS_PROPERTYNAME = '{http://owncloud.org/ns}permissions';
|
||||
|
||||
private IConfig $config;
|
||||
private IMetadataManager $metadataManager;
|
||||
private IPreview $previewManager;
|
||||
private bool $metadataEnabled;
|
||||
private ?Tree $tree;
|
||||
private AlbumMapper $albumMapper;
|
||||
|
||||
public function __construct(
|
||||
IConfig $config,
|
||||
IMetadataManager $metadataManager,
|
||||
IPreview $previewManager,
|
||||
AlbumMapper $albumMapper
|
||||
) {
|
||||
$this->config = $config;
|
||||
$this->metadataManager = $metadataManager;
|
||||
$this->previewManager = $previewManager;
|
||||
$this->albumMapper = $albumMapper;
|
||||
$this->metadataEnabled = $this->config->getSystemValueBool('enable_file_metadata', true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -122,20 +116,8 @@ class PropFindPlugin extends ServerPlugin {
|
|||
return $filteredPermissions;
|
||||
});
|
||||
|
||||
if ($this->metadataEnabled) {
|
||||
$propFind->handle(FilesPlugin::FILE_METADATA_SIZE, function () use ($node) {
|
||||
if (!str_starts_with($node->getFile()->getMimetype(), 'image')) {
|
||||
return json_encode((object)[]);
|
||||
}
|
||||
|
||||
if ($node->getFile()->hasMetadata('size')) {
|
||||
$sizeMetadata = $node->getFile()->getMetadata('size');
|
||||
} else {
|
||||
$sizeMetadata = $this->metadataManager->fetchMetadataFor('size', [$node->getFile()->getFileId()])[$node->getFile()->getFileId()];
|
||||
}
|
||||
|
||||
return $sizeMetadata->getValue();
|
||||
});
|
||||
foreach ($node->getFileInfo()->getMetadata() as $metadataKey => $metadataValue) {
|
||||
$propFind->handle(FilesPlugin::FILE_METADATA_PREFIX.$metadataKey, $metadataValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -146,35 +128,11 @@ class PropFindPlugin extends ServerPlugin {
|
|||
$propFind->handle(self::LOCATION_PROPERTYNAME, fn () => $node->getAlbum()->getAlbum()->getLocation());
|
||||
$propFind->handle(self::DATE_RANGE_PROPERTYNAME, fn () => json_encode($node->getDateRange()));
|
||||
$propFind->handle(self::COLLABORATORS_PROPERTYNAME, fn () => $node->getCollaborators());
|
||||
|
||||
// TODO detect dynamically which metadata groups are requested and
|
||||
// preload all of them and not just size
|
||||
if ($this->metadataEnabled && in_array(FilesPlugin::FILE_METADATA_SIZE, $propFind->getRequestedProperties(), true)) {
|
||||
$fileIds = $node->getAlbum()->getFileIds();
|
||||
|
||||
$preloadedMetadata = $this->metadataManager->fetchMetadataFor('size', $fileIds);
|
||||
foreach ($node->getAlbum()->getFiles() as $file) {
|
||||
if (str_starts_with($file->getMimeType(), 'image')) {
|
||||
$file->setMetadata('size', $preloadedMetadata[$file->getFileId()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($node instanceof PlaceRoot) {
|
||||
$propFind->handle(self::LAST_PHOTO_PROPERTYNAME, fn () => $node->getFirstPhoto());
|
||||
$propFind->handle(self::NBITEMS_PROPERTYNAME, fn () => count($node->getChildren()));
|
||||
|
||||
// TODO detect dynamically which metadata groups are requested and
|
||||
// preload all of them and not just size
|
||||
if ($this->metadataEnabled && in_array(FilesPlugin::FILE_METADATA_SIZE, $propFind->getRequestedProperties(), true)) {
|
||||
$fileIds = $node->getFileIds();
|
||||
$preloadedMetadata = $this->metadataManager->fetchMetadataFor('size', $fileIds);
|
||||
|
||||
foreach ($node->getChildren() as $file) {
|
||||
$file->getFile()->setMetadata('size', $preloadedMetadata[$file->getFileId()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,12 +25,13 @@ declare(strict_types=1);
|
|||
|
||||
namespace OCA\Photos\Service;
|
||||
|
||||
use OC\Metadata\IMetadataManager;
|
||||
use OCA\Photos\DB\Place\PlaceMapper;
|
||||
use OCP\FilesMetadata\Exceptions\FilesMetadataNotFoundException;
|
||||
use OCP\FilesMetadata\IFilesMetadataManager;
|
||||
|
||||
class MediaPlaceManager {
|
||||
public function __construct(
|
||||
private IMetadataManager $metadataManager,
|
||||
private IFilesMetadataManager $filesMetadataManager,
|
||||
private ReverseGeoCoderService $rgcService,
|
||||
private PlaceMapper $placeMapper,
|
||||
) {
|
||||
|
@ -46,31 +47,23 @@ class MediaPlaceManager {
|
|||
$this->placeMapper->setPlaceForFile($place, $fileId);
|
||||
}
|
||||
|
||||
public function updatePlaceForFile(int $fileId): void {
|
||||
$place = $this->getPlaceForFile($fileId);
|
||||
|
||||
if ($place === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->placeMapper->updatePlaceForFile($place, $fileId);
|
||||
}
|
||||
|
||||
private function getPlaceForFile(int $fileId): ?string {
|
||||
$gpsMetadata = $this->metadataManager->fetchMetadataFor('gps', [$fileId])[$fileId];
|
||||
$metadata = $gpsMetadata->getDecodedValue();
|
||||
|
||||
if (count($metadata) === 0) {
|
||||
public function getPlaceForFile(int $fileId): ?string {
|
||||
try {
|
||||
$metadata = $this->filesMetadataManager->getMetadata($fileId, true);
|
||||
} catch (FilesMetadataNotFoundException) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$latitude = $metadata['latitude'];
|
||||
$longitude = $metadata['longitude'];
|
||||
|
||||
if ($latitude === null || $longitude === null) {
|
||||
if (!$metadata->hasKey('photos-gps')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$coordinate = $metadata->getArray('photos-gps');
|
||||
|
||||
$latitude = $coordinate['latitude'];
|
||||
$longitude = $coordinate['longitude'];
|
||||
|
||||
return $this->rgcService->getPlaceForCoordinates($latitude, $longitude);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -250,9 +250,9 @@ export default {
|
|||
const file = this.files[fileId]
|
||||
return {
|
||||
id: file.fileid,
|
||||
width: file.fileMetadataSizeParsed.width,
|
||||
height: file.fileMetadataSizeParsed.height,
|
||||
ratio: this.croppedLayout ? 1 : file.fileMetadataSizeParsed.width / file.fileMetadataSizeParsed.height,
|
||||
width: file.metadataPhotosSize.width,
|
||||
height: file.metadataPhotosSize.height,
|
||||
ratio: this.croppedLayout ? 1 : file.metadataPhotosSize.width / file.metadataPhotosSize.height,
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -27,7 +27,8 @@ const props = `
|
|||
<d:resourcetype />
|
||||
<nc:face-detections />
|
||||
<nc:face-preview-image />
|
||||
<nc:file-metadata-size />
|
||||
<nc:metadata-photos-size />
|
||||
<nc:metadata-photos-original_date_time />
|
||||
<nc:has-preview />
|
||||
<nc:realpath />
|
||||
<oc:favorite />
|
||||
|
|
|
@ -129,7 +129,7 @@ export default async function(path = '', options = {}) {
|
|||
</d:where>
|
||||
<d:orderby>
|
||||
<d:order>
|
||||
<d:prop><d:getlastmodified/></d:prop>
|
||||
<d:prop><nc:metadata-photos-original_date_time/></d:prop>
|
||||
<d:descending/>
|
||||
</d:order>
|
||||
</d:orderby>
|
||||
|
|
|
@ -42,9 +42,9 @@ import { genFileInfo } from '../utils/fileUtils.js'
|
|||
* @property {string} basename - The name of the file (ex: "790-IMG_20180906_085724.jpg").
|
||||
* @property {string} filename - The file name of the file (ex: "/photos/admin/places/Athens/790-IMG_20180906_085724.jpg").
|
||||
* @property {string} source - The full source of the collection (ex: "https://nextcloud_server1.test/remote.php/dav//photos/admin/places/Athens/790-IMG_20180906_085724.jpg").
|
||||
* @property {object} fileMetadataSizeParsed - The metadata of the file.
|
||||
* @property {number} fileMetadataSizeParsed.width - The width of the file.
|
||||
* @property {number} fileMetadataSizeParsed.height - The height of the file.
|
||||
* @property {object} metadataPhotosSize - The metadata of the file.
|
||||
* @property {number} metadataPhotosSize.width - The width of the file.
|
||||
* @property {number} metadataPhotosSize.height - The height of the file.
|
||||
*/
|
||||
|
||||
/** @typedef {Object<string, Collection>} IndexedCollections */
|
||||
|
@ -84,7 +84,8 @@ function getCollectionFilesDavRequest(extraProps = []) {
|
|||
<d:getetag />
|
||||
<d:getlastmodified />
|
||||
<d:resourcetype />
|
||||
<nc:file-metadata-size />
|
||||
<nc:metadata-photos-size />
|
||||
<nc:metadata-photos-original_date_time />
|
||||
<nc:has-preview />
|
||||
<oc:favorite />
|
||||
<oc:fileid />
|
||||
|
|
|
@ -39,7 +39,8 @@ function getCollectionFilesDavRequest(extraProps = []) {
|
|||
<d:getetag />
|
||||
<d:getlastmodified />
|
||||
<d:resourcetype />
|
||||
<nc:file-metadata-size />
|
||||
<nc:metadata-photos-size />
|
||||
<nc:metadata-photos-original_date_time />
|
||||
<nc:has-preview />
|
||||
<oc:favorite />
|
||||
<oc:fileid />
|
||||
|
|
|
@ -50,12 +50,12 @@ const mutations = {
|
|||
}
|
||||
|
||||
if (file.fileid >= 0) {
|
||||
if (file.fileMetadataSize?.length > 1) {
|
||||
file.fileMetadataSizeParsed = JSON.parse(file.fileMetadataSize?.replace(/"/g, '"') ?? '{}')
|
||||
file.fileMetadataSizeParsed.width = file.fileMetadataSizeParsed?.width ?? 256
|
||||
file.fileMetadataSizeParsed.height = file.fileMetadataSizeParsed?.height ?? 256
|
||||
file.metadataPhotosSize = {}
|
||||
if (file.width && file.height) {
|
||||
file.metadataPhotosSize.width = file.width
|
||||
file.metadataPhotosSize.height = file.height
|
||||
} else {
|
||||
file.fileMetadataSizeParsed = { width: 256, height: 256 }
|
||||
file.metadataPhotosSize = { width: 256, height: 256 }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,9 +63,10 @@ const mutations = {
|
|||
file.fileid = file.fileid.toString()
|
||||
|
||||
// Precalculate dates as it is expensive.
|
||||
file.timestamp = moment(file.lastmod).unix() // For sorting
|
||||
file.month = moment(file.lastmod).format('YYYYMM') // For grouping by month
|
||||
file.day = moment(file.lastmod).format('MMDD') // For On this day
|
||||
const date = moment(file.lastmod)
|
||||
file.timestamp = date.unix() // For sorting
|
||||
file.month = date.format('YYYYMM') // For grouping by month
|
||||
file.day = date.format('MMDD') // For On this day
|
||||
|
||||
// Schedule the file to add
|
||||
files[file.fileid] = file
|
||||
|
|
|
@ -583,96 +583,6 @@ namespace OC\Files\Storage\Wrapper{
|
|||
}
|
||||
}
|
||||
|
||||
namespace OC\Metadata {
|
||||
|
||||
use OCP\Files\File;
|
||||
|
||||
/**
|
||||
* Interface to manage additional metadata for files
|
||||
*/
|
||||
interface IMetadataManager {
|
||||
/**
|
||||
* @param class-string<IMetadataProvider> $className
|
||||
*/
|
||||
public function registerProvider(string $className): void;
|
||||
|
||||
/**
|
||||
* Generate the metadata for one file
|
||||
*/
|
||||
public function generateMetadata(File $node, bool $checkExisting = false): void;
|
||||
|
||||
/**
|
||||
* Clear the metadata for one file
|
||||
*/
|
||||
public function clearMetadata(int $fileId): void;
|
||||
|
||||
/** @return array<int, FileMetadata> */
|
||||
public function fetchMetadataFor(string $group, array $fileIds): array;
|
||||
|
||||
/**
|
||||
* Get the capabilites as an array of mimetype regex to the type provided
|
||||
*/
|
||||
public function getCapabilities(): array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for the metadata providers. If you want an application to provide
|
||||
* some metadata, you can use this to store them.
|
||||
*/
|
||||
interface IMetadataProvider {
|
||||
/**
|
||||
* The list of groups that this metadata provider is able to provide.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public static function groupsProvided(): array;
|
||||
|
||||
/**
|
||||
* Check if the metadata provider is available. A metadata provider might be
|
||||
* unavailable due to a php extension not being installed.
|
||||
*/
|
||||
public static function isAvailable(): bool;
|
||||
|
||||
/**
|
||||
* Get the mimetypes supported as a regex.
|
||||
*/
|
||||
public static function getMimetypesSupported(): string;
|
||||
|
||||
/**
|
||||
* Execute the extraction on the specified file. The metadata should be
|
||||
* grouped by metadata
|
||||
*
|
||||
* Each group should be json serializable and the string representation
|
||||
* shouldn't be longer than 4000 characters.
|
||||
*
|
||||
* @param File $file The file to extract the metadata from
|
||||
* @param array<string, FileMetadata> An array containing all the metadata fetched.
|
||||
*/
|
||||
public function execute(File $file): array;
|
||||
}
|
||||
|
||||
use OCP\AppFramework\Db\Entity;
|
||||
use OCP\DB\Types;
|
||||
|
||||
/**
|
||||
* @method string getGroupName()
|
||||
* @method void setGroupName(string $groupName)
|
||||
* @method string getValue()
|
||||
* @method void setValue(string $value)
|
||||
* @see \OC\Core\Migrations\Version240000Date20220404230027
|
||||
*/
|
||||
class FileMetadata extends Entity {
|
||||
public function getDecodedValue(): array {
|
||||
return json_decode($this->getValue(), true) ?? [];
|
||||
}
|
||||
|
||||
public function setArrayAsValue(array $value): void {
|
||||
$this->setValue(json_encode($value, JSON_THROW_ON_ERROR));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace OCA\DAV\Connector\Sabre {
|
||||
class Node extends \Sabre\Dav\INode {
|
||||
public function getNode(): \OCP\Files\Node { }
|
||||
|
|
Loading…
Reference in New Issue