big refactor: move more stuff to backend

Signed-off-by: Varun Patil <varunpatil@ucla.edu>
pull/563/head
Varun Patil 2023-03-23 16:58:49 -07:00
parent 78d063eed6
commit 7d9db06421
25 changed files with 893 additions and 944 deletions

View File

@ -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) ?? [];
}
}

View File

@ -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.
*

View File

@ -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() ?: [];
}
}

View File

@ -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() ?: [];
}
}

View File

@ -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)

View File

@ -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();
}
}

View File

@ -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());

View File

@ -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;
}
}

View File

@ -51,9 +51,4 @@ trait GenericApiControllerParams
{
return null !== $this->request->getParam('reverse');
}
protected function getShareToken()
{
return $this->request->getParam('token');
}
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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,
];
}
}

View File

@ -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,
));
}
}

View File

@ -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()

View File

@ -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;
}
}

View File

@ -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);

View File

@ -1,242 +0,0 @@
<?php
declare(strict_types=1);
namespace OCA\Memories\Db;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
trait TimelineQueryPeopleFaceRecognition
{
protected IDBConnection $connection;
public function transformPeopleFaceRecognitionFilter(IQueryBuilder &$query, string $userId, int $currentModel, string $personStr)
{
// 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($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']);
}
}

View File

@ -1,165 +0,0 @@
<?php
declare(strict_types=1);
namespace OCA\Memories\Db;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
trait TimelineQueryPeopleRecognize
{
protected IDBConnection $connection;
public function transformPeopleRecognitionFilter(IQueryBuilder $query, string $userId, string $faceStr, bool $isAggregate)
{
// 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 (!$isAggregate) {
// Multiple detections for the same image
$query->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']);
}
}

View File

@ -1,80 +0,0 @@
<?php
declare(strict_types=1);
namespace OCA\Memories\Db;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
trait TimelineQueryPlaces
{
protected IDBConnection $connection;
public function transformPlaceFilter(IQueryBuilder &$query, string $userId, int $locationId)
{
$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 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();
}
}

View File

@ -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,
];
}
}

View File

@ -1,111 +0,0 @@
<?php
declare(strict_types=1);
namespace OCA\Memories\Db;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
trait TimelineQueryTags
{
protected IDBConnection $connection;
public 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();
}
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();
}
}

View File

@ -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;
}

View File

@ -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.
*

View File

@ -653,7 +653,7 @@ export default defineComponent({
if (this.$route.name === "albums" && this.$route.params.name) {
const user = <string>this.$route.params.user;
const name = <string>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 = <string>this.$route.params.name;
query.set("place", <string>name.split("-", 1)[0]);
query.set("places", <string>name.split("-", 1)[0]);
}
// Tags
if (this.$route.name === "tags" && this.$route.params.name) {
query.set("tag", <string>this.$route.params.name);
query.set("tags", <string>this.$route.params.name);
}
// Map Bounds

View File

@ -12,7 +12,7 @@ function tok(url: string) {
url = API.Q(url, { token });
} else if (route.name === "album-share") {
const token = <string>route.params.token;
url = API.Q(url, { token, album: token });
url = API.Q(url, { token, albums: token });
}
return url;
}