big refactor: move more stuff to backend
Signed-off-by: Varun Patil <varunpatil@ucla.edu>pull/563/head
parent
78d063eed6
commit
7d9db06421
|
@ -23,18 +23,23 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace OCA\Memories\ClustersBackend;
|
namespace OCA\Memories\ClustersBackend;
|
||||||
|
|
||||||
use OCA\Memories\Db\TimelineQuery;
|
use OCA\Memories\Db\AlbumsQuery;
|
||||||
use OCA\Memories\Exceptions;
|
use OCA\Memories\Exceptions;
|
||||||
use OCA\Memories\Util;
|
use OCA\Memories\Util;
|
||||||
|
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||||
|
use OCP\IRequest;
|
||||||
|
|
||||||
class AlbumsBackend extends Backend
|
class AlbumsBackend extends Backend
|
||||||
{
|
{
|
||||||
protected TimelineQuery $timelineQuery;
|
protected AlbumsQuery $albumsQuery;
|
||||||
|
protected IRequest $request;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
TimelineQuery $timelineQuery
|
AlbumsQuery $albumsQuery,
|
||||||
|
IRequest $request
|
||||||
) {
|
) {
|
||||||
$this->timelineQuery = $timelineQuery;
|
$this->albumsQuery = $albumsQuery;
|
||||||
|
$this->request = $request;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function appName(): string
|
public function appName(): string
|
||||||
|
@ -52,6 +57,27 @@ class AlbumsBackend extends Backend
|
||||||
return explode('/', $name)[1];
|
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
|
public function getClusters(): array
|
||||||
{
|
{
|
||||||
/** @var \OCP\IRequest $request */
|
/** @var \OCP\IRequest $request */
|
||||||
|
@ -61,10 +87,10 @@ class AlbumsBackend extends Backend
|
||||||
$list = [];
|
$list = [];
|
||||||
$t = (int) $request->getParam('t', 0);
|
$t = (int) $request->getParam('t', 0);
|
||||||
if ($t & 1) { // personal
|
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
|
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
|
// Remove elements with duplicate album_id
|
||||||
|
@ -85,7 +111,7 @@ class AlbumsBackend extends Backend
|
||||||
public function getPhotos(string $name, ?int $limit = null): array
|
public function getPhotos(string $name, ?int $limit = null): array
|
||||||
{
|
{
|
||||||
// Get album
|
// Get album
|
||||||
$album = $this->timelineQuery->getAlbumIfAllowed(Util::getUID(), $name);
|
$album = $this->albumsQuery->getIfAllowed(Util::getUID(), $name);
|
||||||
if (null === $album) {
|
if (null === $album) {
|
||||||
throw Exceptions::NotFound("album {$name}");
|
throw Exceptions::NotFound("album {$name}");
|
||||||
}
|
}
|
||||||
|
@ -93,6 +119,6 @@ class AlbumsBackend extends Backend
|
||||||
// Get files
|
// Get files
|
||||||
$id = (int) $album['album_id'];
|
$id = (int) $album['album_id'];
|
||||||
|
|
||||||
return $this->timelineQuery->getAlbumPhotos($id, $limit) ?? [];
|
return $this->albumsQuery->getAlbumPhotos($id, $limit) ?? [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,9 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace OCA\Memories\ClustersBackend;
|
namespace OCA\Memories\ClustersBackend;
|
||||||
|
|
||||||
|
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||||
|
use OCP\IRequest;
|
||||||
|
|
||||||
abstract class Backend
|
abstract class Backend
|
||||||
{
|
{
|
||||||
/** Mapping of backend name to className */
|
/** Mapping of backend name to className */
|
||||||
|
@ -39,6 +42,18 @@ abstract class Backend
|
||||||
*/
|
*/
|
||||||
abstract public function isEnabled(): bool;
|
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.
|
* 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;
|
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.
|
* Register a new backend.
|
||||||
*
|
*
|
||||||
|
|
|
@ -25,20 +25,25 @@ namespace OCA\Memories\ClustersBackend;
|
||||||
|
|
||||||
use OCA\Memories\Db\TimelineQuery;
|
use OCA\Memories\Db\TimelineQuery;
|
||||||
use OCA\Memories\Util;
|
use OCA\Memories\Util;
|
||||||
|
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||||
use OCP\IConfig;
|
use OCP\IConfig;
|
||||||
|
use OCP\IRequest;
|
||||||
|
|
||||||
class FaceRecognitionBackend extends Backend
|
class FaceRecognitionBackend extends Backend
|
||||||
{
|
{
|
||||||
use PeopleBackendUtils;
|
use PeopleBackendUtils;
|
||||||
|
|
||||||
protected TimelineQuery $timelineQuery;
|
protected IRequest $request;
|
||||||
|
protected TimelineQuery $tq;
|
||||||
protected IConfig $config;
|
protected IConfig $config;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
TimelineQuery $timelineQuery,
|
IRequest $request,
|
||||||
|
TimelineQuery $tq,
|
||||||
IConfig $config
|
IConfig $config
|
||||||
) {
|
) {
|
||||||
$this->timelineQuery = $timelineQuery;
|
$this->request = $request;
|
||||||
|
$this->tq = $tq;
|
||||||
$this->config = $config;
|
$this->config = $config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,17 +58,138 @@ class FaceRecognitionBackend extends Backend
|
||||||
&& Util::facerecognitionIsEnabled();
|
&& 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
|
public function getClusters(): array
|
||||||
{
|
{
|
||||||
return array_merge(
|
$faces = array_merge(
|
||||||
$this->timelineQuery->getFaceRecognitionPersons($this->model()),
|
$this->getFaceRecognitionPersons(),
|
||||||
$this->timelineQuery->getFaceRecognitionClusters($this->model())
|
$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
|
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)
|
public function sortPhotosForPreview(array &$photos)
|
||||||
|
@ -93,4 +219,93 @@ class FaceRecognitionBackend extends Backend
|
||||||
{
|
{
|
||||||
return (int) $this->config->getAppValue('facerecognition', 'model', -1);
|
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() ?: [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,15 +25,18 @@ namespace OCA\Memories\ClustersBackend;
|
||||||
|
|
||||||
use OCA\Memories\Db\TimelineQuery;
|
use OCA\Memories\Db\TimelineQuery;
|
||||||
use OCA\Memories\Util;
|
use OCA\Memories\Util;
|
||||||
|
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||||
|
use OCP\IRequest;
|
||||||
|
|
||||||
class PlacesBackend extends Backend
|
class PlacesBackend extends Backend
|
||||||
{
|
{
|
||||||
protected TimelineQuery $timelineQuery;
|
protected TimelineQuery $tq;
|
||||||
|
protected IRequest $request;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(TimelineQuery $tq, IRequest $request)
|
||||||
TimelineQuery $timelineQuery
|
{
|
||||||
) {
|
$this->tq = $tq;
|
||||||
$this->timelineQuery = $timelineQuery;
|
$this->request = $request;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function appName(): string
|
public function appName(): string
|
||||||
|
@ -46,13 +49,72 @@ class PlacesBackend extends Backend
|
||||||
return Util::placesGISType() > 0;
|
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
|
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
|
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() ?: [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,17 +25,20 @@ namespace OCA\Memories\ClustersBackend;
|
||||||
|
|
||||||
use OCA\Memories\Db\TimelineQuery;
|
use OCA\Memories\Db\TimelineQuery;
|
||||||
use OCA\Memories\Util;
|
use OCA\Memories\Util;
|
||||||
|
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||||
|
use OCP\IRequest;
|
||||||
|
|
||||||
class RecognizeBackend extends Backend
|
class RecognizeBackend extends Backend
|
||||||
{
|
{
|
||||||
use PeopleBackendUtils;
|
use PeopleBackendUtils;
|
||||||
|
|
||||||
protected TimelineQuery $timelineQuery;
|
protected TimelineQuery $tq;
|
||||||
|
protected IRequest $request;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(TimelineQuery $tq, IRequest $request)
|
||||||
TimelineQuery $timelineQuery
|
{
|
||||||
) {
|
$this->tq = $tq;
|
||||||
$this->timelineQuery = $timelineQuery;
|
$this->request = $request;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function appName(): string
|
public function appName(): string
|
||||||
|
@ -48,14 +51,155 @@ class RecognizeBackend extends Backend
|
||||||
return Util::recognizeIsEnabled();
|
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
|
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
|
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)
|
public function sortPhotosForPreview(array &$photos)
|
||||||
|
|
|
@ -25,15 +25,18 @@ namespace OCA\Memories\ClustersBackend;
|
||||||
|
|
||||||
use OCA\Memories\Db\TimelineQuery;
|
use OCA\Memories\Db\TimelineQuery;
|
||||||
use OCA\Memories\Util;
|
use OCA\Memories\Util;
|
||||||
|
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||||
|
use OCP\IRequest;
|
||||||
|
|
||||||
class TagsBackend extends Backend
|
class TagsBackend extends Backend
|
||||||
{
|
{
|
||||||
protected TimelineQuery $timelineQuery;
|
protected TimelineQuery $tq;
|
||||||
|
protected IRequest $request;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(TimelineQuery $tq, IRequest $request)
|
||||||
TimelineQuery $timelineQuery
|
{
|
||||||
) {
|
$this->tq = $tq;
|
||||||
$this->timelineQuery = $timelineQuery;
|
$this->request = $request;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function appName(): string
|
public function appName(): string
|
||||||
|
@ -46,13 +49,103 @@ class TagsBackend extends Backend
|
||||||
return Util::tagsIsEnabled();
|
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
|
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
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,11 +114,7 @@ class ClustersController extends GenericApiController
|
||||||
throw Exceptions::NotLoggedIn();
|
throw Exceptions::NotLoggedIn();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (\array_key_exists($backend, Backend::$backends)) {
|
$this->backend = Backend::get($backend);
|
||||||
$this->backend = \OC::$server->get(Backend::$backends[$backend]);
|
|
||||||
} else {
|
|
||||||
throw new \Exception("Invalid clusters backend '{$backend}'");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$this->backend->isEnabled()) {
|
if (!$this->backend->isEnabled()) {
|
||||||
throw Exceptions::NotEnabled($this->backend->appName());
|
throw Exceptions::NotEnabled($this->backend->appName());
|
||||||
|
|
|
@ -40,21 +40,18 @@ class DaysController extends GenericApiController
|
||||||
public function days(): Http\Response
|
public function days(): Http\Response
|
||||||
{
|
{
|
||||||
return Util::guardEx(function () {
|
return Util::guardEx(function () {
|
||||||
$uid = $this->getShareToken() ? '' : Util::getUID();
|
|
||||||
|
|
||||||
$list = $this->timelineQuery->getDays(
|
$list = $this->timelineQuery->getDays(
|
||||||
$uid,
|
|
||||||
$this->isRecursive(),
|
$this->isRecursive(),
|
||||||
$this->isArchive(),
|
$this->isArchive(),
|
||||||
$this->getTransformations(true),
|
$this->getTransformations(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($this->isMonthView()) {
|
if ($this->isMonthView()) {
|
||||||
// Group days together into months
|
// Group days together into months
|
||||||
$list = $this->timelineQuery->daysToMonths($list);
|
$list = $this->daysToMonths($list);
|
||||||
} else {
|
} else {
|
||||||
// Preload some day responses
|
// Preload some day responses
|
||||||
$this->preloadDays($list, $uid);
|
$this->preloadDays($list);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reverse response if requested. Folders still stay at top.
|
// Reverse response if requested. Folders still stay at top.
|
||||||
|
@ -80,8 +77,6 @@ class DaysController extends GenericApiController
|
||||||
public function day(string $id): Http\Response
|
public function day(string $id): Http\Response
|
||||||
{
|
{
|
||||||
return Util::guardEx(function () use ($id) {
|
return Util::guardEx(function () use ($id) {
|
||||||
$uid = $this->getShareToken() ? '' : Util::getUID();
|
|
||||||
|
|
||||||
// Check for wildcard
|
// Check for wildcard
|
||||||
$dayIds = [];
|
$dayIds = [];
|
||||||
if ('*' === $id) {
|
if ('*' === $id) {
|
||||||
|
@ -98,16 +93,15 @@ class DaysController extends GenericApiController
|
||||||
|
|
||||||
// Convert to actual dayIds if month view
|
// Convert to actual dayIds if month view
|
||||||
if ($this->isMonthView()) {
|
if ($this->isMonthView()) {
|
||||||
$dayIds = $this->timelineQuery->monthIdToDayIds((int) $dayIds[0]);
|
$dayIds = $this->monthIdToDayIds((int) $dayIds[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run actual query
|
// Run actual query
|
||||||
$list = $this->timelineQuery->getDay(
|
$list = $this->timelineQuery->getDay(
|
||||||
$uid,
|
|
||||||
$dayIds,
|
$dayIds,
|
||||||
$this->isRecursive(),
|
$this->isRecursive(),
|
||||||
$this->isArchive(),
|
$this->isArchive(),
|
||||||
$this->getTransformations(false),
|
$this->getTransformations(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Force month id for dayId for month view
|
// Force month id for dayId for month view
|
||||||
|
@ -145,20 +139,16 @@ class DaysController extends GenericApiController
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get transformations depending on the request.
|
* 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 = [];
|
$transforms = [];
|
||||||
|
|
||||||
// Filter for one album
|
// Add clustering transforms
|
||||||
if (($albumId = $this->request->getParam('album')) && Util::albumsIsEnabled()) {
|
$transforms = array_merge($transforms, \OCA\Memories\ClustersBackend\Backend::getTransforms($this->request));
|
||||||
$transforms[] = [$this->timelineQuery, 'transformAlbumFilter', $albumId];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Other transforms not allowed for public shares
|
// Other transforms not allowed for public shares
|
||||||
if (null === $this->userSession->getUser()) {
|
if (!Util::isLoggedIn()) {
|
||||||
return $transforms;
|
return $transforms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,47 +162,14 @@ class DaysController extends GenericApiController
|
||||||
$transforms[] = [$this->timelineQuery, 'transformVideoFilter'];
|
$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
|
// Filter geological bounds
|
||||||
$bounds = $this->request->getParam('mapbounds');
|
if ($bounds = $this->request->getParam('mapbounds')) {
|
||||||
if ($bounds) {
|
|
||||||
$transforms[] = [$this->timelineQuery, 'transformMapBoundsFilter', $bounds];
|
$transforms[] = [$this->timelineQuery, 'transformMapBoundsFilter', $bounds];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limit number of responses for day query
|
// Limit number of responses for day query
|
||||||
$limit = $this->request->getParam('limit');
|
if ($limit = $this->request->getParam('limit')) {
|
||||||
if ($limit) {
|
$transforms[] = [$this->timelineQuery, 'transformLimit', (int) $limit];
|
||||||
$transforms[] = [$this->timelineQuery, 'transformLimitDay', (int) $limit];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $transforms;
|
return $transforms;
|
||||||
|
@ -221,10 +178,9 @@ class DaysController extends GenericApiController
|
||||||
/**
|
/**
|
||||||
* Preload a few "day" at the start of "days" response.
|
* Preload a few "day" at the start of "days" response.
|
||||||
*
|
*
|
||||||
* @param array $days the days array
|
* @param array $days the days array
|
||||||
* @param string $uid User ID or blank for public shares
|
|
||||||
*/
|
*/
|
||||||
private function preloadDays(array &$days, string $uid)
|
private function preloadDays(array &$days)
|
||||||
{
|
{
|
||||||
$transforms = $this->getTransformations(false);
|
$transforms = $this->getTransformations(false);
|
||||||
$preloaded = 0;
|
$preloaded = 0;
|
||||||
|
@ -246,7 +202,6 @@ class DaysController extends GenericApiController
|
||||||
|
|
||||||
if (\count($preloadDayIds) > 0) {
|
if (\count($preloadDayIds) > 0) {
|
||||||
$allDetails = $this->timelineQuery->getDay(
|
$allDetails = $this->timelineQuery->getDay(
|
||||||
$uid,
|
|
||||||
$preloadDayIds,
|
$preloadDayIds,
|
||||||
$this->isRecursive(),
|
$this->isRecursive(),
|
||||||
$this->isArchive(),
|
$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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,9 +51,4 @@ trait GenericApiControllerParams
|
||||||
{
|
{
|
||||||
return null !== $this->request->getParam('reverse');
|
return null !== $this->request->getParam('reverse');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getShareToken()
|
|
||||||
{
|
|
||||||
return $this->request->getParam('token');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
namespace OCA\Memories\Controller;
|
namespace OCA\Memories\Controller;
|
||||||
|
|
||||||
use OCA\Memories\Db\TimelineQuery;
|
use OCA\Memories\Db\AlbumsQuery;
|
||||||
use OCP\App\IAppManager;
|
use OCP\App\IAppManager;
|
||||||
use OCP\AppFramework\Controller;
|
use OCP\AppFramework\Controller;
|
||||||
use OCP\AppFramework\Http\RedirectResponse;
|
use OCP\AppFramework\Http\RedirectResponse;
|
||||||
|
@ -26,7 +26,7 @@ class PublicAlbumController extends Controller
|
||||||
protected IUserSession $userSession;
|
protected IUserSession $userSession;
|
||||||
protected IRootFolder $rootFolder;
|
protected IRootFolder $rootFolder;
|
||||||
protected IURLGenerator $urlGenerator;
|
protected IURLGenerator $urlGenerator;
|
||||||
protected TimelineQuery $timelineQuery;
|
protected AlbumsQuery $albumsQuery;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
string $appName,
|
string $appName,
|
||||||
|
@ -37,7 +37,7 @@ class PublicAlbumController extends Controller
|
||||||
IUserSession $userSession,
|
IUserSession $userSession,
|
||||||
IRootFolder $rootFolder,
|
IRootFolder $rootFolder,
|
||||||
IURLGenerator $urlGenerator,
|
IURLGenerator $urlGenerator,
|
||||||
TimelineQuery $timelineQuery
|
AlbumsQuery $albumsQuery
|
||||||
) {
|
) {
|
||||||
$this->appName = $appName;
|
$this->appName = $appName;
|
||||||
$this->eventDispatcher = $eventDispatcher;
|
$this->eventDispatcher = $eventDispatcher;
|
||||||
|
@ -47,7 +47,7 @@ class PublicAlbumController extends Controller
|
||||||
$this->userSession = $userSession;
|
$this->userSession = $userSession;
|
||||||
$this->rootFolder = $rootFolder;
|
$this->rootFolder = $rootFolder;
|
||||||
$this->urlGenerator = $urlGenerator;
|
$this->urlGenerator = $urlGenerator;
|
||||||
$this->timelineQuery = $timelineQuery;
|
$this->albumsQuery = $albumsQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -58,7 +58,7 @@ class PublicAlbumController extends Controller
|
||||||
public function showShare(string $token)
|
public function showShare(string $token)
|
||||||
{
|
{
|
||||||
// Validate token exists
|
// Validate token exists
|
||||||
$album = $this->timelineQuery->getAlbumByLink($token);
|
$album = $this->albumsQuery->getAlbumByLink($token);
|
||||||
if (!$album) {
|
if (!$album) {
|
||||||
return new TemplateResponse('core', '404', [], 'guest');
|
return new TemplateResponse('core', '404', [], 'guest');
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,7 @@ class PublicAlbumController extends Controller
|
||||||
$uid = $user->getUID();
|
$uid = $user->getUID();
|
||||||
$albumId = (int) $album['album_id'];
|
$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'];
|
$idStr = $album['user'].'/'.$album['name'];
|
||||||
$url = $this->urlGenerator->linkToRoute('memories.Page.albums', ['id' => $idStr]);
|
$url = $this->urlGenerator->linkToRoute('memories.Page.albums', ['id' => $idStr]);
|
||||||
|
|
||||||
|
@ -99,7 +99,7 @@ class PublicAlbumController extends Controller
|
||||||
{
|
{
|
||||||
$fileId = (int) $album['last_added_photo'];
|
$fileId = (int) $album['last_added_photo'];
|
||||||
$albumId = (int) $album['album_id'];
|
$albumId = (int) $album['album_id'];
|
||||||
$owner = $this->timelineQuery->albumHasFile($albumId, $fileId);
|
$owner = $this->albumsQuery->hasFile($albumId, $fileId);
|
||||||
if (!$owner) {
|
if (!$owner) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,30 +7,17 @@ namespace OCA\Memories\Db;
|
||||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||||
use OCP\IDBConnection;
|
use OCP\IDBConnection;
|
||||||
|
|
||||||
trait TimelineQueryAlbums
|
class AlbumsQuery
|
||||||
{
|
{
|
||||||
protected IDBConnection $connection;
|
protected IDBConnection $connection;
|
||||||
|
|
||||||
/** Transform only for album */
|
public function __construct(IDBConnection $connection)
|
||||||
public function transformAlbumFilter(IQueryBuilder &$query, string $uid, string $albumId)
|
|
||||||
{
|
{
|
||||||
// Get album object
|
$this->connection = $connection;
|
||||||
$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'),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get list of albums */
|
/** Get list of albums */
|
||||||
public function getAlbums(string $uid, bool $shared = false)
|
public function getList(string $uid, bool $shared = false)
|
||||||
{
|
{
|
||||||
$query = $this->connection->getQueryBuilder();
|
$query = $this->connection->getQueryBuilder();
|
||||||
|
|
||||||
|
@ -79,50 +66,12 @@ trait TimelineQueryAlbums
|
||||||
return $albums;
|
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.
|
* Check if an album has a file.
|
||||||
*
|
*
|
||||||
* @return bool|string owner of 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 = $this->connection->getQueryBuilder();
|
||||||
$query->select('owner')->from('photos_albums_files')->where(
|
$query->select('owner')->from('photos_albums_files')->where(
|
||||||
|
@ -140,7 +89,7 @@ trait TimelineQueryAlbums
|
||||||
*
|
*
|
||||||
* @return bool|string owner of file
|
* @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 = $this->connection->getQueryBuilder();
|
||||||
$query->select('paf.owner')->from('photos_albums_files', 'paf')->where(
|
$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 $uid UID of CURRENT user
|
||||||
* @param string $albumId $user/$name where $user is the OWNER of the album
|
* @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;
|
$album = null;
|
||||||
|
|
||||||
|
@ -208,7 +157,7 @@ trait TimelineQueryAlbums
|
||||||
|
|
||||||
// Check in collaborators instead
|
// Check in collaborators instead
|
||||||
$albumNumId = (int) $album['album_id'];
|
$albumNumId = (int) $album['album_id'];
|
||||||
if ($this->userIsAlbumCollaborator($uid, $albumNumId)) {
|
if ($this->userIsCollaborator($uid, $albumNumId)) {
|
||||||
return $album;
|
return $album;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,7 +172,7 @@ trait TimelineQueryAlbums
|
||||||
* @param string $uid User ID
|
* @param string $uid User ID
|
||||||
* @param int $albumId Album ID (numeric)
|
* @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();
|
$query = $this->connection->getQueryBuilder();
|
||||||
$ids = $this->getSelfCollaborators($uid);
|
$ids = $this->getSelfCollaborators($uid);
|
|
@ -6,20 +6,16 @@ namespace OCA\Memories\Db;
|
||||||
|
|
||||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||||
use OCP\IDBConnection;
|
use OCP\IDBConnection;
|
||||||
|
use OCP\IRequest;
|
||||||
|
|
||||||
class TimelineQuery
|
class TimelineQuery
|
||||||
{
|
{
|
||||||
use TimelineQueryAlbums;
|
|
||||||
use TimelineQueryDays;
|
use TimelineQueryDays;
|
||||||
use TimelineQueryFilters;
|
use TimelineQueryFilters;
|
||||||
use TimelineQueryFolders;
|
use TimelineQueryFolders;
|
||||||
use TimelineQueryLivePhoto;
|
use TimelineQueryLivePhoto;
|
||||||
use TimelineQueryMap;
|
use TimelineQueryMap;
|
||||||
use TimelineQueryPeopleFaceRecognition;
|
|
||||||
use TimelineQueryPeopleRecognize;
|
|
||||||
use TimelineQueryPlaces;
|
|
||||||
use TimelineQuerySingleItem;
|
use TimelineQuerySingleItem;
|
||||||
use TimelineQueryTags;
|
|
||||||
|
|
||||||
public const TIMELINE_SELECT = [
|
public const TIMELINE_SELECT = [
|
||||||
'm.isvideo', 'm.video_duration', 'm.datetaken', 'm.dayid', 'm.w', 'm.h', 'm.liveid',
|
'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 IDBConnection $connection;
|
||||||
|
protected IRequest $request;
|
||||||
private ?TimelineRoot $_root = null;
|
private ?TimelineRoot $_root = null;
|
||||||
|
|
||||||
public function __construct(IDBConnection $connection)
|
public function __construct(IDBConnection $connection, IRequest $request)
|
||||||
{
|
{
|
||||||
$this->connection = $connection;
|
$this->connection = $connection;
|
||||||
|
$this->request = $request;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function root(): TimelineRoot
|
public function root(): TimelineRoot
|
||||||
|
@ -45,6 +43,11 @@ class TimelineQuery
|
||||||
return $this->_root;
|
return $this->_root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getBuilder()
|
||||||
|
{
|
||||||
|
return $this->connection->getQueryBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
public static function debugQuery(IQueryBuilder &$query, string $sql = '')
|
public static function debugQuery(IQueryBuilder &$query, string $sql = '')
|
||||||
{
|
{
|
||||||
// Print the query and exit
|
// Print the query and exit
|
||||||
|
@ -74,63 +77,4 @@ class TimelineQuery
|
||||||
|
|
||||||
return $sql;
|
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,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,7 +85,6 @@ trait TimelineQueryDays
|
||||||
* @return array The days response
|
* @return array The days response
|
||||||
*/
|
*/
|
||||||
public function getDays(
|
public function getDays(
|
||||||
string $uid,
|
|
||||||
bool $recursive,
|
bool $recursive,
|
||||||
bool $archive,
|
bool $archive,
|
||||||
array $queryTransforms = []
|
array $queryTransforms = []
|
||||||
|
@ -105,7 +104,7 @@ trait TimelineQueryDays
|
||||||
;
|
;
|
||||||
|
|
||||||
// Apply all transformations
|
// Apply all transformations
|
||||||
$this->applyAllTransforms($queryTransforms, $query, $uid);
|
$this->applyAllTransforms($queryTransforms, $query, true);
|
||||||
|
|
||||||
$cursor = $this->executeQueryWithCTEs($query);
|
$cursor = $this->executeQueryWithCTEs($query);
|
||||||
$rows = $cursor->fetchAll();
|
$rows = $cursor->fetchAll();
|
||||||
|
@ -127,7 +126,6 @@ trait TimelineQueryDays
|
||||||
* @return array An array of day responses
|
* @return array An array of day responses
|
||||||
*/
|
*/
|
||||||
public function getDay(
|
public function getDay(
|
||||||
string $uid,
|
|
||||||
?array $day_ids,
|
?array $day_ids,
|
||||||
bool $recursive,
|
bool $recursive,
|
||||||
bool $archive,
|
bool $archive,
|
||||||
|
@ -160,14 +158,14 @@ trait TimelineQueryDays
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add favorite field
|
// Add favorite field
|
||||||
$this->addFavoriteTag($query, $uid);
|
$this->addFavoriteTag($query);
|
||||||
|
|
||||||
// Group and sort by date taken
|
// Group and sort by date taken
|
||||||
$query->orderBy('m.datetaken', 'DESC');
|
$query->orderBy('m.datetaken', 'DESC');
|
||||||
$query->addOrderBy('m.fileid', 'DESC'); // tie-breaker
|
$query->addOrderBy('m.fileid', 'DESC'); // tie-breaker
|
||||||
|
|
||||||
// Apply all transformations
|
// Apply all transformations
|
||||||
$this->applyAllTransforms($queryTransforms, $query, $uid);
|
$this->applyAllTransforms($queryTransforms, $query, false);
|
||||||
|
|
||||||
$cursor = $this->executeQueryWithCTEs($query);
|
$cursor = $this->executeQueryWithCTEs($query);
|
||||||
$rows = $cursor->fetchAll();
|
$rows = $cursor->fetchAll();
|
||||||
|
@ -176,6 +174,66 @@ trait TimelineQueryDays
|
||||||
return $this->processDay($rows);
|
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.
|
* Process the days response.
|
||||||
*
|
*
|
||||||
|
@ -215,9 +273,8 @@ trait TimelineQueryDays
|
||||||
unset($row['liveid']);
|
unset($row['liveid']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// All transform processing
|
// All cluster transformations
|
||||||
$this->processPeopleRecognizeDetection($row);
|
\OCA\Memories\ClustersBackend\Backend::applyDayPostTransforms($this->request, $row);
|
||||||
$this->processFaceRecognitionDetection($row);
|
|
||||||
|
|
||||||
// We don't need these fields
|
// We don't need these fields
|
||||||
unset($row['datetaken']);
|
unset($row['datetaken']);
|
||||||
|
@ -226,25 +283,6 @@ trait TimelineQueryDays
|
||||||
return $day;
|
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.
|
* Get all folders inside a top folder.
|
||||||
*/
|
*/
|
||||||
|
@ -257,45 +295,4 @@ trait TimelineQueryDays
|
||||||
$query->setParameter('topFolderIds', $root->getIds(), IQueryBuilder::PARAM_INT_ARRAY);
|
$query->setParameter('topFolderIds', $root->getIds(), IQueryBuilder::PARAM_INT_ARRAY);
|
||||||
$query->setParameter('cteFoldersArchive', $archive, IQueryBuilder::PARAM_BOOL);
|
$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,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,60 +4,63 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace OCA\Memories\Db;
|
namespace OCA\Memories\Db;
|
||||||
|
|
||||||
|
use OCA\Memories\Util;
|
||||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||||
use OCP\ITags;
|
use OCP\ITags;
|
||||||
|
|
||||||
trait TimelineQueryFilters
|
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(
|
if (Util::isLoggedIn()) {
|
||||||
$query->expr()->eq('vcoi.objid', 'm.fileid'),
|
$query->innerJoin('m', 'vcategory_to_object', 'vcoi', $query->expr()->andX(
|
||||||
$query->expr()->in('vcoi.categoryid', $this->getFavoriteVCategoryFun($query, $userId)),
|
$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(
|
if (Util::isLoggedIn()) {
|
||||||
$query->expr()->eq('vco.objid', 'm.fileid'),
|
$query->leftJoin('m', 'vcategory_to_object', 'vco', $query->expr()->andX(
|
||||||
$query->expr()->in('vco.categoryid', $this->getFavoriteVCategoryFun($query, $userId)),
|
$query->expr()->eq('vco.objid', 'm.fileid'),
|
||||||
));
|
$query->expr()->in('vco.categoryid', $this->getFavoriteVCategoryFun($query)),
|
||||||
$query->addSelect('vco.categoryid');
|
));
|
||||||
|
$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')));
|
$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) {
|
||||||
if ($limit < 1 || $limit > 100) {
|
$query->setMaxResults($limit);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
$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) {
|
foreach ($transforms as &$transform) {
|
||||||
$fun = \array_slice($transform, 0, 2);
|
$fun = \array_slice($transform, 0, 2);
|
||||||
$params = \array_slice($transform, 2);
|
$params = \array_slice($transform, 2);
|
||||||
array_unshift($params, $uid);
|
array_unshift($params, $aggregate);
|
||||||
array_unshift($params, $query);
|
array_unshift($params, $query);
|
||||||
$fun(...$params);
|
$fun(...$params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getFavoriteVCategoryFun(IQueryBuilder &$query, string $userId)
|
private function getFavoriteVCategoryFun(IQueryBuilder &$query)
|
||||||
{
|
{
|
||||||
return $query->createFunction(
|
return $query->createFunction(
|
||||||
$query->getConnection()->getQueryBuilder()->select('id')->from('vcategory', 'vc')->where(
|
$query->getConnection()->getQueryBuilder()->select('id')->from('vcategory', 'vc')->where(
|
||||||
$query->expr()->andX(
|
$query->expr()->andX(
|
||||||
$query->expr()->eq('type', $query->createNamedParameter('files')),
|
$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)),
|
$query->expr()->eq('category', $query->createNamedParameter(ITags::TAG_FAVORITE)),
|
||||||
)
|
)
|
||||||
)->getSQL()
|
)->getSQL()
|
||||||
|
|
|
@ -30,14 +30,13 @@ trait TimelineQueryFolders
|
||||||
$query->setMaxResults(4);
|
$query->setMaxResults(4);
|
||||||
|
|
||||||
// FETCH tag previews
|
// FETCH tag previews
|
||||||
$cursor = $this->executeQueryWithCTEs($query);
|
$rows = $this->executeQueryWithCTEs($query)->fetchAll();
|
||||||
$ans = $cursor->fetchAll();
|
|
||||||
|
|
||||||
// Post-process
|
// Post-process
|
||||||
foreach ($ans as &$row) {
|
foreach ($rows as &$row) {
|
||||||
$row['fileid'] = (int) $row['fileid'];
|
$row['fileid'] = (int) $row['fileid'];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $ans;
|
return $rows;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ trait TimelineQueryMap
|
||||||
{
|
{
|
||||||
protected IDBConnection $connection;
|
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 = explode(',', $bounds);
|
||||||
$bounds = array_map('floatval', $bounds);
|
$bounds = array_map('floatval', $bounds);
|
||||||
|
@ -59,7 +59,7 @@ trait TimelineQueryMap
|
||||||
$query = $this->joinFilecache($query);
|
$query = $this->joinFilecache($query);
|
||||||
|
|
||||||
// Bound the query to the map bounds
|
// Bound the query to the map bounds
|
||||||
$this->transformMapBoundsFilter($query, '', $bounds, 'c');
|
$this->transformMapBoundsFilter($query, false, $bounds, 'c');
|
||||||
|
|
||||||
// Execute query
|
// Execute query
|
||||||
$cursor = $this->executeQueryWithCTEs($query);
|
$cursor = $this->executeQueryWithCTEs($query);
|
||||||
|
|
|
@ -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']);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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']);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -27,4 +27,63 @@ trait TimelineQuerySingleItem
|
||||||
|
|
||||||
return $query->executeQuery()->fetch();
|
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,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -23,7 +23,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace OCA\Memories\Manager;
|
namespace OCA\Memories\Manager;
|
||||||
|
|
||||||
use OCA\Memories\Db\TimelineQuery;
|
use OCA\Memories\Db\AlbumsQuery;
|
||||||
use OCA\Memories\Db\TimelineRoot;
|
use OCA\Memories\Db\TimelineRoot;
|
||||||
use OCA\Memories\Exif;
|
use OCA\Memories\Exif;
|
||||||
use OCA\Memories\Util;
|
use OCA\Memories\Util;
|
||||||
|
@ -41,20 +41,20 @@ class FsManager
|
||||||
protected IConfig $config;
|
protected IConfig $config;
|
||||||
protected IUserSession $userSession;
|
protected IUserSession $userSession;
|
||||||
protected IRootFolder $rootFolder;
|
protected IRootFolder $rootFolder;
|
||||||
protected TimelineQuery $timelineQuery;
|
protected AlbumsQuery $albumsQuery;
|
||||||
protected IRequest $request;
|
protected IRequest $request;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
IConfig $config,
|
IConfig $config,
|
||||||
IUserSession $userSession,
|
IUserSession $userSession,
|
||||||
IRootFolder $rootFolder,
|
IRootFolder $rootFolder,
|
||||||
TimelineQuery $timelineQuery,
|
AlbumsQuery $albumsQuery,
|
||||||
IRequest $request
|
IRequest $request
|
||||||
) {
|
) {
|
||||||
$this->config = $config;
|
$this->config = $config;
|
||||||
$this->userSession = $userSession;
|
$this->userSession = $userSession;
|
||||||
$this->rootFolder = $rootFolder;
|
$this->rootFolder = $rootFolder;
|
||||||
$this->timelineQuery = $timelineQuery;
|
$this->albumsQuery = $albumsQuery;
|
||||||
$this->request = $request;
|
$this->request = $request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,11 +64,11 @@ class FsManager
|
||||||
$user = $this->userSession->getUser();
|
$user = $this->userSession->getUser();
|
||||||
|
|
||||||
// Albums have no folder
|
// Albums have no folder
|
||||||
if ($this->request->getParam('album') && Util::albumsIsEnabled()) {
|
if ($this->request->getParam('albums') && Util::albumsIsEnabled()) {
|
||||||
if (null !== $user) {
|
if (null !== $user) {
|
||||||
return $root;
|
return $root;
|
||||||
}
|
}
|
||||||
if (($token = $this->getShareToken()) && $this->timelineQuery->getAlbumByLink($token)) {
|
if (($token = $this->getShareToken()) && $this->albumsQuery->getAlbumByLink($token)) {
|
||||||
return $root;
|
return $root;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -164,7 +164,7 @@ class FsManager
|
||||||
}
|
}
|
||||||
$uid = $user->getUID();
|
$uid = $user->getUID();
|
||||||
|
|
||||||
$owner = $this->timelineQuery->albumHasUserFile($uid, $id);
|
$owner = $this->albumsQuery->userHasFile($uid, $id);
|
||||||
if (!$owner) {
|
if (!$owner) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -186,13 +186,13 @@ class FsManager
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
// Album share
|
// Album share
|
||||||
if ($this->request->getParam('album')) {
|
if ($this->request->getParam('albums')) {
|
||||||
$album = $this->timelineQuery->getAlbumByLink($this->getShareToken());
|
$album = $this->albumsQuery->getAlbumByLink($this->getShareToken());
|
||||||
if (null === $album) {
|
if (null === $album) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$owner = $this->timelineQuery->albumHasFile((int) $album['album_id'], $id);
|
$owner = $this->albumsQuery->hasFile((int) $album['album_id'], $id);
|
||||||
if (!$owner) {
|
if (!$owner) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,14 @@ trait UtilController
|
||||||
return self::getUser()->getUID();
|
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.
|
* Get a user's home folder.
|
||||||
*
|
*
|
||||||
|
|
|
@ -653,7 +653,7 @@ export default defineComponent({
|
||||||
if (this.$route.name === "albums" && this.$route.params.name) {
|
if (this.$route.name === "albums" && this.$route.params.name) {
|
||||||
const user = <string>this.$route.params.user;
|
const user = <string>this.$route.params.user;
|
||||||
const name = <string>this.$route.params.name;
|
const name = <string>this.$route.params.name;
|
||||||
query.set("album", `${user}/${name}`);
|
query.set("albums", `${user}/${name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// People
|
// People
|
||||||
|
@ -676,12 +676,12 @@ export default defineComponent({
|
||||||
// Places
|
// Places
|
||||||
if (this.$route.name === "places" && this.$route.params.name) {
|
if (this.$route.name === "places" && this.$route.params.name) {
|
||||||
const name = <string>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
|
// Tags
|
||||||
if (this.$route.name === "tags" && this.$route.params.name) {
|
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
|
// Map Bounds
|
||||||
|
|
|
@ -12,7 +12,7 @@ function tok(url: string) {
|
||||||
url = API.Q(url, { token });
|
url = API.Q(url, { token });
|
||||||
} else if (route.name === "album-share") {
|
} else if (route.name === "album-share") {
|
||||||
const token = <string>route.params.token;
|
const token = <string>route.params.token;
|
||||||
url = API.Q(url, { token, album: token });
|
url = API.Q(url, { token, albums: token });
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue