Skip to content

Commit

Permalink
Add CLI to reverse geocode pictures' coordinates
Browse files Browse the repository at this point in the history
Signed-off-by: Louis Chemineau <louis@chmn.me>
  • Loading branch information
artonge committed Oct 3, 2022
1 parent 1cc3625 commit 40ba055
Show file tree
Hide file tree
Showing 7 changed files with 447 additions and 8 deletions.
15 changes: 9 additions & 6 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
<?xml version="1.0"?>
<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
<info xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
<id>photos</id>
<name>Photos</name>
<summary>Your memories under your control</summary>
<description>Your memories under your control</description>
<version>1.8.0</version>
<licence>agpl</licence>
<author mail="skjnldsv@protonmail.com">John Molakvoæ</author>
<author mail="skjnldsv@protonmail.com">John Molakvoæ</author>
<namespace>Photos</namespace>
<category>multimedia</category>
<types>
<dav />
<authentication />
</types>

<website>https://github.com/nextcloud/photos</website>
<bugs>https://github.com/nextcloud/photos/issues</bugs>
<website>https://github.com/nextcloud/photos</website>
<bugs>https://github.com/nextcloud/photos/issues</bugs>
<repository>https://github.com/nextcloud/photos.git</repository>
<default_enable />
<dependencies>
Expand All @@ -30,6 +29,10 @@
</navigation>
</navigations>

<commands>
<command>OCA\Photos\Command\ReverseGeoCodeMedia</command>
</commands>

<sabre>
<collections>
<collection>OCA\Photos\Sabre\RootCollection</collection>
Expand All @@ -39,4 +42,4 @@
<plugin>OCA\Photos\Sabre\Album\PropFindPlugin</plugin>
</plugins>
</sabre>
</info>
</info>
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,8 @@
"vimeo/psalm": "^4.22",
"sabre/dav": "^4.2.1",
"nextcloud/ocp": "dev-master"
},
"require": {
"hexogen/kdtree": "^0.2.0"
}
}
66 changes: 64 additions & 2 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

71 changes: 71 additions & 0 deletions lib/Command/DownloadReverseGeocodingFiles.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?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\Service;

use OCA\Photos\Service\ReverseGeoCoderService;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class DownloadReverseGeocodingFiles extends Command {
private ReverseGeoCoderService $rgcService;

public function __construct(
ReverseGeoCoderService $rgcService
) {
parent::__construct();
$this->rgcService = $rgcService;
}

/**
* Configure the command
*
* @return void
*/
protected function configure() {
$this->setName('photos:update-locations-files')
->setDescription('Update the necessary reverse geocoding files');
}

/**
* Execute the command
*
* @param InputInterface $input
* @param OutputInterface $output
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int {
try {
$this->rgcService->init(true);
} catch (\Exception $ex) {
$output->writeln('<error>Failed to update reverse geocoding files</error>');
$output->writeln($ex->getMessage());
return 1;
}

return 0;
}
}
125 changes: 125 additions & 0 deletions lib/Command/ReverseGeoCodeMedia.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?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 OC\Metadata\MetadataManager;
use OCP\IUser;
use OCP\IConfig;
use OCP\Files\IRootFolder;
use OCP\Files\Folder;
use OCP\BackgroundJob\IJobList;
use OCA\Photos\Service\ReverseGeoCoderService;
use OCA\Photos\Service\LocationTagManager;
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 ReverseGeoCodeMedia extends Command {
private ReverseGeoCoderService $rgcService;
private IRootFolder $rootFolder;
private MetadataManager $metadataManager;
private LocationTagManager $locationTagManager;
private IConfig $config;

public function __construct(
ReverseGeoCoderService $rgcService,
IJobList $jobList,
IRootFolder $rootFolder,
MetadataManager $metadataManager,
LocationTagManager $locationTagManager,
IConfig $config
) {
parent::__construct();
$this->rgcService = $rgcService;
$this->config = $config;
$this->jobList = $jobList;
$this->rootFolder = $rootFolder;
$this->metadataManager = $metadataManager;
$this->locationTagManager = $locationTagManager;
}

/**
* Configure the command
*
* @return void
*/
protected function configure() {
$this->setName('photos:reverse-geocode-media')
->setDescription('Reverse geocode coordinates of users\' media')
->addOption('force', 'f', InputOption::VALUE_OPTIONAL, 'Force the download of the reverse geocoding files.', false);
}

/**
* Execute the command
*
* @param InputInterface $input
* @param OutputInterface $output
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int {
if (!$this->config->getSystemValueBool('enable_file_metadata', true)) {
throw new \Exception('File metadata is not enabled.');
}

$this->rgcService->init();
// TODO: add per user runs.
$this->scanForAllUsers();

return 0;
}

private function scanForAllUsers() {
$users = $this->userManager->search('');

foreach ($users as $user) {
$this->scanFilesForUser($user);
}
}

private function scanFilesForUser(IUser $user) {
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
$this->scanFolder($userFolder);
}

private function scanFolder(Folder $folder) {
foreach ($folder->getDirectoryListing() as $node) {
if ($node instanceof Folder) {
$this->scanFolder($node);
continue;
}

if (!str_starts_with($node->getMimeType(), 'image')) {
continue;
}

$gpsMetadata = $this->metadataManager->fetchMetadataFor('gps', [$node->getId()])[$node->getId()];
[$latitude, $longitude] = $gpsMetadata->getMetadata();
$locationId = $this->rgcService->getLocationIdForCoordinates($latitude, $longitude);
$this->locationTagManager->tagFileWithLocationId($node, $locationId);
}
}
}
50 changes: 50 additions & 0 deletions lib/Service/LocationTagManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace OCA\Photos\Service;

use OCP\Files\File;
use OCP\SystemTag\ISystemTagManager;
use OCP\SystemTag\ISystemTagObjectMapper;
use OCP\SystemTag\TagNotFoundException;

class LocationTagManager {
const LOCATION_TAG_PREFIX = 'apps:photos:location*';

private ISystemTagManager $systemTagManager;
private ISystemTagObjectMapper $systemTagObjectMapper;
private array $locationTags;

public function __construct(
ISystemTagManager $systemTagManager,
ISystemTagObjectMapper $systemTagObjectMapper
) {
$this->systemTagManager = $systemTagManager;
$this->systemTagObjectMapper = $systemTagObjectMapper;
$this->locationTags = $this->systemTagManager->getAllTags(false, self::LOCATION_TAG_PREFIX.':*');
}

public function tagFileWithLocationId(File $file, int $locationId) {
$locationTag = self::LOCATION_TAG_PREFIX.':'.$locationId;
$this->createTagIfNoExist($locationTag);
$this->removeExistingLocationTags($file);
$this->systemTagObjectMapper->assignTags($file->getId(), 'files', [$locationTag]);
}

private function createTagIfNoExist(string $tag) {
try {
$this->systemTagManager->getTag($tag, false, false);
} catch (\Exception $ex) {
if ($ex instanceof TagNotFoundException) {
$this->systemTagManager->createTag($tag, false, false);
}

throw $ex;
}
}

private function removeExistingLocationTags(File $file) {
$existingTags = $this->systemTagObjectMapper->getTagIdsForObjects([$file->getId()], 'files');
$existingLocationTags = array_intersect($this->locationTags, $existingTags);
$this->systemTagObjectMapper->unassignTags($file->getId(), 'files', $existingLocationTags);
}
}
Loading

0 comments on commit 40ba055

Please sign in to comment.