Skip to content

Commit 40ba055

Browse files
committed
Add CLI to reverse geocode pictures' coordinates
Signed-off-by: Louis Chemineau <louis@chmn.me>
1 parent 1cc3625 commit 40ba055

7 files changed

Lines changed: 447 additions & 8 deletions

File tree

appinfo/info.xml

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,21 @@
11
<?xml version="1.0"?>
2-
<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
3-
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
2+
<info xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
43
<id>photos</id>
54
<name>Photos</name>
65
<summary>Your memories under your control</summary>
76
<description>Your memories under your control</description>
87
<version>1.8.0</version>
98
<licence>agpl</licence>
10-
<author mail="skjnldsv@protonmail.com">John Molakvoæ</author>
9+
<author mail="skjnldsv@protonmail.com">John Molakvoæ</author>
1110
<namespace>Photos</namespace>
1211
<category>multimedia</category>
1312
<types>
1413
<dav />
1514
<authentication />
1615
</types>
1716

18-
<website>https://github.com/nextcloud/photos</website>
19-
<bugs>https://github.com/nextcloud/photos/issues</bugs>
17+
<website>https://github.com/nextcloud/photos</website>
18+
<bugs>https://github.com/nextcloud/photos/issues</bugs>
2019
<repository>https://github.com/nextcloud/photos.git</repository>
2120
<default_enable />
2221
<dependencies>
@@ -30,6 +29,10 @@
3029
</navigation>
3130
</navigations>
3231

32+
<commands>
33+
<command>OCA\Photos\Command\ReverseGeoCodeMedia</command>
34+
</commands>
35+
3336
<sabre>
3437
<collections>
3538
<collection>OCA\Photos\Sabre\RootCollection</collection>
@@ -39,4 +42,4 @@
3942
<plugin>OCA\Photos\Sabre\Album\PropFindPlugin</plugin>
4043
</plugins>
4144
</sabre>
42-
</info>
45+
</info>

composer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,8 @@
2020
"vimeo/psalm": "^4.22",
2121
"sabre/dav": "^4.2.1",
2222
"nextcloud/ocp": "dev-master"
23+
},
24+
"require": {
25+
"hexogen/kdtree": "^0.2.0"
2326
}
2427
}

composer.lock

Lines changed: 64 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* @copyright Copyright (c) 2022 Louis Chemineau <louis@chmn.me>
6+
*
7+
* @author Louis Chemineau <louis@chmn.me>
8+
*
9+
* @license AGPL-3.0-or-later
10+
*
11+
* This program is free software: you can redistribute it and/or modify
12+
* it under the terms of the GNU Affero General Public License as
13+
* published by the Free Software Foundation, either version 3 of the
14+
* License, or (at your option) any later version.
15+
*
16+
* This program is distributed in the hope that it will be useful,
17+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
18+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+
* GNU Affero General Public License for more details.
20+
*
21+
* You should have received a copy of the GNU Affero General Public License
22+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
23+
*
24+
*/
25+
namespace OCA\Photos\Service;
26+
27+
use OCA\Photos\Service\ReverseGeoCoderService;
28+
use Symfony\Component\Console\Command\Command;
29+
use Symfony\Component\Console\Input\InputInterface;
30+
use Symfony\Component\Console\Output\OutputInterface;
31+
32+
class DownloadReverseGeocodingFiles extends Command {
33+
private ReverseGeoCoderService $rgcService;
34+
35+
public function __construct(
36+
ReverseGeoCoderService $rgcService
37+
) {
38+
parent::__construct();
39+
$this->rgcService = $rgcService;
40+
}
41+
42+
/**
43+
* Configure the command
44+
*
45+
* @return void
46+
*/
47+
protected function configure() {
48+
$this->setName('photos:update-locations-files')
49+
->setDescription('Update the necessary reverse geocoding files');
50+
}
51+
52+
/**
53+
* Execute the command
54+
*
55+
* @param InputInterface $input
56+
* @param OutputInterface $output
57+
*
58+
* @return int
59+
*/
60+
protected function execute(InputInterface $input, OutputInterface $output): int {
61+
try {
62+
$this->rgcService->init(true);
63+
} catch (\Exception $ex) {
64+
$output->writeln('<error>Failed to update reverse geocoding files</error>');
65+
$output->writeln($ex->getMessage());
66+
return 1;
67+
}
68+
69+
return 0;
70+
}
71+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* @copyright Copyright (c) 2022 Louis Chemineau <louis@chmn.me>
6+
*
7+
* @author Louis Chemineau <louis@chmn.me>
8+
*
9+
* @license AGPL-3.0-or-later
10+
*
11+
* This program is free software: you can redistribute it and/or modify
12+
* it under the terms of the GNU Affero General Public License as
13+
* published by the Free Software Foundation, either version 3 of the
14+
* License, or (at your option) any later version.
15+
*
16+
* This program is distributed in the hope that it will be useful,
17+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
18+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+
* GNU Affero General Public License for more details.
20+
*
21+
* You should have received a copy of the GNU Affero General Public License
22+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
23+
*
24+
*/
25+
namespace OCA\Photos\Command;
26+
27+
use OC\Metadata\MetadataManager;
28+
use OCP\IUser;
29+
use OCP\IConfig;
30+
use OCP\Files\IRootFolder;
31+
use OCP\Files\Folder;
32+
use OCP\BackgroundJob\IJobList;
33+
use OCA\Photos\Service\ReverseGeoCoderService;
34+
use OCA\Photos\Service\LocationTagManager;
35+
use Symfony\Component\Console\Command\Command;
36+
use Symfony\Component\Console\Input\InputInterface;
37+
use Symfony\Component\Console\Input\InputOption;
38+
use Symfony\Component\Console\Output\OutputInterface;
39+
40+
class ReverseGeoCodeMedia extends Command {
41+
private ReverseGeoCoderService $rgcService;
42+
private IRootFolder $rootFolder;
43+
private MetadataManager $metadataManager;
44+
private LocationTagManager $locationTagManager;
45+
private IConfig $config;
46+
47+
public function __construct(
48+
ReverseGeoCoderService $rgcService,
49+
IJobList $jobList,
50+
IRootFolder $rootFolder,
51+
MetadataManager $metadataManager,
52+
LocationTagManager $locationTagManager,
53+
IConfig $config
54+
) {
55+
parent::__construct();
56+
$this->rgcService = $rgcService;
57+
$this->config = $config;
58+
$this->jobList = $jobList;
59+
$this->rootFolder = $rootFolder;
60+
$this->metadataManager = $metadataManager;
61+
$this->locationTagManager = $locationTagManager;
62+
}
63+
64+
/**
65+
* Configure the command
66+
*
67+
* @return void
68+
*/
69+
protected function configure() {
70+
$this->setName('photos:reverse-geocode-media')
71+
->setDescription('Reverse geocode coordinates of users\' media')
72+
->addOption('force', 'f', InputOption::VALUE_OPTIONAL, 'Force the download of the reverse geocoding files.', false);
73+
}
74+
75+
/**
76+
* Execute the command
77+
*
78+
* @param InputInterface $input
79+
* @param OutputInterface $output
80+
*
81+
* @return int
82+
*/
83+
protected function execute(InputInterface $input, OutputInterface $output): int {
84+
if (!$this->config->getSystemValueBool('enable_file_metadata', true)) {
85+
throw new \Exception('File metadata is not enabled.');
86+
}
87+
88+
$this->rgcService->init();
89+
// TODO: add per user runs.
90+
$this->scanForAllUsers();
91+
92+
return 0;
93+
}
94+
95+
private function scanForAllUsers() {
96+
$users = $this->userManager->search('');
97+
98+
foreach ($users as $user) {
99+
$this->scanFilesForUser($user);
100+
}
101+
}
102+
103+
private function scanFilesForUser(IUser $user) {
104+
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
105+
$this->scanFolder($userFolder);
106+
}
107+
108+
private function scanFolder(Folder $folder) {
109+
foreach ($folder->getDirectoryListing() as $node) {
110+
if ($node instanceof Folder) {
111+
$this->scanFolder($node);
112+
continue;
113+
}
114+
115+
if (!str_starts_with($node->getMimeType(), 'image')) {
116+
continue;
117+
}
118+
119+
$gpsMetadata = $this->metadataManager->fetchMetadataFor('gps', [$node->getId()])[$node->getId()];
120+
[$latitude, $longitude] = $gpsMetadata->getMetadata();
121+
$locationId = $this->rgcService->getLocationIdForCoordinates($latitude, $longitude);
122+
$this->locationTagManager->tagFileWithLocationId($node, $locationId);
123+
}
124+
}
125+
}

lib/Service/LocationTagManager.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace OCA\Photos\Service;
4+
5+
use OCP\Files\File;
6+
use OCP\SystemTag\ISystemTagManager;
7+
use OCP\SystemTag\ISystemTagObjectMapper;
8+
use OCP\SystemTag\TagNotFoundException;
9+
10+
class LocationTagManager {
11+
const LOCATION_TAG_PREFIX = 'apps:photos:location*';
12+
13+
private ISystemTagManager $systemTagManager;
14+
private ISystemTagObjectMapper $systemTagObjectMapper;
15+
private array $locationTags;
16+
17+
public function __construct(
18+
ISystemTagManager $systemTagManager,
19+
ISystemTagObjectMapper $systemTagObjectMapper
20+
) {
21+
$this->systemTagManager = $systemTagManager;
22+
$this->systemTagObjectMapper = $systemTagObjectMapper;
23+
$this->locationTags = $this->systemTagManager->getAllTags(false, self::LOCATION_TAG_PREFIX.':*');
24+
}
25+
26+
public function tagFileWithLocationId(File $file, int $locationId) {
27+
$locationTag = self::LOCATION_TAG_PREFIX.':'.$locationId;
28+
$this->createTagIfNoExist($locationTag);
29+
$this->removeExistingLocationTags($file);
30+
$this->systemTagObjectMapper->assignTags($file->getId(), 'files', [$locationTag]);
31+
}
32+
33+
private function createTagIfNoExist(string $tag) {
34+
try {
35+
$this->systemTagManager->getTag($tag, false, false);
36+
} catch (\Exception $ex) {
37+
if ($ex instanceof TagNotFoundException) {
38+
$this->systemTagManager->createTag($tag, false, false);
39+
}
40+
41+
throw $ex;
42+
}
43+
}
44+
45+
private function removeExistingLocationTags(File $file) {
46+
$existingTags = $this->systemTagObjectMapper->getTagIdsForObjects([$file->getId()], 'files');
47+
$existingLocationTags = array_intersect($this->locationTags, $existingTags);
48+
$this->systemTagObjectMapper->unassignTags($file->getId(), 'files', $existingLocationTags);
49+
}
50+
}

0 commit comments

Comments
 (0)