Skip to content

Commit 4ed1fc1

Browse files
authored
Merge pull request #1310 from nextcloud/artonge/feat/reverse_geocoding_cli
Add CLI and listeners to reverse geocode pictures' coordinates
2 parents 79217e6 + d94f30c commit 4ed1fc1

20 files changed

Lines changed: 1099 additions & 78 deletions

.github/workflows/cypress.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ jobs:
1919
- name: Checkout app
2020
uses: actions/checkout@v3
2121

22+
- name: Install server dependencies
23+
run: composer install
24+
2225
- name: Read package.json node and npm engines version
2326
uses: skjnldsv/read-package-engines-version-actions@v2.0
2427
id: versions
@@ -35,7 +38,7 @@ jobs:
3538
- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
3639
run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}"
3740

38-
- name: Install dependencies & build app
41+
- name: Install node dependencies & build app
3942
run: |
4043
npm ci
4144
TESTING=true npm run build --if-present

.php-cs-fixer.dist.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
declare(strict_types=1);
44

5-
require_once './vendor/autoload.php';
5+
require_once __DIR__ . '/vendor/autoload.php';
66

77
use Nextcloud\CodingStandard\Config;
88

appinfo/info.xml

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
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"
3+
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
44
<id>photos</id>
55
<name>Photos</name>
66
<summary>Your memories under your control</summary>
77
<description>Your memories under your control</description>
88
<version>2.2.0</version>
99
<licence>agpl</licence>
10-
<author mail="skjnldsv@protonmail.com">John Molakvoæ</author>
10+
<author mail="skjnldsv@protonmail.com">John Molakvoæ</author>
1111
<namespace>Photos</namespace>
1212
<category>multimedia</category>
1313
<types>
1414
<dav />
1515
<authentication />
1616
</types>
1717

18-
<website>https://github.com/nextcloud/photos</website>
19-
<bugs>https://github.com/nextcloud/photos/issues</bugs>
18+
<website>https://github.com/nextcloud/photos</website>
19+
<bugs>https://github.com/nextcloud/photos/issues</bugs>
2020
<repository>https://github.com/nextcloud/photos.git</repository>
2121
<default_enable />
2222
<dependencies>
@@ -30,6 +30,11 @@
3030
</navigation>
3131
</navigations>
3232

33+
<commands>
34+
<command>OCA\Photos\Command\UpdateReverseGeocodingFilesCommand</command>
35+
<command>OCA\Photos\Command\MapMediaToLocationCommand</command>
36+
</commands>
37+
3338
<sabre>
3439
<collections>
3540
<collection>OCA\Photos\Sabre\RootCollection</collection>
@@ -39,4 +44,8 @@
3944
<plugin>OCA\Photos\Sabre\Album\PropFindPlugin</plugin>
4045
</plugins>
4146
</sabre>
42-
</info>
47+
48+
<background-jobs>
49+
<job>OCA\Photos\Jobs\AutomaticLocationMapperJob</job>
50+
</background-jobs>
51+
</info>

composer.json

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

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.

lib/Album/AlbumFile.php

Lines changed: 11 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,11 @@
2323

2424
namespace OCA\Photos\Album;
2525

26-
use OC\Metadata\FileMetadata;
26+
use OCA\Photos\DB\PhotosFile;
2727

28-
class AlbumFile {
29-
private int $fileId;
30-
private string $name;
31-
private string $mimeType;
32-
private int $size;
33-
private int $mtime;
34-
private string $etag;
28+
class AlbumFile extends PhotosFile {
3529
private int $added;
3630
private string $owner;
37-
/** @var array<string, FileMetadata> */
38-
private array $metaData = [];
3931

4032
public function __construct(
4133
int $fileId,
@@ -47,52 +39,19 @@ public function __construct(
4739
int $added,
4840
string $owner
4941
) {
50-
$this->fileId = $fileId;
51-
$this->name = $name;
52-
$this->mimeType = $mimeType;
53-
$this->size = $size;
54-
$this->mtime = $mtime;
55-
$this->etag = $etag;
42+
parent::__construct(
43+
$fileId,
44+
$name,
45+
$mimeType,
46+
$size,
47+
$mtime,
48+
$etag
49+
);
50+
5651
$this->added = $added;
5752
$this->owner = $owner;
5853
}
5954

60-
public function getFileId(): int {
61-
return $this->fileId;
62-
}
63-
64-
public function getName(): string {
65-
return $this->name;
66-
}
67-
68-
public function getMimeType(): string {
69-
return $this->mimeType;
70-
}
71-
72-
public function getSize(): int {
73-
return $this->size;
74-
}
75-
76-
public function getMTime(): int {
77-
return $this->mtime;
78-
}
79-
80-
public function getEtag(): string {
81-
return $this->etag;
82-
}
83-
84-
public function setMetadata(string $key, FileMetadata $value): void {
85-
$this->metaData[$key] = $value;
86-
}
87-
88-
public function hasMetadata(string $key): bool {
89-
return isset($this->metaData[$key]);
90-
}
91-
92-
public function getMetadata(string $key): FileMetadata {
93-
return $this->metaData[$key];
94-
}
95-
9655
public function getAdded(): int {
9756
return $this->added;
9857
}

lib/AppInfo/Application.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use OCA\Photos\Listener\TagListener;
3333
use OCA\Photos\Listener\GroupUserRemovedListener;
3434
use OCA\Photos\Listener\GroupDeletedListener;
35+
use OCA\Photos\Listener\LocationManagerEventListener;
3536
use OCP\AppFramework\App;
3637
use OCP\AppFramework\Bootstrap\IBootContext;
3738
use OCP\AppFramework\Bootstrap\IBootstrap;
@@ -40,6 +41,9 @@
4041
use OCP\SystemTag\MapperEvent;
4142
use OCP\Group\Events\UserRemovedEvent;
4243
use OCP\Group\Events\GroupDeletedEvent;
44+
use OCP\Files\Events\Node\NodeWrittenEvent;
45+
46+
require_once __DIR__ . '/../../vendor/autoload.php';
4347

4448
class Application extends App implements IBootstrap {
4549
public const APP_ID = 'photos';
@@ -78,6 +82,9 @@ public function register(IRegistrationContext $context): void {
7882

7983
$context->registerEventListener(GroupDeletedEvent::class, GroupDeletedListener::class);
8084

85+
// Priority of -1 to be triggered after event listeners populating metadata.
86+
$context->registerEventListener(NodeWrittenEvent::class, LocationManagerEventListener::class, -1);
87+
8188
$context->registerEventListener(SabrePluginAuthInitEvent::class, SabrePluginAuthInitListener::class);
8289

8390
$context->registerEventListener(MapperEvent::EVENT_ASSIGN, TagListener::class);
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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 OCP\IConfig;
28+
use OCP\IUserManager;
29+
use OCP\Files\IRootFolder;
30+
use OCP\Files\Folder;
31+
use OCA\Photos\Service\MediaLocationManager;
32+
use Symfony\Component\Console\Command\Command;
33+
use Symfony\Component\Console\Input\InputInterface;
34+
use Symfony\Component\Console\Input\InputOption;
35+
use Symfony\Component\Console\Output\OutputInterface;
36+
37+
class MapMediaToLocationCommand extends Command {
38+
public function __construct(
39+
private IRootFolder $rootFolder,
40+
private MediaLocationManager $mediaLocationManager,
41+
private IConfig $config,
42+
private IUserManager $userManager,
43+
) {
44+
parent::__construct();
45+
}
46+
47+
/**
48+
* Configure the command
49+
*/
50+
protected function configure(): void {
51+
$this->setName('photos:map-media-to-location')
52+
->setDescription('Reverse geocode media coordinates.')
53+
->addOption('user', 'u', InputOption::VALUE_REQUIRED, 'Limit the mapping to a user.', null);
54+
}
55+
56+
/**
57+
* Execute the command
58+
*/
59+
protected function execute(InputInterface $input, OutputInterface $output): int {
60+
if (!$this->config->getSystemValueBool('enable_file_metadata', true)) {
61+
throw new \Exception('File metadata is not enabled.');
62+
}
63+
64+
$userId = $input->getOption('user');
65+
if ($userId === null) {
66+
$this->scanForAllUsers($output);
67+
} else {
68+
$this->scanFilesForUser($userId, $output);
69+
}
70+
71+
return 0;
72+
}
73+
74+
private function scanForAllUsers(OutputInterface $output): void {
75+
$users = $this->userManager->search('');
76+
77+
$output->writeln("Scanning all users:");
78+
foreach ($users as $user) {
79+
$this->scanFilesForUser($user->getUID(), $output);
80+
}
81+
}
82+
83+
private function scanFilesForUser(string $userId, OutputInterface $output): void {
84+
$userFolder = $this->rootFolder->getUserFolder($userId);
85+
$output->write(" - Scanning files for $userId");
86+
$startTime = time();
87+
$count = $this->scanFolder($userFolder);
88+
$timeElapse = time() - $startTime;
89+
$output->writeln(" - $count files, $timeElapse sec");
90+
}
91+
92+
private function scanFolder(Folder $folder): int {
93+
$count = 0;
94+
95+
// Do not scan share and other moveable mounts.
96+
if ($folder->getMountPoint() instanceof \OC\Files\Mount\MoveableMount) {
97+
return $count;
98+
}
99+
100+
foreach ($folder->getDirectoryListing() as $node) {
101+
if ($node instanceof Folder) {
102+
$count += $this->scanFolder($node);
103+
continue;
104+
}
105+
106+
if (!str_starts_with($node->getMimeType(), 'image')) {
107+
continue;
108+
}
109+
110+
$this->mediaLocationManager->setLocationForFile($node->getId());
111+
$count++;
112+
}
113+
114+
return $count;
115+
}
116+
}

0 commit comments

Comments
 (0)