From 7d9db06421f8a965f806f18e4f6f0cc59de887ae Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Thu, 23 Mar 2023 16:58:49 -0700 Subject: [PATCH] big refactor: move more stuff to backend Signed-off-by: Varun Patil --- lib/ClustersBackend/AlbumsBackend.php | 42 ++- lib/ClustersBackend/Backend.php | 64 +++++ .../FaceRecognitionBackend.php | 229 ++++++++++++++++- lib/ClustersBackend/PlacesBackend.php | 76 +++++- lib/ClustersBackend/RecognizeBackend.php | 158 +++++++++++- lib/ClustersBackend/TagsBackend.php | 107 +++++++- lib/Controller/ClustersController.php | 6 +- lib/Controller/DaysController.php | 111 ++++---- lib/Controller/GenericApiControllerParams.php | 5 - lib/Controller/PublicAlbumController.php | 14 +- ...imelineQueryAlbums.php => AlbumsQuery.php} | 69 +---- lib/Db/TimelineQuery.php | 74 +----- lib/Db/TimelineQueryDays.php | 133 +++++----- lib/Db/TimelineQueryFilters.php | 45 ++-- lib/Db/TimelineQueryFolders.php | 7 +- lib/Db/TimelineQueryMap.php | 4 +- lib/Db/TimelineQueryPeopleFaceRecognition.php | 242 ------------------ lib/Db/TimelineQueryPeopleRecognize.php | 165 ------------ lib/Db/TimelineQueryPlaces.php | 80 ------ lib/Db/TimelineQuerySingleItem.php | 59 +++++ lib/Db/TimelineQueryTags.php | 111 -------- lib/Manager/FsManager.php | 20 +- lib/UtilController.php | 8 + src/components/Timeline.vue | 6 +- src/services/API.ts | 2 +- 25 files changed, 893 insertions(+), 944 deletions(-) rename lib/Db/{TimelineQueryAlbums.php => AlbumsQuery.php} (79%) delete mode 100644 lib/Db/TimelineQueryPeopleFaceRecognition.php delete mode 100644 lib/Db/TimelineQueryPeopleRecognize.php delete mode 100644 lib/Db/TimelineQueryPlaces.php delete mode 100644 lib/Db/TimelineQueryTags.php diff --git a/lib/ClustersBackend/AlbumsBackend.php b/lib/ClustersBackend/AlbumsBackend.php index 64125ad2..4b58d64f 100644 --- a/lib/ClustersBackend/AlbumsBackend.php +++ b/lib/ClustersBackend/AlbumsBackend.php @@ -23,18 +23,23 @@ declare(strict_types=1); namespace OCA\Memories\ClustersBackend; -use OCA\Memories\Db\TimelineQuery; +use OCA\Memories\Db\AlbumsQuery; use OCA\Memories\Exceptions; use OCA\Memories\Util; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IRequest; class AlbumsBackend extends Backend { - protected TimelineQuery $timelineQuery; + protected AlbumsQuery $albumsQuery; + protected IRequest $request; public function __construct( - TimelineQuery $timelineQuery + AlbumsQuery $albumsQuery, + IRequest $request ) { - $this->timelineQuery = $timelineQuery; + $this->albumsQuery = $albumsQuery; + $this->request = $request; } public function appName(): string @@ -52,6 +57,27 @@ class AlbumsBackend extends Backend return explode('/', $name)[1]; } + public function transformDays(IQueryBuilder &$query, bool $aggregate): void + { + $albumId = (string) $this->request->getParam('albums'); + + $uid = Util::isLoggedIn() ? Util::getUID() : ''; + + // Get album object + $album = $this->albumsQuery->getIfAllowed($uid, $albumId); + + // Check permission + if (null === $album) { + throw new \Exception("Album {$albumId} not found"); + } + + // WHERE these are items with this album + $query->innerJoin('m', 'photos_albums_files', 'paf', $query->expr()->andX( + $query->expr()->eq('paf.album_id', $query->createNamedParameter($album['album_id'])), + $query->expr()->eq('paf.file_id', 'm.fileid'), + )); + } + public function getClusters(): array { /** @var \OCP\IRequest $request */ @@ -61,10 +87,10 @@ class AlbumsBackend extends Backend $list = []; $t = (int) $request->getParam('t', 0); if ($t & 1) { // personal - $list = array_merge($list, $this->timelineQuery->getAlbums(Util::getUID())); + $list = array_merge($list, $this->albumsQuery->getList(Util::getUID())); } if ($t & 2) { // shared - $list = array_merge($list, $this->timelineQuery->getAlbums(Util::getUID(), true)); + $list = array_merge($list, $this->albumsQuery->getList(Util::getUID(), true)); } // Remove elements with duplicate album_id @@ -85,7 +111,7 @@ class AlbumsBackend extends Backend public function getPhotos(string $name, ?int $limit = null): array { // Get album - $album = $this->timelineQuery->getAlbumIfAllowed(Util::getUID(), $name); + $album = $this->albumsQuery->getIfAllowed(Util::getUID(), $name); if (null === $album) { throw Exceptions::NotFound("album {$name}"); } @@ -93,6 +119,6 @@ class AlbumsBackend extends Backend // Get files $id = (int) $album['album_id']; - return $this->timelineQuery->getAlbumPhotos($id, $limit) ?? []; + return $this->albumsQuery->getAlbumPhotos($id, $limit) ?? []; } } diff --git a/lib/ClustersBackend/Backend.php b/lib/ClustersBackend/Backend.php index 337dcc84..df71baa7 100644 --- a/lib/ClustersBackend/Backend.php +++ b/lib/ClustersBackend/Backend.php @@ -23,6 +23,9 @@ declare(strict_types=1); namespace OCA\Memories\ClustersBackend; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IRequest; + abstract class Backend { /** Mapping of backend name to className */ @@ -39,6 +42,18 @@ abstract class Backend */ abstract public function isEnabled(): bool; + /** + * Apply query transformations for days query. + */ + abstract public function transformDays(IQueryBuilder &$query, bool $aggregate): void; + + /** + * Apply post-query transformations for the given day object. + */ + public function transformDayPhoto(array &$row): void + { + } + /** * Get the cluster list for the current user. */ @@ -53,6 +68,55 @@ abstract class Backend */ abstract public function getPhotos(string $name, ?int $limit = null): array; + /** + * Get a cluster backend. + * + * @param string $name Name of the backend + * + * @throws \Exception If the backend is not registered + */ + public static function get(string $name): self + { + if (!\array_key_exists($name, self::$backends)) { + throw new \Exception("Invalid clusters backend '{$name}'"); + } + + return \OC::$server->get(self::$backends[$name]); + } + + /** + * Apply all query transformations for the given request. + */ + public static function getTransforms(IRequest $request): array + { + $transforms = []; + foreach (array_keys(self::$backends) as $backendName) { + if ($request->getParam($backendName)) { + $backend = self::get($backendName); + if ($backend->isEnabled()) { + $transforms[] = [$backend, 'transformDays']; + } + } + } + + return $transforms; + } + + /** + * Apply all post-query transformations for the given day object. + */ + public static function applyDayPostTransforms(IRequest $request, array &$row): void + { + foreach (array_keys(self::$backends) as $backendName) { + if ($request->getParam($backendName)) { + $backend = self::get($backendName); + if ($backend->isEnabled()) { + $backend->transformDayPhoto($row); + } + } + } + } + /** * Register a new backend. * diff --git a/lib/ClustersBackend/FaceRecognitionBackend.php b/lib/ClustersBackend/FaceRecognitionBackend.php index 222db480..d581a834 100644 --- a/lib/ClustersBackend/FaceRecognitionBackend.php +++ b/lib/ClustersBackend/FaceRecognitionBackend.php @@ -25,20 +25,25 @@ namespace OCA\Memories\ClustersBackend; use OCA\Memories\Db\TimelineQuery; use OCA\Memories\Util; +use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IConfig; +use OCP\IRequest; class FaceRecognitionBackend extends Backend { use PeopleBackendUtils; - protected TimelineQuery $timelineQuery; + protected IRequest $request; + protected TimelineQuery $tq; protected IConfig $config; public function __construct( - TimelineQuery $timelineQuery, + IRequest $request, + TimelineQuery $tq, IConfig $config ) { - $this->timelineQuery = $timelineQuery; + $this->request = $request; + $this->tq = $tq; $this->config = $config; } @@ -53,17 +58,138 @@ class FaceRecognitionBackend extends Backend && Util::facerecognitionIsEnabled(); } + public function transformDays(IQueryBuilder &$query, bool $aggregate): void + { + $personStr = (string) $this->request->getParam('facerecognition'); + + // Get title and uid of face user + $personNames = explode('/', $personStr); + if (2 !== \count($personNames)) { + throw new \Exception('Invalid person query'); + } + + $personUid = $personNames[0]; + $personName = $personNames[1]; + + // Join with images + $query->innerJoin('m', 'facerecog_images', 'fri', $query->expr()->andX( + $query->expr()->eq('fri.file', 'm.fileid'), + $query->expr()->eq('fri.model', $query->createNamedParameter($this->model())), + )); + + // Join with faces + $query->innerJoin( + 'fri', + 'facerecog_faces', + 'frf', + $query->expr()->eq('frf.image', 'fri.id') + ); + + // Join with persons + $nameField = is_numeric($personName) ? 'frp.id' : 'frp.name'; + $query->innerJoin('frf', 'facerecog_persons', 'frp', $query->expr()->andX( + $query->expr()->eq('frf.person', 'frp.id'), + $query->expr()->eq('frp.user', $query->createNamedParameter($personUid)), + $query->expr()->eq($nameField, $query->createNamedParameter($personName)), + )); + + // Add face rect + if (!$aggregate && $this->request->getParam('facerect')) { + $query->addSelect( + 'frf.x AS face_x', + 'frf.y AS face_y', + 'frf.width AS face_width', + 'frf.height AS face_height', + 'm.w AS image_width', + 'm.h AS image_height', + ); + } + } + + public function transformDayPhoto(array &$row): void + { + // Differentiate Recognize queries from Face Recognition + if (!isset($row) || !isset($row['face_width']) || !isset($row['image_width'])) { + return; + } + + // Get percentage position and size + $row['facerect'] = [ + 'w' => (float) $row['face_width'] / $row['image_width'], + 'h' => (float) $row['face_height'] / $row['image_height'], + 'x' => (float) $row['face_x'] / $row['image_width'], + 'y' => (float) $row['face_y'] / $row['image_height'], + ]; + + unset($row['face_x'], $row['face_y'], $row['face_w'], $row['face_h'], $row['image_height'], $row['image_width']); + } + public function getClusters(): array { - return array_merge( - $this->timelineQuery->getFaceRecognitionPersons($this->model()), - $this->timelineQuery->getFaceRecognitionClusters($this->model()) + $faces = array_merge( + $this->getFaceRecognitionPersons(), + $this->getFaceRecognitionClusters() ); + + // Post process + foreach ($faces as &$row) { + $row['id'] = $row['name'] ?: (int) $row['id']; + $row['count'] = (int) $row['count']; + } + + return $faces; } public function getPhotos(string $name, ?int $limit = null): array { - return $this->timelineQuery->getFaceRecognitionPhotos($name, $this->model(), $limit) ?? []; + $query = $this->tq->getBuilder(); + + // SELECT face detections + $query->select( + 'fri.file as file_id', // Get actual file + 'frf.x', // Image cropping + 'frf.y', + 'frf.width', + 'frf.height', + 'm.w as image_width', // Scoring + 'm.h as image_height', + 'frf.confidence', + 'm.fileid', + 'm.datetaken', // Just in case, for postgres + )->from('facerecog_faces', 'frf'); + + // WHERE faces are from images and current model. + $query->innerJoin('frf', 'facerecog_images', 'fri', $query->expr()->andX( + $query->expr()->eq('fri.id', 'frf.image'), + $query->expr()->eq('fri.model', $query->createNamedParameter($this->model())), + )); + + // WHERE these photos are memories indexed + $query->innerJoin('fri', 'memories', 'm', $query->expr()->eq('m.fileid', 'fri.file')); + + $query->innerJoin('frf', 'facerecog_persons', 'frp', $query->expr()->eq('frp.id', 'frf.person')); + if (is_numeric($name)) { + // WHERE faces are from id persons (a cluster). + $query->where($query->expr()->eq('frp.id', $query->createNamedParameter($name))); + } else { + // WHERE faces are from name on persons. + $query->where($query->expr()->eq('frp.name', $query->createNamedParameter($name))); + } + + // WHERE these photos are in the user's requested folder recursively + $query = $this->tq->joinFilecache($query); + + // LIMIT results + if (null !== $limit) { + $query->setMaxResults($limit); + } + + // Sort by date taken so we get recent photos + $query->orderBy('m.datetaken', 'DESC'); + $query->addOrderBy('m.fileid', 'DESC'); // tie-breaker + + // FETCH face detections + return $this->tq->executeQueryWithCTEs($query)->fetchAll() ?: []; } public function sortPhotosForPreview(array &$photos) @@ -93,4 +219,93 @@ class FaceRecognitionBackend extends Backend { return (int) $this->config->getAppValue('facerecognition', 'model', -1); } + + private function getFaceRecognitionClusters(bool $show_singles = false, bool $show_hidden = false) + { + $query = $this->tq->getBuilder(); + + // SELECT all face clusters + $count = $query->func()->count($query->createFunction('DISTINCT m.fileid')); + $query->select('frp.id')->from('facerecog_persons', 'frp'); + $query->selectAlias($count, 'count'); + $query->selectAlias('frp.user', 'user_id'); + + // WHERE there are faces with this cluster + $query->innerJoin('frp', 'facerecog_faces', 'frf', $query->expr()->eq('frp.id', 'frf.person')); + + // WHERE faces are from images. + $query->innerJoin('frf', 'facerecog_images', 'fri', $query->expr()->eq('fri.id', 'frf.image')); + + // WHERE these items are memories indexed photos + $query->innerJoin('fri', 'memories', 'm', $query->expr()->andX( + $query->expr()->eq('fri.file', 'm.fileid'), + $query->expr()->eq('fri.model', $query->createNamedParameter($this->model())), + )); + + // WHERE these photos are in the user's requested folder recursively + $query = $this->tq->joinFilecache($query); + + // GROUP by ID of face cluster + $query->groupBy('frp.id'); + $query->addGroupBy('frp.user'); + $query->where($query->expr()->isNull('frp.name')); + + // By default hides individual faces when they have no name. + if (!$show_singles) { + $query->having($query->expr()->gt($count, $query->createNamedParameter(1))); + } + + // By default it shows the people who were not hidden + if (!$show_hidden) { + $query->andWhere($query->expr()->eq('frp.is_visible', $query->createNamedParameter(true))); + } + + // ORDER by number of faces in cluster and id for response stability. + $query->orderBy('count', 'DESC'); + $query->addOrderBy('frp.id', 'DESC'); + + // It is not worth displaying all unnamed clusters. We show 15 to name them progressively, + $query->setMaxResults(15); + + // FETCH all faces + return $this->tq->executeQueryWithCTEs($query)->fetchAll() ?: []; + } + + private function getFaceRecognitionPersons() + { + $query = $this->tq->getBuilder(); + + // SELECT all face clusters + $count = $query->func()->count($query->createFunction('DISTINCT m.fileid')); + $query->select('frp.name')->from('facerecog_persons', 'frp'); + $query->selectAlias($count, 'count'); + $query->selectAlias('frp.user', 'user_id'); + + // WHERE there are faces with this cluster + $query->innerJoin('frp', 'facerecog_faces', 'frf', $query->expr()->eq('frp.id', 'frf.person')); + + // WHERE faces are from images. + $query->innerJoin('frf', 'facerecog_images', 'fri', $query->expr()->eq('fri.id', 'frf.image')); + + // WHERE these items are memories indexed photos + $query->innerJoin('fri', 'memories', 'm', $query->expr()->andX( + $query->expr()->eq('fri.file', 'm.fileid'), + $query->expr()->eq('fri.model', $query->createNamedParameter($this->model())), + )); + + // WHERE these photos are in the user's requested folder recursively + $query = $this->tq->joinFilecache($query); + + // GROUP by name of face clusters + $query->where($query->expr()->isNotNull('frp.name')); + $query->groupBy('frp.user'); + $query->addGroupBy('frp.name'); + + // ORDER by number of faces in cluster + $query->orderBy('count', 'DESC'); + $query->addOrderBy('frp.name', 'ASC'); + + // FETCH all faces + return $this->tq->executeQueryWithCTEs($query)->fetchAll() ?: []; + } } diff --git a/lib/ClustersBackend/PlacesBackend.php b/lib/ClustersBackend/PlacesBackend.php index 0d038926..0acd113b 100644 --- a/lib/ClustersBackend/PlacesBackend.php +++ b/lib/ClustersBackend/PlacesBackend.php @@ -25,15 +25,18 @@ namespace OCA\Memories\ClustersBackend; use OCA\Memories\Db\TimelineQuery; use OCA\Memories\Util; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IRequest; class PlacesBackend extends Backend { - protected TimelineQuery $timelineQuery; + protected TimelineQuery $tq; + protected IRequest $request; - public function __construct( - TimelineQuery $timelineQuery - ) { - $this->timelineQuery = $timelineQuery; + public function __construct(TimelineQuery $tq, IRequest $request) + { + $this->tq = $tq; + $this->request = $request; } public function appName(): string @@ -46,13 +49,72 @@ class PlacesBackend extends Backend return Util::placesGISType() > 0; } + public function transformDays(IQueryBuilder &$query, bool $aggregate): void + { + $locationId = (int) $this->request->getParam('places'); + + $query->innerJoin('m', 'memories_places', 'mp', $query->expr()->andX( + $query->expr()->eq('mp.fileid', 'm.fileid'), + $query->expr()->eq('mp.osm_id', $query->createNamedParameter($locationId)), + )); + } + public function getClusters(): array { - return $this->timelineQuery->getPlaces(); + $query = $this->tq->getBuilder(); + + // SELECT location name and count of photos + $count = $query->func()->count($query->createFunction('DISTINCT m.fileid'), 'count'); + $query->select('e.osm_id', 'e.name', $count)->from('memories_planet', 'e'); + + // WHERE there are items with this osm_id + $query->innerJoin('e', 'memories_places', 'mp', $query->expr()->eq('mp.osm_id', 'e.osm_id')); + + // WHERE these items are memories indexed photos + $query->innerJoin('mp', 'memories', 'm', $query->expr()->eq('m.fileid', 'mp.fileid')); + + // WHERE these photos are in the user's requested folder recursively + $query = $this->tq->joinFilecache($query); + + // GROUP and ORDER by tag name + $query->groupBy('e.osm_id', 'e.name'); + $query->orderBy($query->createFunction('LOWER(e.name)'), 'ASC'); + $query->addOrderBy('e.osm_id'); // tie-breaker + + // FETCH all tags + $cursor = $this->tq->executeQueryWithCTEs($query); + $places = $cursor->fetchAll(); + + // Post process + foreach ($places as &$row) { + $row['osm_id'] = (int) $row['osm_id']; + $row['count'] = (int) $row['count']; + } + + return $places; } public function getPhotos(string $name, ?int $limit = null): array { - return $this->timelineQuery->getPlacePhotos((int) $name, $limit) ?? []; + $query = $this->tq->getBuilder(); + + // SELECT all photos with this tag + $query->select('f.fileid', 'f.etag')->from('memories_places', 'mp') + ->where($query->expr()->eq('mp.osm_id', $query->createNamedParameter((int) $name))) + ; + + // WHERE these items are memories indexed photos + $query->innerJoin('mp', 'memories', 'm', $query->expr()->eq('m.fileid', 'mp.fileid')); + + // WHERE these photos are in the user's requested folder recursively + $query = $this->tq->joinFilecache($query); + + // MAX number of photos + if (null !== $limit) { + $query->setMaxResults($limit); + } + + // FETCH tag photos + return $this->tq->executeQueryWithCTEs($query)->fetchAll() ?: []; } } diff --git a/lib/ClustersBackend/RecognizeBackend.php b/lib/ClustersBackend/RecognizeBackend.php index cd8fb29e..a84d0c1b 100644 --- a/lib/ClustersBackend/RecognizeBackend.php +++ b/lib/ClustersBackend/RecognizeBackend.php @@ -25,17 +25,20 @@ namespace OCA\Memories\ClustersBackend; use OCA\Memories\Db\TimelineQuery; use OCA\Memories\Util; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IRequest; class RecognizeBackend extends Backend { use PeopleBackendUtils; - protected TimelineQuery $timelineQuery; + protected TimelineQuery $tq; + protected IRequest $request; - public function __construct( - TimelineQuery $timelineQuery - ) { - $this->timelineQuery = $timelineQuery; + public function __construct(TimelineQuery $tq, IRequest $request) + { + $this->tq = $tq; + $this->request = $request; } public function appName(): string @@ -48,14 +51,155 @@ class RecognizeBackend extends Backend return Util::recognizeIsEnabled(); } + public function transformDays(IQueryBuilder &$query, bool $aggregate): void + { + $faceStr = (string) $this->request->getParam('recognize'); + + // Get name and uid of face user + $faceNames = explode('/', $faceStr); + if (2 !== \count($faceNames)) { + throw new \Exception('Invalid face query'); + } + + // Starting with Recognize v3.6, the detections are duplicated for each user + // So we don't need to use the user ID provided by the user, but retain + // this here for backwards compatibility + API consistency with Face Recognition + // $faceUid = $faceNames[0]; + + $faceName = $faceNames[1]; + + if (!$aggregate) { + // Multiple detections for the same image + $query->addSelect('rfd.id AS faceid'); + + // Face Rect + if ($this->request->getParam('facerect')) { + $query->addSelect( + 'rfd.width AS face_w', + 'rfd.height AS face_h', + 'rfd.x AS face_x', + 'rfd.y AS face_y', + ); + } + } + + // Join with cluster + $clusterQuery = null; + if ('NULL' === $faceName) { + $clusterQuery = $query->expr()->isNull('rfd.cluster_id'); + } else { + $nameField = is_numeric($faceName) ? 'rfc.id' : 'rfc.title'; + $query->innerJoin('m', 'recognize_face_clusters', 'rfc', $query->expr()->andX( + $query->expr()->eq('rfc.user_id', $query->createNamedParameter(Util::getUID())), + $query->expr()->eq($nameField, $query->createNamedParameter($faceName)), + )); + $clusterQuery = $query->expr()->eq('rfd.cluster_id', 'rfc.id'); + } + + // Join with detections + $query->innerJoin('m', 'recognize_face_detections', 'rfd', $query->expr()->andX( + $query->expr()->eq('rfd.file_id', 'm.fileid'), + $clusterQuery, + )); + } + + public function transformDayPhoto(array &$row): void + { + // Differentiate Recognize queries from Face Recognition + if (!isset($row) || !isset($row['face_w'])) { + return; + } + + // Convert face rect to object + $row['facerect'] = [ + 'w' => (float) $row['face_w'], + 'h' => (float) $row['face_h'], + 'x' => (float) $row['face_x'], + 'y' => (float) $row['face_y'], + ]; + + unset($row['face_w'], $row['face_h'], $row['face_x'], $row['face_y']); + } + public function getClusters(): array { - return $this->timelineQuery->getPeopleRecognize(Util::getUID()); + $query = $this->tq->getBuilder(); + + // SELECT all face clusters + $count = $query->func()->count($query->createFunction('DISTINCT m.fileid'), 'count'); + $query->select('rfc.id', 'rfc.user_id', 'rfc.title', $count)->from('recognize_face_clusters', 'rfc'); + + // WHERE there are faces with this cluster + $query->innerJoin('rfc', 'recognize_face_detections', 'rfd', $query->expr()->eq('rfc.id', 'rfd.cluster_id')); + + // WHERE these items are memories indexed photos + $query->innerJoin('rfd', 'memories', 'm', $query->expr()->eq('m.fileid', 'rfd.file_id')); + + // WHERE these photos are in the user's requested folder recursively + $query = $this->tq->joinFilecache($query); + + // WHERE this cluster belongs to the user + $query->where($query->expr()->eq('rfc.user_id', $query->createNamedParameter(Util::getUID()))); + + // GROUP by ID of face cluster + $query->groupBy('rfc.id'); + + // ORDER by number of faces in cluster + $query->orderBy($query->createFunction("rfc.title <> ''"), 'DESC'); + $query->addOrderBy('count', 'DESC'); + $query->addOrderBy('rfc.id'); // tie-breaker + + // FETCH all faces + $faces = $this->tq->executeQueryWithCTEs($query)->fetchAll() ?: []; + + // Post process + foreach ($faces as &$row) { + $row['id'] = (int) $row['id']; + $row['count'] = (int) $row['count']; + $row['name'] = $row['title']; + unset($row['title']); + } + + return $faces; } public function getPhotos(string $name, ?int $limit = null): array { - return $this->timelineQuery->getPeopleRecognizePhotos((int) $name, $limit) ?? []; + $query = $this->tq->getBuilder(); + + // SELECT face detections for ID + $query->select( + 'rfd.file_id', // Get actual file + 'rfd.x', // Image cropping + 'rfd.y', + 'rfd.width', + 'rfd.height', + 'm.w as image_width', // Scoring + 'm.h as image_height', + 'm.fileid', + 'm.datetaken', // Just in case, for postgres + )->from('recognize_face_detections', 'rfd'); + + // WHERE detection belongs to this cluster + $query->where($query->expr()->eq('rfd.cluster_id', $query->createNamedParameter($name))); + + // WHERE these photos are memories indexed + $query->innerJoin('rfd', 'memories', 'm', $query->expr()->eq('m.fileid', 'rfd.file_id')); + + // WHERE these photos are in the user's requested folder recursively + $query = $this->tq->joinFilecache($query); + + // LIMIT results + if (null !== $limit) { + $query->setMaxResults($limit); + } + + // Sort by date taken so we get recent photos + $query->orderBy('m.datetaken', 'DESC'); + $query->addOrderBy('m.fileid', 'DESC'); // tie-breaker + + // FETCH face detections + return $this->tq->executeQueryWithCTEs($query)->fetchAll() ?: []; } public function sortPhotosForPreview(array &$photos) diff --git a/lib/ClustersBackend/TagsBackend.php b/lib/ClustersBackend/TagsBackend.php index 26853876..67d74e99 100644 --- a/lib/ClustersBackend/TagsBackend.php +++ b/lib/ClustersBackend/TagsBackend.php @@ -25,15 +25,18 @@ namespace OCA\Memories\ClustersBackend; use OCA\Memories\Db\TimelineQuery; use OCA\Memories\Util; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IRequest; class TagsBackend extends Backend { - protected TimelineQuery $timelineQuery; + protected TimelineQuery $tq; + protected IRequest $request; - public function __construct( - TimelineQuery $timelineQuery - ) { - $this->timelineQuery = $timelineQuery; + public function __construct(TimelineQuery $tq, IRequest $request) + { + $this->tq = $tq; + $this->request = $request; } public function appName(): string @@ -46,13 +49,103 @@ class TagsBackend extends Backend return Util::tagsIsEnabled(); } + public function transformDays(IQueryBuilder &$query, bool $aggregate): void + { + $tagName = (string) $this->request->getParam('tags'); + + $tagId = $this->getSystemTagId($query, $tagName); + if (false === $tagId) { + throw new \Exception("Tag {$tagName} not found"); + } + + $query->innerJoin('m', 'systemtag_object_mapping', 'stom', $query->expr()->andX( + $query->expr()->eq('stom.objecttype', $query->createNamedParameter('files')), + $query->expr()->eq('stom.objectid', 'm.objectid'), + $query->expr()->eq('stom.systemtagid', $query->createNamedParameter($tagId)), + )); + } + public function getClusters(): array { - return $this->timelineQuery->getTags(); + $query = $this->tq->getBuilder(); + + // SELECT visible tag name and count of photos + $count = $query->func()->count($query->createFunction('DISTINCT m.fileid'), 'count'); + $query->select('st.id', 'st.name', $count)->from('systemtag', 'st')->where( + $query->expr()->eq('visibility', $query->createNamedParameter(1)), + ); + + // WHERE there are items with this tag + $query->innerJoin('st', 'systemtag_object_mapping', 'stom', $query->expr()->andX( + $query->expr()->eq('stom.objecttype', $query->createNamedParameter('files')), + $query->expr()->eq('stom.systemtagid', 'st.id'), + )); + + // WHERE these items are memories indexed photos + $query->innerJoin('stom', 'memories', 'm', $query->expr()->eq('m.objectid', 'stom.objectid')); + + // WHERE these photos are in the user's requested folder recursively + $query = $this->tq->joinFilecache($query); + + // GROUP and ORDER by tag name + $query->groupBy('st.id'); + $query->orderBy($query->createFunction('LOWER(st.name)'), 'ASC'); + $query->addOrderBy('st.id'); // tie-breaker + + // FETCH all tags + $cursor = $this->tq->executeQueryWithCTEs($query); + $tags = $cursor->fetchAll(); + + // Post process + foreach ($tags as &$row) { + $row['id'] = (int) $row['id']; + $row['count'] = (int) $row['count']; + } + + return $tags; } public function getPhotos(string $name, ?int $limit = null): array { - return $this->timelineQuery->getTagPhotos($name, $limit) ?? []; + $query = $this->tq->getBuilder(); + $tagId = $this->getSystemTagId($query, $name); + if (false === $tagId) { + return []; + } + + // SELECT all photos with this tag + $query->select('f.fileid', 'f.etag', 'stom.systemtagid')->from( + 'systemtag_object_mapping', + 'stom' + )->where( + $query->expr()->eq('stom.objecttype', $query->createNamedParameter('files')), + $query->expr()->eq('stom.systemtagid', $query->createNamedParameter($tagId)), + ); + + // WHERE these items are memories indexed photos + $query->innerJoin('stom', 'memories', 'm', $query->expr()->eq('m.objectid', 'stom.objectid')); + + // WHERE these photos are in the user's requested folder recursively + $query = $this->tq->joinFilecache($query); + + // MAX number of files + if (null !== $limit) { + $query->setMaxResults($limit); + } + + // FETCH tag photos + return $this->tq->executeQueryWithCTEs($query)->fetchAll(); + } + + private function getSystemTagId(IQueryBuilder $query, string $tagName) + { + $sqb = $query->getConnection()->getQueryBuilder(); + + return $sqb->select('id')->from('systemtag')->where( + $sqb->expr()->andX( + $sqb->expr()->eq('name', $sqb->createNamedParameter($tagName)), + $sqb->expr()->eq('visibility', $sqb->createNamedParameter(1)), + ) + )->executeQuery()->fetchOne(); } } diff --git a/lib/Controller/ClustersController.php b/lib/Controller/ClustersController.php index c81ece18..c9a74ffa 100644 --- a/lib/Controller/ClustersController.php +++ b/lib/Controller/ClustersController.php @@ -114,11 +114,7 @@ class ClustersController extends GenericApiController throw Exceptions::NotLoggedIn(); } - if (\array_key_exists($backend, Backend::$backends)) { - $this->backend = \OC::$server->get(Backend::$backends[$backend]); - } else { - throw new \Exception("Invalid clusters backend '{$backend}'"); - } + $this->backend = Backend::get($backend); if (!$this->backend->isEnabled()) { throw Exceptions::NotEnabled($this->backend->appName()); diff --git a/lib/Controller/DaysController.php b/lib/Controller/DaysController.php index 35020d08..9c5304ce 100644 --- a/lib/Controller/DaysController.php +++ b/lib/Controller/DaysController.php @@ -40,21 +40,18 @@ class DaysController extends GenericApiController public function days(): Http\Response { return Util::guardEx(function () { - $uid = $this->getShareToken() ? '' : Util::getUID(); - $list = $this->timelineQuery->getDays( - $uid, $this->isRecursive(), $this->isArchive(), - $this->getTransformations(true), + $this->getTransformations(), ); if ($this->isMonthView()) { // Group days together into months - $list = $this->timelineQuery->daysToMonths($list); + $list = $this->daysToMonths($list); } else { // Preload some day responses - $this->preloadDays($list, $uid); + $this->preloadDays($list); } // Reverse response if requested. Folders still stay at top. @@ -80,8 +77,6 @@ class DaysController extends GenericApiController public function day(string $id): Http\Response { return Util::guardEx(function () use ($id) { - $uid = $this->getShareToken() ? '' : Util::getUID(); - // Check for wildcard $dayIds = []; if ('*' === $id) { @@ -98,16 +93,15 @@ class DaysController extends GenericApiController // Convert to actual dayIds if month view if ($this->isMonthView()) { - $dayIds = $this->timelineQuery->monthIdToDayIds((int) $dayIds[0]); + $dayIds = $this->monthIdToDayIds((int) $dayIds[0]); } // Run actual query $list = $this->timelineQuery->getDay( - $uid, $dayIds, $this->isRecursive(), $this->isArchive(), - $this->getTransformations(false), + $this->getTransformations(), ); // Force month id for dayId for month view @@ -145,20 +139,16 @@ class DaysController extends GenericApiController /** * Get transformations depending on the request. - * - * @param bool $aggregateOnly Only apply transformations for aggregation (days call) */ - private function getTransformations(bool $aggregateOnly) + private function getTransformations() { $transforms = []; - // Filter for one album - if (($albumId = $this->request->getParam('album')) && Util::albumsIsEnabled()) { - $transforms[] = [$this->timelineQuery, 'transformAlbumFilter', $albumId]; - } + // Add clustering transforms + $transforms = array_merge($transforms, \OCA\Memories\ClustersBackend\Backend::getTransforms($this->request)); // Other transforms not allowed for public shares - if (null === $this->userSession->getUser()) { + if (!Util::isLoggedIn()) { return $transforms; } @@ -172,47 +162,14 @@ class DaysController extends GenericApiController $transforms[] = [$this->timelineQuery, 'transformVideoFilter']; } - // Filter only for one face on Recognize - if (($recognize = $this->request->getParam('recognize')) && Util::recognizeIsEnabled()) { - $transforms[] = [$this->timelineQuery, 'transformPeopleRecognitionFilter', $recognize, $aggregateOnly]; - - $faceRect = $this->request->getParam('facerect'); - if ($faceRect && !$aggregateOnly) { - $transforms[] = [$this->timelineQuery, 'transformPeopleRecognizeRect', $recognize]; - } - } - - // Filter only for one face on Face Recognition - if (($face = $this->request->getParam('facerecognition')) && Util::facerecognitionIsEnabled()) { - $currentModel = (int) $this->config->getAppValue('facerecognition', 'model', -1); - $transforms[] = [$this->timelineQuery, 'transformPeopleFaceRecognitionFilter', $currentModel, $face]; - - $faceRect = $this->request->getParam('facerect'); - if ($faceRect && !$aggregateOnly) { - $transforms[] = [$this->timelineQuery, 'transformPeopleFaceRecognitionRect', $face]; - } - } - - // Filter only for one tag - if (($tagName = $this->request->getParam('tag')) && Util::tagsIsEnabled()) { - $transforms[] = [$this->timelineQuery, 'transformTagFilter', $tagName]; - } - - // Filter only for one place - if (($locationId = $this->request->getParam('place')) && Util::placesGISType() > 0) { - $transforms[] = [$this->timelineQuery, 'transformPlaceFilter', (int) $locationId]; - } - // Filter geological bounds - $bounds = $this->request->getParam('mapbounds'); - if ($bounds) { + if ($bounds = $this->request->getParam('mapbounds')) { $transforms[] = [$this->timelineQuery, 'transformMapBoundsFilter', $bounds]; } // Limit number of responses for day query - $limit = $this->request->getParam('limit'); - if ($limit) { - $transforms[] = [$this->timelineQuery, 'transformLimitDay', (int) $limit]; + if ($limit = $this->request->getParam('limit')) { + $transforms[] = [$this->timelineQuery, 'transformLimit', (int) $limit]; } return $transforms; @@ -221,10 +178,9 @@ class DaysController extends GenericApiController /** * Preload a few "day" at the start of "days" response. * - * @param array $days the days array - * @param string $uid User ID or blank for public shares + * @param array $days the days array */ - private function preloadDays(array &$days, string $uid) + private function preloadDays(array &$days) { $transforms = $this->getTransformations(false); $preloaded = 0; @@ -246,7 +202,6 @@ class DaysController extends GenericApiController if (\count($preloadDayIds) > 0) { $allDetails = $this->timelineQuery->getDay( - $uid, $preloadDayIds, $this->isRecursive(), $this->isArchive(), @@ -266,4 +221,42 @@ class DaysController extends GenericApiController } } } + + /** + * Convert days response to months response. + * The dayId is used to group the days into months. + */ + private function daysToMonths(array $days) + { + $months = []; + foreach ($days as $day) { + $dayId = $day['dayid']; + $time = $dayId * 86400; + $monthid = strtotime(date('Ym', $time).'01') / 86400; + + if (empty($months) || $months[\count($months) - 1]['dayid'] !== $monthid) { + $months[] = [ + 'dayid' => $monthid, + 'count' => 0, + ]; + } + + $months[\count($months) - 1]['count'] += $day['count']; + } + + return $months; + } + + /** Convert list of month IDs to list of dayIds */ + private function monthIdToDayIds(int $monthId) + { + $dayIds = []; + $firstDay = (int) $monthId; + $lastDay = strtotime(date('Ymt', $firstDay * 86400)) / 86400; + for ($i = $firstDay; $i <= $lastDay; ++$i) { + $dayIds[] = (string) $i; + } + + return $dayIds; + } } diff --git a/lib/Controller/GenericApiControllerParams.php b/lib/Controller/GenericApiControllerParams.php index 56192b47..b7b70cd1 100644 --- a/lib/Controller/GenericApiControllerParams.php +++ b/lib/Controller/GenericApiControllerParams.php @@ -51,9 +51,4 @@ trait GenericApiControllerParams { return null !== $this->request->getParam('reverse'); } - - protected function getShareToken() - { - return $this->request->getParam('token'); - } } diff --git a/lib/Controller/PublicAlbumController.php b/lib/Controller/PublicAlbumController.php index ae819a27..c4b31970 100644 --- a/lib/Controller/PublicAlbumController.php +++ b/lib/Controller/PublicAlbumController.php @@ -2,7 +2,7 @@ namespace OCA\Memories\Controller; -use OCA\Memories\Db\TimelineQuery; +use OCA\Memories\Db\AlbumsQuery; use OCP\App\IAppManager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\RedirectResponse; @@ -26,7 +26,7 @@ class PublicAlbumController extends Controller protected IUserSession $userSession; protected IRootFolder $rootFolder; protected IURLGenerator $urlGenerator; - protected TimelineQuery $timelineQuery; + protected AlbumsQuery $albumsQuery; public function __construct( string $appName, @@ -37,7 +37,7 @@ class PublicAlbumController extends Controller IUserSession $userSession, IRootFolder $rootFolder, IURLGenerator $urlGenerator, - TimelineQuery $timelineQuery + AlbumsQuery $albumsQuery ) { $this->appName = $appName; $this->eventDispatcher = $eventDispatcher; @@ -47,7 +47,7 @@ class PublicAlbumController extends Controller $this->userSession = $userSession; $this->rootFolder = $rootFolder; $this->urlGenerator = $urlGenerator; - $this->timelineQuery = $timelineQuery; + $this->albumsQuery = $albumsQuery; } /** @@ -58,7 +58,7 @@ class PublicAlbumController extends Controller public function showShare(string $token) { // Validate token exists - $album = $this->timelineQuery->getAlbumByLink($token); + $album = $this->albumsQuery->getAlbumByLink($token); if (!$album) { return new TemplateResponse('core', '404', [], 'guest'); } @@ -69,7 +69,7 @@ class PublicAlbumController extends Controller $uid = $user->getUID(); $albumId = (int) $album['album_id']; - if ($uid === $album['user'] || $this->timelineQuery->userIsAlbumCollaborator($uid, $albumId)) { + if ($uid === $album['user'] || $this->albumsQuery->userIsCollaborator($uid, $albumId)) { $idStr = $album['user'].'/'.$album['name']; $url = $this->urlGenerator->linkToRoute('memories.Page.albums', ['id' => $idStr]); @@ -99,7 +99,7 @@ class PublicAlbumController extends Controller { $fileId = (int) $album['last_added_photo']; $albumId = (int) $album['album_id']; - $owner = $this->timelineQuery->albumHasFile($albumId, $fileId); + $owner = $this->albumsQuery->hasFile($albumId, $fileId); if (!$owner) { return; } diff --git a/lib/Db/TimelineQueryAlbums.php b/lib/Db/AlbumsQuery.php similarity index 79% rename from lib/Db/TimelineQueryAlbums.php rename to lib/Db/AlbumsQuery.php index 37c4171f..e9b5c29d 100644 --- a/lib/Db/TimelineQueryAlbums.php +++ b/lib/Db/AlbumsQuery.php @@ -7,30 +7,17 @@ namespace OCA\Memories\Db; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; -trait TimelineQueryAlbums +class AlbumsQuery { protected IDBConnection $connection; - /** Transform only for album */ - public function transformAlbumFilter(IQueryBuilder &$query, string $uid, string $albumId) + public function __construct(IDBConnection $connection) { - // Get album object - $album = $this->getAlbumIfAllowed($uid, $albumId); - - // Check permission - if (null === $album) { - throw new \Exception("Album {$albumId} not found"); - } - - // WHERE these are items with this album - $query->innerJoin('m', 'photos_albums_files', 'paf', $query->expr()->andX( - $query->expr()->eq('paf.album_id', $query->createNamedParameter($album['album_id'])), - $query->expr()->eq('paf.file_id', 'm.fileid'), - )); + $this->connection = $connection; } /** Get list of albums */ - public function getAlbums(string $uid, bool $shared = false) + public function getList(string $uid, bool $shared = false) { $query = $this->connection->getQueryBuilder(); @@ -79,50 +66,12 @@ trait TimelineQueryAlbums return $albums; } - /** - * Convert days response to months response. - * The dayId is used to group the days into months. - */ - public function daysToMonths(array &$days) - { - $months = []; - foreach ($days as &$day) { - $dayId = $day['dayid']; - $time = $dayId * 86400; - $monthid = strtotime(date('Ym', $time).'01') / 86400; - - if (empty($months) || $months[\count($months) - 1]['dayid'] !== $monthid) { - $months[] = [ - 'dayid' => $monthid, - 'count' => 0, - ]; - } - - $months[\count($months) - 1]['count'] += $day['count']; - } - - return $months; - } - - /** Convert list of month IDs to list of dayIds */ - public function monthIdToDayIds(int $monthId) - { - $dayIds = []; - $firstDay = (int) $monthId; - $lastDay = strtotime(date('Ymt', $firstDay * 86400)) / 86400; - for ($i = $firstDay; $i <= $lastDay; ++$i) { - $dayIds[] = (string) $i; - } - - return $dayIds; - } - /** * Check if an album has a file. * * @return bool|string owner of file */ - public function albumHasFile(int $albumId, int $fileId) + public function hasFile(int $albumId, int $fileId) { $query = $this->connection->getQueryBuilder(); $query->select('owner')->from('photos_albums_files')->where( @@ -140,7 +89,7 @@ trait TimelineQueryAlbums * * @return bool|string owner of file */ - public function albumHasUserFile(string $uid, int $fileId) + public function userHasFile(string $uid, int $fileId) { $query = $this->connection->getQueryBuilder(); $query->select('paf.owner')->from('photos_albums_files', 'paf')->where( @@ -175,7 +124,7 @@ trait TimelineQueryAlbums * @param string $uid UID of CURRENT user * @param string $albumId $user/$name where $user is the OWNER of the album */ - public function getAlbumIfAllowed(string $uid, string $albumId) + public function getIfAllowed(string $uid, string $albumId) { $album = null; @@ -208,7 +157,7 @@ trait TimelineQueryAlbums // Check in collaborators instead $albumNumId = (int) $album['album_id']; - if ($this->userIsAlbumCollaborator($uid, $albumNumId)) { + if ($this->userIsCollaborator($uid, $albumNumId)) { return $album; } @@ -223,7 +172,7 @@ trait TimelineQueryAlbums * @param string $uid User ID * @param int $albumId Album ID (numeric) */ - public function userIsAlbumCollaborator(string $uid, int $albumId): bool + public function userIsCollaborator(string $uid, int $albumId): bool { $query = $this->connection->getQueryBuilder(); $ids = $this->getSelfCollaborators($uid); diff --git a/lib/Db/TimelineQuery.php b/lib/Db/TimelineQuery.php index 00dc7ebe..2e80e71f 100644 --- a/lib/Db/TimelineQuery.php +++ b/lib/Db/TimelineQuery.php @@ -6,20 +6,16 @@ namespace OCA\Memories\Db; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; +use OCP\IRequest; class TimelineQuery { - use TimelineQueryAlbums; use TimelineQueryDays; use TimelineQueryFilters; use TimelineQueryFolders; use TimelineQueryLivePhoto; use TimelineQueryMap; - use TimelineQueryPeopleFaceRecognition; - use TimelineQueryPeopleRecognize; - use TimelineQueryPlaces; use TimelineQuerySingleItem; - use TimelineQueryTags; public const TIMELINE_SELECT = [ 'm.isvideo', 'm.video_duration', 'm.datetaken', 'm.dayid', 'm.w', 'm.h', 'm.liveid', @@ -27,11 +23,13 @@ class TimelineQuery ]; protected IDBConnection $connection; + protected IRequest $request; private ?TimelineRoot $_root = null; - public function __construct(IDBConnection $connection) + public function __construct(IDBConnection $connection, IRequest $request) { $this->connection = $connection; + $this->request = $request; } public function root(): TimelineRoot @@ -45,6 +43,11 @@ class TimelineQuery return $this->_root; } + public function getBuilder() + { + return $this->connection->getQueryBuilder(); + } + public static function debugQuery(IQueryBuilder &$query, string $sql = '') { // Print the query and exit @@ -74,63 +77,4 @@ class TimelineQuery return $sql; } - - public function getInfoById(int $id, bool $basic): array - { - $qb = $this->connection->getQueryBuilder(); - $qb->select('fileid', 'dayid', 'datetaken', 'w', 'h') - ->from('memories') - ->where($qb->expr()->eq('fileid', $qb->createNamedParameter($id, \PDO::PARAM_INT))) - ; - - if (!$basic) { - $qb->addSelect('exif'); - } - - $result = $qb->executeQuery(); - $row = $result->fetch(); - $result->closeCursor(); - - $utcTs = 0; - - try { - $utcDate = new \DateTime($row['datetaken'], new \DateTimeZone('UTC')); - $utcTs = $utcDate->getTimestamp(); - } catch (\Throwable $e) { - } - - $exif = []; - if (!$basic && !empty($row['exif'])) { - try { - $exif = json_decode($row['exif'], true); - } catch (\Throwable $e) { - } - } - - $gisType = \OCA\Memories\Util::placesGISType(); - $address = -1 === $gisType ? 'Geocoding Unconfigured' : null; - if (!$basic && $gisType > 0) { - $qb = $this->connection->getQueryBuilder(); - $qb->select('e.name') - ->from('memories_places', 'mp') - ->innerJoin('mp', 'memories_planet', 'e', $qb->expr()->eq('mp.osm_id', 'e.osm_id')) - ->where($qb->expr()->eq('mp.fileid', $qb->createNamedParameter($id, \PDO::PARAM_INT))) - ->orderBy('e.admin_level', 'DESC') - ; - $places = $qb->executeQuery()->fetchAll(\PDO::FETCH_COLUMN); - if (\count($places) > 0) { - $address = implode(', ', $places); - } - } - - return [ - 'fileid' => (int) $row['fileid'], - 'dayid' => (int) $row['dayid'], - 'w' => (int) $row['w'], - 'h' => (int) $row['h'], - 'datetaken' => $utcTs, - 'address' => $address, - 'exif' => $exif, - ]; - } } diff --git a/lib/Db/TimelineQueryDays.php b/lib/Db/TimelineQueryDays.php index 20484539..a97d592d 100644 --- a/lib/Db/TimelineQueryDays.php +++ b/lib/Db/TimelineQueryDays.php @@ -85,7 +85,6 @@ trait TimelineQueryDays * @return array The days response */ public function getDays( - string $uid, bool $recursive, bool $archive, array $queryTransforms = [] @@ -105,7 +104,7 @@ trait TimelineQueryDays ; // Apply all transformations - $this->applyAllTransforms($queryTransforms, $query, $uid); + $this->applyAllTransforms($queryTransforms, $query, true); $cursor = $this->executeQueryWithCTEs($query); $rows = $cursor->fetchAll(); @@ -127,7 +126,6 @@ trait TimelineQueryDays * @return array An array of day responses */ public function getDay( - string $uid, ?array $day_ids, bool $recursive, bool $archive, @@ -160,14 +158,14 @@ trait TimelineQueryDays } // Add favorite field - $this->addFavoriteTag($query, $uid); + $this->addFavoriteTag($query); // Group and sort by date taken $query->orderBy('m.datetaken', 'DESC'); $query->addOrderBy('m.fileid', 'DESC'); // tie-breaker // Apply all transformations - $this->applyAllTransforms($queryTransforms, $query, $uid); + $this->applyAllTransforms($queryTransforms, $query, false); $cursor = $this->executeQueryWithCTEs($query); $rows = $cursor->fetchAll(); @@ -176,6 +174,66 @@ trait TimelineQueryDays return $this->processDay($rows); } + public function executeQueryWithCTEs(IQueryBuilder $query, string $psql = '') + { + $sql = empty($psql) ? $query->getSQL() : $psql; + $params = $query->getParameters(); + $types = $query->getParameterTypes(); + + // Get SQL + $CTE_SQL = \array_key_exists('cteFoldersArchive', $params) && $params['cteFoldersArchive'] + ? CTE_FOLDERS_ARCHIVE + : CTE_FOLDERS; + + // Add WITH clause if needed + if (false !== strpos($sql, 'cte_folders')) { + $sql = $CTE_SQL.' '.$sql; + } + + return $this->connection->executeQuery($sql, $params, $types); + } + + /** + * Inner join with oc_filecache. + * + * @param IQueryBuilder $query Query builder + * @param TimelineRoot $root Either the top folder or null for all + * @param bool $recursive Whether to get the days recursively + * @param bool $archive Whether to get the days only from the archive folder + */ + public function joinFilecache( + IQueryBuilder $query, + ?TimelineRoot $root = null, + bool $recursive = true, + bool $archive = false + ) { + if (null === $root) { + $root = $this->root(); + } + + // Join with memories + $baseOp = $query->expr()->eq('f.fileid', 'm.fileid'); + if ($root->isEmpty()) { + return $query->innerJoin('m', 'filecache', 'f', $baseOp); + } + + // Filter by folder (recursive or otherwise) + $pathOp = null; + if ($recursive) { + // Join with folders CTE + $this->addSubfolderJoinParams($query, $root, $archive); + $query->innerJoin('f', 'cte_folders', 'cte_f', $query->expr()->eq('f.parent', 'cte_f.fileid')); + } else { + // If getting non-recursively folder only check for parent + $pathOp = $query->expr()->eq('f.parent', $query->createNamedParameter($root->getOneId(), IQueryBuilder::PARAM_INT)); + } + + return $query->innerJoin('m', 'filecache', 'f', $query->expr()->andX( + $baseOp, + $pathOp, + )); + } + /** * Process the days response. * @@ -215,9 +273,8 @@ trait TimelineQueryDays unset($row['liveid']); } - // All transform processing - $this->processPeopleRecognizeDetection($row); - $this->processFaceRecognitionDetection($row); + // All cluster transformations + \OCA\Memories\ClustersBackend\Backend::applyDayPostTransforms($this->request, $row); // We don't need these fields unset($row['datetaken']); @@ -226,25 +283,6 @@ trait TimelineQueryDays return $day; } - private function executeQueryWithCTEs(IQueryBuilder &$query, string $psql = '') - { - $sql = empty($psql) ? $query->getSQL() : $psql; - $params = $query->getParameters(); - $types = $query->getParameterTypes(); - - // Get SQL - $CTE_SQL = \array_key_exists('cteFoldersArchive', $params) && $params['cteFoldersArchive'] - ? CTE_FOLDERS_ARCHIVE - : CTE_FOLDERS; - - // Add WITH clause if needed - if (false !== strpos($sql, 'cte_folders')) { - $sql = $CTE_SQL.' '.$sql; - } - - return $this->connection->executeQuery($sql, $params, $types); - } - /** * Get all folders inside a top folder. */ @@ -257,45 +295,4 @@ trait TimelineQueryDays $query->setParameter('topFolderIds', $root->getIds(), IQueryBuilder::PARAM_INT_ARRAY); $query->setParameter('cteFoldersArchive', $archive, IQueryBuilder::PARAM_BOOL); } - - /** - * Inner join with oc_filecache. - * - * @param IQueryBuilder $query Query builder - * @param TimelineRoot $root Either the top folder or null for all - * @param bool $recursive Whether to get the days recursively - * @param bool $archive Whether to get the days only from the archive folder - */ - private function joinFilecache( - IQueryBuilder $query, - ?TimelineRoot $root = null, - bool $recursive = true, - bool $archive = false - ) { - if (null === $root) { - $root = $this->root(); - } - - // Join with memories - $baseOp = $query->expr()->eq('f.fileid', 'm.fileid'); - if ($root->isEmpty()) { - return $query->innerJoin('m', 'filecache', 'f', $baseOp); - } - - // Filter by folder (recursive or otherwise) - $pathOp = null; - if ($recursive) { - // Join with folders CTE - $this->addSubfolderJoinParams($query, $root, $archive); - $query->innerJoin('f', 'cte_folders', 'cte_f', $query->expr()->eq('f.parent', 'cte_f.fileid')); - } else { - // If getting non-recursively folder only check for parent - $pathOp = $query->expr()->eq('f.parent', $query->createNamedParameter($root->getOneId(), IQueryBuilder::PARAM_INT)); - } - - return $query->innerJoin('m', 'filecache', 'f', $query->expr()->andX( - $baseOp, - $pathOp, - )); - } } diff --git a/lib/Db/TimelineQueryFilters.php b/lib/Db/TimelineQueryFilters.php index bd57180c..0dd60f83 100644 --- a/lib/Db/TimelineQueryFilters.php +++ b/lib/Db/TimelineQueryFilters.php @@ -4,60 +4,63 @@ declare(strict_types=1); namespace OCA\Memories\Db; +use OCA\Memories\Util; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\ITags; trait TimelineQueryFilters { - public function transformFavoriteFilter(IQueryBuilder &$query, string $userId) + public function transformFavoriteFilter(IQueryBuilder &$query, bool $aggregate) { - $query->innerJoin('m', 'vcategory_to_object', 'vcoi', $query->expr()->andX( - $query->expr()->eq('vcoi.objid', 'm.fileid'), - $query->expr()->in('vcoi.categoryid', $this->getFavoriteVCategoryFun($query, $userId)), - )); + if (Util::isLoggedIn()) { + $query->innerJoin('m', 'vcategory_to_object', 'vcoi', $query->expr()->andX( + $query->expr()->eq('vcoi.objid', 'm.fileid'), + $query->expr()->in('vcoi.categoryid', $this->getFavoriteVCategoryFun($query)), + )); + } } - public function addFavoriteTag(IQueryBuilder &$query, string $userId) + public function addFavoriteTag(IQueryBuilder &$query) { - $query->leftJoin('m', 'vcategory_to_object', 'vco', $query->expr()->andX( - $query->expr()->eq('vco.objid', 'm.fileid'), - $query->expr()->in('vco.categoryid', $this->getFavoriteVCategoryFun($query, $userId)), - )); - $query->addSelect('vco.categoryid'); + if (Util::isLoggedIn()) { + $query->leftJoin('m', 'vcategory_to_object', 'vco', $query->expr()->andX( + $query->expr()->eq('vco.objid', 'm.fileid'), + $query->expr()->in('vco.categoryid', $this->getFavoriteVCategoryFun($query)), + )); + $query->addSelect('vco.categoryid'); + } } - public function transformVideoFilter(IQueryBuilder &$query, string $userId) + public function transformVideoFilter(IQueryBuilder &$query, bool $aggregate) { $query->andWhere($query->expr()->eq('m.isvideo', $query->createNamedParameter('1'))); } - public function transformLimitDay(IQueryBuilder &$query, string $userId, int $limit) + public function transformLimit(IQueryBuilder &$query, bool $aggregate, int $limit) { - // The valid range for limit is 1 - 100; otherwise abort - if ($limit < 1 || $limit > 100) { - return; + if ($limit >= 1 || $limit <= 100) { + $query->setMaxResults($limit); } - $query->setMaxResults($limit); } - private function applyAllTransforms(array $transforms, IQueryBuilder &$query, string $uid): void + private function applyAllTransforms(array $transforms, IQueryBuilder &$query, bool $aggregate): void { foreach ($transforms as &$transform) { $fun = \array_slice($transform, 0, 2); $params = \array_slice($transform, 2); - array_unshift($params, $uid); + array_unshift($params, $aggregate); array_unshift($params, $query); $fun(...$params); } } - private function getFavoriteVCategoryFun(IQueryBuilder &$query, string $userId) + private function getFavoriteVCategoryFun(IQueryBuilder &$query) { return $query->createFunction( $query->getConnection()->getQueryBuilder()->select('id')->from('vcategory', 'vc')->where( $query->expr()->andX( $query->expr()->eq('type', $query->createNamedParameter('files')), - $query->expr()->eq('uid', $query->createNamedParameter($userId)), + $query->expr()->eq('uid', $query->createNamedParameter(Util::getUID())), $query->expr()->eq('category', $query->createNamedParameter(ITags::TAG_FAVORITE)), ) )->getSQL() diff --git a/lib/Db/TimelineQueryFolders.php b/lib/Db/TimelineQueryFolders.php index 347e7e64..c5a5bed8 100644 --- a/lib/Db/TimelineQueryFolders.php +++ b/lib/Db/TimelineQueryFolders.php @@ -30,14 +30,13 @@ trait TimelineQueryFolders $query->setMaxResults(4); // FETCH tag previews - $cursor = $this->executeQueryWithCTEs($query); - $ans = $cursor->fetchAll(); + $rows = $this->executeQueryWithCTEs($query)->fetchAll(); // Post-process - foreach ($ans as &$row) { + foreach ($rows as &$row) { $row['fileid'] = (int) $row['fileid']; } - return $ans; + return $rows; } } diff --git a/lib/Db/TimelineQueryMap.php b/lib/Db/TimelineQueryMap.php index a09b22fb..e23fde17 100644 --- a/lib/Db/TimelineQueryMap.php +++ b/lib/Db/TimelineQueryMap.php @@ -11,7 +11,7 @@ trait TimelineQueryMap { protected IDBConnection $connection; - public function transformMapBoundsFilter(IQueryBuilder &$query, string $userId, string $bounds, $table = 'm') + public function transformMapBoundsFilter(IQueryBuilder &$query, bool $aggregate, string $bounds, string $table = 'm') { $bounds = explode(',', $bounds); $bounds = array_map('floatval', $bounds); @@ -59,7 +59,7 @@ trait TimelineQueryMap $query = $this->joinFilecache($query); // Bound the query to the map bounds - $this->transformMapBoundsFilter($query, '', $bounds, 'c'); + $this->transformMapBoundsFilter($query, false, $bounds, 'c'); // Execute query $cursor = $this->executeQueryWithCTEs($query); diff --git a/lib/Db/TimelineQueryPeopleFaceRecognition.php b/lib/Db/TimelineQueryPeopleFaceRecognition.php deleted file mode 100644 index 1d5ce773..00000000 --- a/lib/Db/TimelineQueryPeopleFaceRecognition.php +++ /dev/null @@ -1,242 +0,0 @@ -innerJoin('m', 'facerecog_images', 'fri', $query->expr()->andX( - $query->expr()->eq('fri.file', 'm.fileid'), - $query->expr()->eq('fri.model', $query->createNamedParameter($currentModel)), - )); - - // Join with faces - $query->innerJoin( - 'fri', - 'facerecog_faces', - 'frf', - $query->expr()->eq('frf.image', 'fri.id') - ); - - // Join with persons - $nameField = is_numeric($personName) ? 'frp.id' : 'frp.name'; - $query->innerJoin('frf', 'facerecog_persons', 'frp', $query->expr()->andX( - $query->expr()->eq('frf.person', 'frp.id'), - $query->expr()->eq('frp.user', $query->createNamedParameter($personUid)), - $query->expr()->eq($nameField, $query->createNamedParameter($personName)), - )); - } - - public function transformPeopleFaceRecognitionRect(IQueryBuilder &$query, string $userId) - { - // Include detection params in response - $query->addSelect( - 'frf.x AS face_x', - 'frf.y AS face_y', - 'frf.width AS face_width', - 'frf.height AS face_height', - 'm.w AS image_width', - 'm.h AS image_height', - ); - } - - public function getFaceRecognitionPhotos(string $id, int $currentModel, ?int $limit) - { - $query = $this->connection->getQueryBuilder(); - - // SELECT face detections - $query->select( - 'fri.file as file_id', // Get actual file - 'frf.x', // Image cropping - 'frf.y', - 'frf.width', - 'frf.height', - 'm.w as image_width', // Scoring - 'm.h as image_height', - 'frf.confidence', - 'm.fileid', - 'm.datetaken', // Just in case, for postgres - )->from('facerecog_faces', 'frf'); - - // WHERE faces are from images and current model. - $query->innerJoin('frf', 'facerecog_images', 'fri', $query->expr()->andX( - $query->expr()->eq('fri.id', 'frf.image'), - $query->expr()->eq('fri.model', $query->createNamedParameter($currentModel)), - )); - - // WHERE these photos are memories indexed - $query->innerJoin('fri', 'memories', 'm', $query->expr()->eq('m.fileid', 'fri.file')); - - $query->innerJoin('frf', 'facerecog_persons', 'frp', $query->expr()->eq('frp.id', 'frf.person')); - if (is_numeric($id)) { - // WHERE faces are from id persons (a cluster). - $query->where($query->expr()->eq('frp.id', $query->createNamedParameter($id))); - } else { - // WHERE faces are from name on persons. - $query->where($query->expr()->eq('frp.name', $query->createNamedParameter($id))); - } - - // WHERE these photos are in the user's requested folder recursively - $query = $this->joinFilecache($query); - - // LIMIT results - if (null !== $limit) { - $query->setMaxResults($limit); - } - - // Sort by date taken so we get recent photos - $query->orderBy('m.datetaken', 'DESC'); - $query->addOrderBy('m.fileid', 'DESC'); // tie-breaker - - // FETCH face detections - return $this->executeQueryWithCTEs($query)->fetchAll(); - } - - public function getFaceRecognitionClusters(int $currentModel, bool $show_singles = false, bool $show_hidden = false) - { - $query = $this->connection->getQueryBuilder(); - - // SELECT all face clusters - $count = $query->func()->count($query->createFunction('DISTINCT m.fileid')); - $query->select('frp.id')->from('facerecog_persons', 'frp'); - $query->selectAlias($count, 'count'); - $query->selectAlias('frp.user', 'user_id'); - - // WHERE there are faces with this cluster - $query->innerJoin('frp', 'facerecog_faces', 'frf', $query->expr()->eq('frp.id', 'frf.person')); - - // WHERE faces are from images. - $query->innerJoin('frf', 'facerecog_images', 'fri', $query->expr()->eq('fri.id', 'frf.image')); - - // WHERE these items are memories indexed photos - $query->innerJoin('fri', 'memories', 'm', $query->expr()->andX( - $query->expr()->eq('fri.file', 'm.fileid'), - $query->expr()->eq('fri.model', $query->createNamedParameter($currentModel)), - )); - - // WHERE these photos are in the user's requested folder recursively - $query = $this->joinFilecache($query); - - // GROUP by ID of face cluster - $query->groupBy('frp.id'); - $query->addGroupBy('frp.user'); - $query->where($query->expr()->isNull('frp.name')); - - // By default hides individual faces when they have no name. - if (!$show_singles) { - $query->having($query->expr()->gt($count, $query->createNamedParameter(1))); - } - - // By default it shows the people who were not hidden - if (!$show_hidden) { - $query->andWhere($query->expr()->eq('frp.is_visible', $query->createNamedParameter(true))); - } - - // ORDER by number of faces in cluster and id for response stability. - $query->orderBy('count', 'DESC'); - $query->addOrderBy('frp.id', 'DESC'); - - // It is not worth displaying all unnamed clusters. We show 15 to name them progressively, - $query->setMaxResults(15); - - // FETCH all faces - $cursor = $this->executeQueryWithCTEs($query); - $faces = $cursor->fetchAll(); - - // Post process - foreach ($faces as &$row) { - $row['id'] = (int) $row['id']; - $row['count'] = (int) $row['count']; - } - - return $faces; - } - - public function getFaceRecognitionPersons(int $currentModel) - { - $query = $this->connection->getQueryBuilder(); - - // SELECT all face clusters - $count = $query->func()->count($query->createFunction('DISTINCT m.fileid')); - $query->select('frp.name')->from('facerecog_persons', 'frp'); - $query->selectAlias($count, 'count'); - $query->selectAlias('frp.user', 'user_id'); - - // WHERE there are faces with this cluster - $query->innerJoin('frp', 'facerecog_faces', 'frf', $query->expr()->eq('frp.id', 'frf.person')); - - // WHERE faces are from images. - $query->innerJoin('frf', 'facerecog_images', 'fri', $query->expr()->eq('fri.id', 'frf.image')); - - // WHERE these items are memories indexed photos - $query->innerJoin('fri', 'memories', 'm', $query->expr()->andX( - $query->expr()->eq('fri.file', 'm.fileid'), - $query->expr()->eq('fri.model', $query->createNamedParameter($currentModel)), - )); - - // WHERE these photos are in the user's requested folder recursively - $query = $this->joinFilecache($query); - - // GROUP by name of face clusters - $query->where($query->expr()->isNotNull('frp.name')); - $query->groupBy('frp.user'); - $query->addGroupBy('frp.name'); - - // ORDER by number of faces in cluster - $query->orderBy('count', 'DESC'); - $query->addOrderBy('frp.name', 'ASC'); - - // FETCH all faces - $cursor = $this->executeQueryWithCTEs($query); - $faces = $cursor->fetchAll(); - - // Post process - foreach ($faces as &$row) { - $row['id'] = $row['name']; - $row['count'] = (int) $row['count']; - } - - return $faces; - } - - /** Convert face fields to object */ - private function processFaceRecognitionDetection(&$row) - { - if (!isset($row)) { - return; - } - - // Differentiate Recognize queries from Face Recognition - if (!isset($row['face_width']) || !isset($row['image_width'])) { - return; - } - - // Get percentage position and size - $row['facerect'] = [ - 'w' => (float) $row['face_width'] / $row['image_width'], - 'h' => (float) $row['face_height'] / $row['image_height'], - 'x' => (float) $row['face_x'] / $row['image_width'], - 'y' => (float) $row['face_y'] / $row['image_height'], - ]; - - unset($row['face_x'], $row['face_y'], $row['face_w'], $row['face_h'], $row['image_height'], $row['image_width']); - } -} diff --git a/lib/Db/TimelineQueryPeopleRecognize.php b/lib/Db/TimelineQueryPeopleRecognize.php deleted file mode 100644 index 994d5d98..00000000 --- a/lib/Db/TimelineQueryPeopleRecognize.php +++ /dev/null @@ -1,165 +0,0 @@ -addSelect('rfd.id AS faceid'); - } - - // Join with cluster - $clusterQuery = null; - if ('NULL' === $faceName) { - $clusterQuery = $query->expr()->isNull('rfd.cluster_id'); - } else { - $nameField = is_numeric($faceName) ? 'rfc.id' : 'rfc.title'; - $query->innerJoin('m', 'recognize_face_clusters', 'rfc', $query->expr()->andX( - $query->expr()->eq('rfc.user_id', $query->createNamedParameter($userId)), - $query->expr()->eq($nameField, $query->createNamedParameter($faceName)), - )); - $clusterQuery = $query->expr()->eq('rfd.cluster_id', 'rfc.id'); - } - - // Join with detections - $query->innerJoin('m', 'recognize_face_detections', 'rfd', $query->expr()->andX( - $query->expr()->eq('rfd.file_id', 'm.fileid'), - $clusterQuery, - )); - } - - public function transformPeopleRecognizeRect(IQueryBuilder &$query, string $userId) - { - // Include detection params in response - $query->addSelect( - 'rfd.width AS face_w', - 'rfd.height AS face_h', - 'rfd.x AS face_x', - 'rfd.y AS face_y', - ); - } - - public function getPeopleRecognize(string $uid) - { - $query = $this->connection->getQueryBuilder(); - - // SELECT all face clusters - $count = $query->func()->count($query->createFunction('DISTINCT m.fileid'), 'count'); - $query->select('rfc.id', 'rfc.user_id', 'rfc.title', $count)->from('recognize_face_clusters', 'rfc'); - - // WHERE there are faces with this cluster - $query->innerJoin('rfc', 'recognize_face_detections', 'rfd', $query->expr()->eq('rfc.id', 'rfd.cluster_id')); - - // WHERE these items are memories indexed photos - $query->innerJoin('rfd', 'memories', 'm', $query->expr()->eq('m.fileid', 'rfd.file_id')); - - // WHERE these photos are in the user's requested folder recursively - $query = $this->joinFilecache($query); - - // WHERE this cluster belongs to the user - $query->where($query->expr()->eq('rfc.user_id', $query->createNamedParameter($uid))); - - // GROUP by ID of face cluster - $query->groupBy('rfc.id'); - - // ORDER by number of faces in cluster - $query->orderBy($query->createFunction("rfc.title <> ''"), 'DESC'); - $query->addOrderBy('count', 'DESC'); - $query->addOrderBy('rfc.id'); // tie-breaker - - // FETCH all faces - $cursor = $this->executeQueryWithCTEs($query); - $faces = $cursor->fetchAll(); - - // Post process - foreach ($faces as &$row) { - $row['id'] = (int) $row['id']; - $row['count'] = (int) $row['count']; - $row['name'] = $row['title']; - unset($row['title']); - } - - return $faces; - } - - public function getPeopleRecognizePhotos(int $id, ?int $limit): array - { - $query = $this->connection->getQueryBuilder(); - - // SELECT face detections for ID - $query->select( - 'rfd.file_id', // Get actual file - 'rfd.x', // Image cropping - 'rfd.y', - 'rfd.width', - 'rfd.height', - 'm.w as image_width', // Scoring - 'm.h as image_height', - 'm.fileid', - 'm.datetaken', // Just in case, for postgres - )->from('recognize_face_detections', 'rfd'); - - // WHERE detection belongs to this cluster - $query->where($query->expr()->eq('rfd.cluster_id', $query->createNamedParameter($id))); - - // WHERE these photos are memories indexed - $query->innerJoin('rfd', 'memories', 'm', $query->expr()->eq('m.fileid', 'rfd.file_id')); - - // WHERE these photos are in the user's requested folder recursively - $query = $this->joinFilecache($query); - - // LIMIT results - if (null !== $limit) { - $query->setMaxResults($limit); - } - - // Sort by date taken so we get recent photos - $query->orderBy('m.datetaken', 'DESC'); - $query->addOrderBy('m.fileid', 'DESC'); // tie-breaker - - // FETCH face detections - return $this->executeQueryWithCTEs($query)->fetchAll(); - } - - /** Convert face fields to object */ - private function processPeopleRecognizeDetection(&$row) - { - // Differentiate Recognize queries from Face Recognition - if (!isset($row) || !isset($row['face_w'])) { - return; - } - - // Convert face rect to object - $row['facerect'] = [ - 'w' => (float) $row['face_w'], - 'h' => (float) $row['face_h'], - 'x' => (float) $row['face_x'], - 'y' => (float) $row['face_y'], - ]; - - unset($row['face_w'], $row['face_h'], $row['face_x'], $row['face_y']); - } -} diff --git a/lib/Db/TimelineQueryPlaces.php b/lib/Db/TimelineQueryPlaces.php deleted file mode 100644 index 6c6d9873..00000000 --- a/lib/Db/TimelineQueryPlaces.php +++ /dev/null @@ -1,80 +0,0 @@ -innerJoin('m', 'memories_places', 'mp', $query->expr()->andX( - $query->expr()->eq('mp.fileid', 'm.fileid'), - $query->expr()->eq('mp.osm_id', $query->createNamedParameter($locationId)), - )); - } - - public function getPlaces() - { - $query = $this->connection->getQueryBuilder(); - - // SELECT location name and count of photos - $count = $query->func()->count($query->createFunction('DISTINCT m.fileid'), 'count'); - $query->select('e.osm_id', 'e.name', $count)->from('memories_planet', 'e'); - - // WHERE there are items with this osm_id - $query->innerJoin('e', 'memories_places', 'mp', $query->expr()->eq('mp.osm_id', 'e.osm_id')); - - // WHERE these items are memories indexed photos - $query->innerJoin('mp', 'memories', 'm', $query->expr()->eq('m.fileid', 'mp.fileid')); - - // WHERE these photos are in the user's requested folder recursively - $query = $this->joinFilecache($query); - - // GROUP and ORDER by tag name - $query->groupBy('e.osm_id', 'e.name'); - $query->orderBy($query->createFunction('LOWER(e.name)'), 'ASC'); - $query->addOrderBy('e.osm_id'); // tie-breaker - - // FETCH all tags - $cursor = $this->executeQueryWithCTEs($query); - $places = $cursor->fetchAll(); - - // Post process - foreach ($places as &$row) { - $row['osm_id'] = (int) $row['osm_id']; - $row['count'] = (int) $row['count']; - } - - return $places; - } - - public function getPlacePhotos(int $id, ?int $limit): array - { - $query = $this->connection->getQueryBuilder(); - - // SELECT all photos with this tag - $query->select('f.fileid', 'f.etag')->from('memories_places', 'mp') - ->where($query->expr()->eq('mp.osm_id', $query->createNamedParameter($id))) - ; - - // WHERE these items are memories indexed photos - $query->innerJoin('mp', 'memories', 'm', $query->expr()->eq('m.fileid', 'mp.fileid')); - - // WHERE these photos are in the user's requested folder recursively - $query = $this->joinFilecache($query); - - // MAX number of photos - if (null !== $limit) { - $query->setMaxResults($limit); - } - - // FETCH tag photos - return $this->executeQueryWithCTEs($query)->fetchAll(); - } -} diff --git a/lib/Db/TimelineQuerySingleItem.php b/lib/Db/TimelineQuerySingleItem.php index 6afdee97..46da1a68 100644 --- a/lib/Db/TimelineQuerySingleItem.php +++ b/lib/Db/TimelineQuerySingleItem.php @@ -27,4 +27,63 @@ trait TimelineQuerySingleItem return $query->executeQuery()->fetch(); } + + public function getInfoById(int $id, bool $basic): array + { + $qb = $this->connection->getQueryBuilder(); + $qb->select('fileid', 'dayid', 'datetaken', 'w', 'h') + ->from('memories') + ->where($qb->expr()->eq('fileid', $qb->createNamedParameter($id, \PDO::PARAM_INT))) + ; + + if (!$basic) { + $qb->addSelect('exif'); + } + + $result = $qb->executeQuery(); + $row = $result->fetch(); + $result->closeCursor(); + + $utcTs = 0; + + try { + $utcDate = new \DateTime($row['datetaken'], new \DateTimeZone('UTC')); + $utcTs = $utcDate->getTimestamp(); + } catch (\Throwable $e) { + } + + $exif = []; + if (!$basic && !empty($row['exif'])) { + try { + $exif = json_decode($row['exif'], true); + } catch (\Throwable $e) { + } + } + + $gisType = \OCA\Memories\Util::placesGISType(); + $address = -1 === $gisType ? 'Geocoding Unconfigured' : null; + if (!$basic && $gisType > 0) { + $qb = $this->connection->getQueryBuilder(); + $qb->select('e.name') + ->from('memories_places', 'mp') + ->innerJoin('mp', 'memories_planet', 'e', $qb->expr()->eq('mp.osm_id', 'e.osm_id')) + ->where($qb->expr()->eq('mp.fileid', $qb->createNamedParameter($id, \PDO::PARAM_INT))) + ->orderBy('e.admin_level', 'DESC') + ; + $places = $qb->executeQuery()->fetchAll(\PDO::FETCH_COLUMN); + if (\count($places) > 0) { + $address = implode(', ', $places); + } + } + + return [ + 'fileid' => (int) $row['fileid'], + 'dayid' => (int) $row['dayid'], + 'w' => (int) $row['w'], + 'h' => (int) $row['h'], + 'datetaken' => $utcTs, + 'address' => $address, + 'exif' => $exif, + ]; + } } diff --git a/lib/Db/TimelineQueryTags.php b/lib/Db/TimelineQueryTags.php deleted file mode 100644 index 7e05d415..00000000 --- a/lib/Db/TimelineQueryTags.php +++ /dev/null @@ -1,111 +0,0 @@ -getConnection()->getQueryBuilder(); - - return $sqb->select('id')->from('systemtag')->where( - $sqb->expr()->andX( - $sqb->expr()->eq('name', $sqb->createNamedParameter($tagName)), - $sqb->expr()->eq('visibility', $sqb->createNamedParameter(1)), - ) - )->executeQuery()->fetchOne(); - } - - public function transformTagFilter(IQueryBuilder &$query, string $userId, string $tagName) - { - $tagId = $this->getSystemTagId($query, $tagName); - if (false === $tagId) { - throw new \Exception("Tag {$tagName} not found"); - } - - $query->innerJoin('m', 'systemtag_object_mapping', 'stom', $query->expr()->andX( - $query->expr()->eq('stom.objecttype', $query->createNamedParameter('files')), - $query->expr()->eq('stom.objectid', 'm.objectid'), - $query->expr()->eq('stom.systemtagid', $query->createNamedParameter($tagId)), - )); - } - - public function getTags() - { - $query = $this->connection->getQueryBuilder(); - - // SELECT visible tag name and count of photos - $count = $query->func()->count($query->createFunction('DISTINCT m.fileid'), 'count'); - $query->select('st.id', 'st.name', $count)->from('systemtag', 'st')->where( - $query->expr()->eq('visibility', $query->createNamedParameter(1)), - ); - - // WHERE there are items with this tag - $query->innerJoin('st', 'systemtag_object_mapping', 'stom', $query->expr()->andX( - $query->expr()->eq('stom.objecttype', $query->createNamedParameter('files')), - $query->expr()->eq('stom.systemtagid', 'st.id'), - )); - - // WHERE these items are memories indexed photos - $query->innerJoin('stom', 'memories', 'm', $query->expr()->eq('m.objectid', 'stom.objectid')); - - // WHERE these photos are in the user's requested folder recursively - $query = $this->joinFilecache($query); - - // GROUP and ORDER by tag name - $query->groupBy('st.id'); - $query->orderBy($query->createFunction('LOWER(st.name)'), 'ASC'); - $query->addOrderBy('st.id'); // tie-breaker - - // FETCH all tags - $cursor = $this->executeQueryWithCTEs($query); - $tags = $cursor->fetchAll(); - - // Post process - foreach ($tags as &$row) { - $row['id'] = (int) $row['id']; - $row['count'] = (int) $row['count']; - } - - return $tags; - } - - public function getTagPhotos(string $tagName, ?int $limit) - { - $query = $this->connection->getQueryBuilder(); - $tagId = $this->getSystemTagId($query, $tagName); - if (false === $tagId) { - return []; - } - - // SELECT all photos with this tag - $query->select('f.fileid', 'f.etag', 'stom.systemtagid')->from( - 'systemtag_object_mapping', - 'stom' - )->where( - $query->expr()->eq('stom.objecttype', $query->createNamedParameter('files')), - $query->expr()->eq('stom.systemtagid', $query->createNamedParameter($tagId)), - ); - - // WHERE these items are memories indexed photos - $query->innerJoin('stom', 'memories', 'm', $query->expr()->eq('m.objectid', 'stom.objectid')); - - // WHERE these photos are in the user's requested folder recursively - $query = $this->joinFilecache($query); - - // MAX number of files - if (null !== $limit) { - $query->setMaxResults($limit); - } - - // FETCH tag photos - return $this->executeQueryWithCTEs($query)->fetchAll(); - } -} diff --git a/lib/Manager/FsManager.php b/lib/Manager/FsManager.php index 752e94cc..30af17fc 100644 --- a/lib/Manager/FsManager.php +++ b/lib/Manager/FsManager.php @@ -23,7 +23,7 @@ declare(strict_types=1); namespace OCA\Memories\Manager; -use OCA\Memories\Db\TimelineQuery; +use OCA\Memories\Db\AlbumsQuery; use OCA\Memories\Db\TimelineRoot; use OCA\Memories\Exif; use OCA\Memories\Util; @@ -41,20 +41,20 @@ class FsManager protected IConfig $config; protected IUserSession $userSession; protected IRootFolder $rootFolder; - protected TimelineQuery $timelineQuery; + protected AlbumsQuery $albumsQuery; protected IRequest $request; public function __construct( IConfig $config, IUserSession $userSession, IRootFolder $rootFolder, - TimelineQuery $timelineQuery, + AlbumsQuery $albumsQuery, IRequest $request ) { $this->config = $config; $this->userSession = $userSession; $this->rootFolder = $rootFolder; - $this->timelineQuery = $timelineQuery; + $this->albumsQuery = $albumsQuery; $this->request = $request; } @@ -64,11 +64,11 @@ class FsManager $user = $this->userSession->getUser(); // Albums have no folder - if ($this->request->getParam('album') && Util::albumsIsEnabled()) { + if ($this->request->getParam('albums') && Util::albumsIsEnabled()) { if (null !== $user) { return $root; } - if (($token = $this->getShareToken()) && $this->timelineQuery->getAlbumByLink($token)) { + if (($token = $this->getShareToken()) && $this->albumsQuery->getAlbumByLink($token)) { return $root; } } @@ -164,7 +164,7 @@ class FsManager } $uid = $user->getUID(); - $owner = $this->timelineQuery->albumHasUserFile($uid, $id); + $owner = $this->albumsQuery->userHasFile($uid, $id); if (!$owner) { return null; } @@ -186,13 +186,13 @@ class FsManager { try { // Album share - if ($this->request->getParam('album')) { - $album = $this->timelineQuery->getAlbumByLink($this->getShareToken()); + if ($this->request->getParam('albums')) { + $album = $this->albumsQuery->getAlbumByLink($this->getShareToken()); if (null === $album) { return null; } - $owner = $this->timelineQuery->albumHasFile((int) $album['album_id'], $id); + $owner = $this->albumsQuery->hasFile((int) $album['album_id'], $id); if (!$owner) { return null; } diff --git a/lib/UtilController.php b/lib/UtilController.php index 7d2c8015..82f40d87 100644 --- a/lib/UtilController.php +++ b/lib/UtilController.php @@ -52,6 +52,14 @@ trait UtilController return self::getUser()->getUID(); } + /** + * Check if the user is logged in. + */ + public static function isLoggedIn(): bool + { + return null !== \OC::$server->get(\OCP\IUserSession::class)->getUser(); + } + /** * Get a user's home folder. * diff --git a/src/components/Timeline.vue b/src/components/Timeline.vue index 829b7a49..cfa87170 100644 --- a/src/components/Timeline.vue +++ b/src/components/Timeline.vue @@ -653,7 +653,7 @@ export default defineComponent({ if (this.$route.name === "albums" && this.$route.params.name) { const user = this.$route.params.user; const name = this.$route.params.name; - query.set("album", `${user}/${name}`); + query.set("albums", `${user}/${name}`); } // People @@ -676,12 +676,12 @@ export default defineComponent({ // Places if (this.$route.name === "places" && this.$route.params.name) { const name = this.$route.params.name; - query.set("place", name.split("-", 1)[0]); + query.set("places", name.split("-", 1)[0]); } // Tags if (this.$route.name === "tags" && this.$route.params.name) { - query.set("tag", this.$route.params.name); + query.set("tags", this.$route.params.name); } // Map Bounds diff --git a/src/services/API.ts b/src/services/API.ts index a70cd906..95a25db5 100644 --- a/src/services/API.ts +++ b/src/services/API.ts @@ -12,7 +12,7 @@ function tok(url: string) { url = API.Q(url, { token }); } else if (route.name === "album-share") { const token = route.params.token; - url = API.Q(url, { token, album: token }); + url = API.Q(url, { token, albums: token }); } return url; }