People: Add faces API endpoint & JS model #22
This commit is contained in:
parent
8492efebcf
commit
ed22f245db
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-09-02 14:05+0000\n"
|
||||
"POT-Creation-Date: 2021-09-18 12:59+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -17,277 +17,305 @@ msgstr ""
|
|||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: messages.go:75
|
||||
#: messages.go:81
|
||||
msgid "Unexpected error, please try again"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:76
|
||||
#: messages.go:82
|
||||
msgid "Invalid request"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:77
|
||||
#: messages.go:83
|
||||
msgid "Changes could not be saved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:78
|
||||
#: messages.go:84
|
||||
msgid "Could not be deleted"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:79
|
||||
#: messages.go:85
|
||||
#, c-format
|
||||
msgid "%s already exists"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:80 messages.go:83
|
||||
msgid "Not found on server, deleted?"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:81
|
||||
msgid "File not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:82
|
||||
msgid "Selection not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:84
|
||||
msgid "Account not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:85
|
||||
msgid "User not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:86
|
||||
msgid "Label not found"
|
||||
msgid "Not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:87
|
||||
msgid "Album not found"
|
||||
msgid "File not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:88
|
||||
msgid "Subject not found"
|
||||
msgid "Selection not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:89
|
||||
msgid "Not available in public mode"
|
||||
msgid "Entity not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:90
|
||||
msgid "not available in read-only mode"
|
||||
msgid "Account not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:91
|
||||
msgid "Please log in and try again"
|
||||
msgid "User not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:92
|
||||
msgid "Upload might be offensive"
|
||||
msgid "Label not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:93
|
||||
msgid "No items selected"
|
||||
msgid "Album not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:94
|
||||
msgid "Failed creating file, please check permissions"
|
||||
msgid "Subject not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:95
|
||||
msgid "Failed creating folder, please check permissions"
|
||||
msgid "Person not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:96
|
||||
msgid "Could not connect, please try again"
|
||||
msgid "Face not found"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:97
|
||||
msgid "Invalid password, please try again"
|
||||
msgid "Not available in public mode"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:98
|
||||
msgid "Feature disabled"
|
||||
msgid "not available in read-only mode"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:99
|
||||
msgid "No labels selected"
|
||||
msgid "Please log in and try again"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:100
|
||||
msgid "No albums selected"
|
||||
msgid "Upload might be offensive"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:101
|
||||
msgid "No files available for download"
|
||||
msgid "No items selected"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:102
|
||||
msgid "Failed to create zip file"
|
||||
msgid "Failed creating file, please check permissions"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:103
|
||||
msgid "Invalid credentials"
|
||||
msgid "Failed creating folder, please check permissions"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:104
|
||||
msgid "Invalid link"
|
||||
msgid "Could not connect, please try again"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:105
|
||||
msgid "Invalid password, please try again"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:106
|
||||
msgid "Feature disabled"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:107
|
||||
msgid "Changes successfully saved"
|
||||
msgid "No labels selected"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:108
|
||||
msgid "Album created"
|
||||
msgid "No albums selected"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:109
|
||||
msgid "Album saved"
|
||||
msgid "No files available for download"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:110
|
||||
#, c-format
|
||||
msgid "Album %s deleted"
|
||||
msgid "Failed to create zip file"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:111
|
||||
msgid "Album contents cloned"
|
||||
msgid "Invalid credentials"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:112
|
||||
msgid "File removed from stack"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:113
|
||||
msgid "File deleted"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:114
|
||||
#, c-format
|
||||
msgid "Selection added to %s"
|
||||
msgid "Invalid link"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:115
|
||||
#, c-format
|
||||
msgid "One entry added to %s"
|
||||
msgid "Changes successfully saved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:116
|
||||
#, c-format
|
||||
msgid "%d entries added to %s"
|
||||
msgid "Album created"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:117
|
||||
#, c-format
|
||||
msgid "One entry removed from %s"
|
||||
msgid "Album saved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:118
|
||||
#, c-format
|
||||
msgid "%d entries removed from %s"
|
||||
msgid "Album %s deleted"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:119
|
||||
msgid "Account created"
|
||||
msgid "Album contents cloned"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:120
|
||||
msgid "Account saved"
|
||||
msgid "File removed from stack"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:121
|
||||
msgid "Account deleted"
|
||||
msgid "File deleted"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:122
|
||||
msgid "Settings saved"
|
||||
#, c-format
|
||||
msgid "Selection added to %s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:123
|
||||
msgid "Password changed"
|
||||
#, c-format
|
||||
msgid "One entry added to %s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:124
|
||||
#, c-format
|
||||
msgid "Import completed in %d s"
|
||||
msgid "%d entries added to %s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:125
|
||||
msgid "Import canceled"
|
||||
#, c-format
|
||||
msgid "One entry removed from %s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:126
|
||||
#, c-format
|
||||
msgid "Indexing completed in %d s"
|
||||
msgid "%d entries removed from %s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:127
|
||||
msgid "Indexing originals..."
|
||||
msgid "Account created"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:128
|
||||
#, c-format
|
||||
msgid "Indexing files in %s"
|
||||
msgid "Account saved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:129
|
||||
msgid "Indexing canceled"
|
||||
msgid "Account deleted"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:130
|
||||
#, c-format
|
||||
msgid "Removed %d files and %d photos"
|
||||
msgid "Settings saved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:131
|
||||
#, c-format
|
||||
msgid "Moving files from %s"
|
||||
msgid "Password changed"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:132
|
||||
#, c-format
|
||||
msgid "Copying files from %s"
|
||||
msgid "Import completed in %d s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:133
|
||||
msgid "Labels deleted"
|
||||
msgid "Import canceled"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:134
|
||||
msgid "Label saved"
|
||||
#, c-format
|
||||
msgid "Indexing completed in %d s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:135
|
||||
msgid "Indexing originals..."
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:136
|
||||
#, c-format
|
||||
msgid "Indexing files in %s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:137
|
||||
msgid "Indexing canceled"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:138
|
||||
#, c-format
|
||||
msgid "Removed %d files and %d photos"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:139
|
||||
#, c-format
|
||||
msgid "Moving files from %s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:140
|
||||
#, c-format
|
||||
msgid "Copying files from %s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:141
|
||||
msgid "Labels deleted"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:142
|
||||
msgid "Label saved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:143
|
||||
msgid "Subject saved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:144
|
||||
msgid "Subject deleted"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:145
|
||||
msgid "Person saved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:146
|
||||
msgid "Person deleted"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:147
|
||||
#, c-format
|
||||
msgid "%d files uploaded in %d s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:136
|
||||
#: messages.go:148
|
||||
msgid "Selection approved"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:137
|
||||
#: messages.go:149
|
||||
msgid "Selection archived"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:138
|
||||
#: messages.go:150
|
||||
msgid "Selection restored"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:139
|
||||
#: messages.go:151
|
||||
msgid "Selection marked as private"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:140
|
||||
#: messages.go:152
|
||||
msgid "Albums deleted"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:141
|
||||
#: messages.go:153
|
||||
#, c-format
|
||||
msgid "Zip created in %d s"
|
||||
msgstr ""
|
||||
|
||||
#: messages.go:142
|
||||
#: messages.go:154
|
||||
msgid "Permanently deleted"
|
||||
msgstr ""
|
||||
|
|
108
frontend/src/model/face.js
Normal file
108
frontend/src/model/face.js
Normal file
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
|
||||
Copyright (c) 2018 - 2021 Michael Mayer <hello@photoprism.org>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
PhotoPrism® is a registered trademark of Michael Mayer. You may use it as required
|
||||
to describe our software, run your own server, for educational purposes, but not for
|
||||
offering commercial goods, products, or services without prior written permission.
|
||||
In other words, please ask.
|
||||
|
||||
Feel free to send an e-mail to hello@photoprism.org if you have questions,
|
||||
want to support our work, or just want to say hello.
|
||||
|
||||
Additional information can be found in our Developer Guide:
|
||||
https://docs.photoprism.org/developer-guide/
|
||||
|
||||
*/
|
||||
|
||||
import RestModel from "model/rest";
|
||||
import { DateTime } from "luxon";
|
||||
import { config } from "../session";
|
||||
import { $gettext } from "common/vm";
|
||||
|
||||
export class Face extends RestModel {
|
||||
getDefaults() {
|
||||
return {
|
||||
ID: "",
|
||||
Src: "",
|
||||
SubjUID: "",
|
||||
Samples: 0,
|
||||
SampleRadius: 0.0,
|
||||
Collisions: 0,
|
||||
CollisionRadius: 0.0,
|
||||
Marker: null,
|
||||
MatchedAt: "",
|
||||
CreatedAt: "",
|
||||
UpdatedAt: "",
|
||||
};
|
||||
}
|
||||
|
||||
route(view) {
|
||||
return { name: view, query: { q: "face:" + this.ID } };
|
||||
}
|
||||
|
||||
classes(selected) {
|
||||
let classes = ["is-face", "uid-" + this.UID];
|
||||
|
||||
if (selected) classes.push("is-selected");
|
||||
|
||||
return classes;
|
||||
}
|
||||
|
||||
getEntityName() {
|
||||
return this.ID;
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
return this.Name;
|
||||
}
|
||||
|
||||
thumbnailUrl(size) {
|
||||
if (!this.Marker || !this.Marker.FileHash) {
|
||||
return `${config.contentUri}/svg/portrait`;
|
||||
}
|
||||
|
||||
if (!size) {
|
||||
size = "tile_160";
|
||||
}
|
||||
|
||||
if (this.Marker.CropArea && (size === "tile_160" || size === "tile_320")) {
|
||||
return `${config.contentUri}/t/${this.Marker.FileHash}/${config.previewToken()}/${size}/${
|
||||
this.Marker.CropArea
|
||||
}`;
|
||||
} else {
|
||||
return `${config.contentUri}/t/${this.Marker.FileHash}/${config.previewToken()}/${size}`;
|
||||
}
|
||||
}
|
||||
|
||||
getDateString() {
|
||||
return DateTime.fromISO(this.CreatedAt).toLocaleString(DateTime.DATETIME_MED);
|
||||
}
|
||||
|
||||
static batchSize() {
|
||||
return 60;
|
||||
}
|
||||
|
||||
static getCollectionResource() {
|
||||
return "faces";
|
||||
}
|
||||
|
||||
static getModelName() {
|
||||
return $gettext("Face");
|
||||
}
|
||||
}
|
||||
|
||||
export default Face;
|
|
@ -153,7 +153,7 @@ import Event from "pubsub-js";
|
|||
import RestModel from "model/rest";
|
||||
import {MaxItems} from "common/clipboard";
|
||||
import Notify from "common/notify";
|
||||
import {Input, InputInvalid, ClickShort, ClickLong} from "common/input";
|
||||
import {ClickLong, ClickShort, Input, InputInvalid} from "common/input";
|
||||
|
||||
export default {
|
||||
name: 'PPageSubjects',
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/search"
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
"github.com/photoprism/photoprism/internal/workers"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
|
@ -25,8 +26,10 @@ const (
|
|||
accountFolder = "account-folder"
|
||||
)
|
||||
|
||||
// SearchAccounts finds accounts and returns them as JSON.
|
||||
//
|
||||
// GET /api/v1/accounts
|
||||
func GetAccounts(router *gin.RouterGroup) {
|
||||
func SearchAccounts(router *gin.RouterGroup) {
|
||||
router.GET("/accounts", func(c *gin.Context) {
|
||||
s := Auth(SessionID(c), acl.ResourceAccounts, acl.ActionSearch)
|
||||
|
||||
|
@ -51,7 +54,7 @@ func GetAccounts(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
result, err := query.AccountSearch(f)
|
||||
result, err := search.Accounts(f)
|
||||
|
||||
if err != nil {
|
||||
AbortBadRequest(c)
|
||||
|
@ -66,6 +69,8 @@ func GetAccounts(router *gin.RouterGroup) {
|
|||
})
|
||||
}
|
||||
|
||||
// GetAccount returns an account as JSON.
|
||||
//
|
||||
// GET /api/v1/accounts/:id
|
||||
//
|
||||
// Parameters:
|
||||
|
@ -96,6 +101,8 @@ func GetAccount(router *gin.RouterGroup) {
|
|||
})
|
||||
}
|
||||
|
||||
// GetAccountFolders returns folders that belong to an account as JSON.
|
||||
//
|
||||
// GET /api/v1/accounts/:id/folders
|
||||
//
|
||||
// Parameters:
|
||||
|
|
|
@ -10,10 +10,10 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetAccounts(t *testing.T) {
|
||||
func TestSearchAccounts(t *testing.T) {
|
||||
t.Run("successful request", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
GetAccounts(router)
|
||||
SearchAccounts(router)
|
||||
sess := AuthenticateAdmin(app, router)
|
||||
r := AuthenticatedRequest(app, "GET", "/api/v1/accounts?count=10", sess)
|
||||
val := gjson.Get(r.Body.String(), "#(AccName=\"Test Account\").AccURL")
|
||||
|
@ -24,7 +24,7 @@ func TestGetAccounts(t *testing.T) {
|
|||
})
|
||||
t.Run("invalid request", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
GetAccounts(router)
|
||||
SearchAccounts(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/accounts?xxx=10")
|
||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||
})
|
||||
|
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/photoprism/photoprism/internal/photoprism"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/search"
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
|
@ -41,7 +42,7 @@ func SaveAlbumAsYaml(a entity.Album) {
|
|||
}
|
||||
|
||||
// GET /api/v1/albums
|
||||
func GetAlbums(router *gin.RouterGroup) {
|
||||
func SearchAlbums(router *gin.RouterGroup) {
|
||||
router.GET("/albums", func(c *gin.Context) {
|
||||
s := Auth(SessionID(c), acl.ResourceAlbums, acl.ActionSearch)
|
||||
|
||||
|
@ -61,10 +62,10 @@ func GetAlbums(router *gin.RouterGroup) {
|
|||
|
||||
// Guest permissions are limited to shared albums.
|
||||
if s.Guest() {
|
||||
f.ID = s.Shares.Join(query.Or)
|
||||
f.ID = s.Shares.Join(txt.Or)
|
||||
}
|
||||
|
||||
result, err := query.AlbumSearch(f)
|
||||
result, err := search.Albums(f)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
|
@ -334,7 +335,7 @@ func CloneAlbums(router *gin.RouterGroup) {
|
|||
continue
|
||||
}
|
||||
|
||||
photos, err := query.AlbumPhotos(cloneAlbum, 10000)
|
||||
photos, err := search.AlbumPhotos(cloneAlbum, 10000)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("album: %s", err)
|
||||
|
@ -474,7 +475,7 @@ func DownloadAlbum(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
files, err := query.AlbumPhotos(a, 10000)
|
||||
files, err := search.AlbumPhotos(a, 10000)
|
||||
|
||||
if err != nil {
|
||||
AbortEntityNotFound(c)
|
||||
|
|
|
@ -5,15 +5,16 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetAlbums(t *testing.T) {
|
||||
func TestSearchAlbums(t *testing.T) {
|
||||
t.Run("successful request", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
GetAlbums(router)
|
||||
SearchAlbums(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/albums?count=10")
|
||||
count := gjson.Get(r.Body.String(), "#")
|
||||
assert.LessOrEqual(t, int64(3), count.Int())
|
||||
|
@ -21,7 +22,7 @@ func TestGetAlbums(t *testing.T) {
|
|||
})
|
||||
t.Run("invalid request", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
GetAlbums(router)
|
||||
SearchAlbums(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/albums?xxx=10")
|
||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||
})
|
||||
|
@ -112,7 +113,7 @@ func TestDeleteAlbum(t *testing.T) {
|
|||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
val := gjson.Get(r.Body.String(), "Slug")
|
||||
assert.Equal(t, "delete", val.String())
|
||||
GetAlbums(router)
|
||||
SearchAlbums(router)
|
||||
r2 := PerformRequest(app, "GET", "/api/v1/albums/"+uid)
|
||||
assert.Equal(t, http.StatusNotFound, r2.Code)
|
||||
})
|
||||
|
|
|
@ -180,7 +180,7 @@ func TestBatchLabelsDelete(t *testing.T) {
|
|||
app, router, _ := NewApiTest()
|
||||
|
||||
// Register routes.
|
||||
GetLabels(router)
|
||||
SearchLabels(router)
|
||||
BatchLabelsDelete(router)
|
||||
|
||||
r := PerformRequest(app, "GET", "/api/v1/labels?count=15")
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/search"
|
||||
)
|
||||
|
||||
type EntityEvent string
|
||||
|
@ -17,7 +17,7 @@ const (
|
|||
|
||||
func PublishPhotoEvent(e EntityEvent, uid string, c *gin.Context) {
|
||||
f := form.PhotoSearch{ID: uid, Merged: true}
|
||||
result, _, err := query.PhotoSearch(f)
|
||||
result, _, err := search.Photos(f)
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
|
@ -30,7 +30,7 @@ func PublishPhotoEvent(e EntityEvent, uid string, c *gin.Context) {
|
|||
|
||||
func PublishAlbumEvent(e EntityEvent, uid string, c *gin.Context) {
|
||||
f := form.AlbumSearch{ID: uid}
|
||||
result, err := query.AlbumSearch(f)
|
||||
result, err := search.Albums(f)
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
|
@ -43,7 +43,7 @@ func PublishAlbumEvent(e EntityEvent, uid string, c *gin.Context) {
|
|||
|
||||
func PublishLabelEvent(e EntityEvent, uid string, c *gin.Context) {
|
||||
f := form.LabelSearch{ID: uid}
|
||||
result, err := query.Labels(f)
|
||||
result, err := search.Labels(f)
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
|
@ -56,7 +56,7 @@ func PublishLabelEvent(e EntityEvent, uid string, c *gin.Context) {
|
|||
|
||||
func PublishSubjectEvent(e EntityEvent, uid string, c *gin.Context) {
|
||||
f := form.SubjectSearch{ID: uid}
|
||||
result, err := query.SubjectSearch(f)
|
||||
result, err := search.Subjects(f)
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
|
|
113
internal/api/face.go
Normal file
113
internal/api/face.go
Normal file
|
@ -0,0 +1,113 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/photoprism/photoprism/internal/search"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// SearchFaces finds and returns faces as JSON.
|
||||
//
|
||||
// GET /api/v1/faces
|
||||
func SearchFaces(router *gin.RouterGroup) {
|
||||
router.GET("/faces", func(c *gin.Context) {
|
||||
s := Auth(SessionID(c), acl.ResourceSubjects, acl.ActionSearch)
|
||||
|
||||
if s.Invalid() {
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
var f form.FaceSearch
|
||||
|
||||
err := c.MustBindWith(&f, binding.Form)
|
||||
|
||||
if err != nil {
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := search.Faces(f)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
AddCountHeader(c, len(result))
|
||||
AddLimitHeader(c, f.Count)
|
||||
AddOffsetHeader(c, f.Offset)
|
||||
AddTokenHeaders(c)
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
})
|
||||
}
|
||||
|
||||
// GetFace returns a face as JSON.
|
||||
//
|
||||
// GET /api/v1/faces/:id
|
||||
func GetFace(router *gin.RouterGroup) {
|
||||
router.GET("/faces/:id", func(c *gin.Context) {
|
||||
s := Auth(SessionID(c), acl.ResourceSubjects, acl.ActionRead)
|
||||
|
||||
if s.Invalid() {
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
f := form.FaceSearch{ID: c.Param("id"), Markers: true}
|
||||
|
||||
if results, err := search.Faces(f); err != nil || len(results) < 1 {
|
||||
Abort(c, http.StatusNotFound, i18n.ErrFaceNotFound)
|
||||
return
|
||||
} else {
|
||||
c.JSON(http.StatusOK, results[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateFace updates face properties.
|
||||
//
|
||||
// PUT /api/v1/faces/:id
|
||||
func UpdateFace(router *gin.RouterGroup) {
|
||||
router.PUT("/faces/:id", func(c *gin.Context) {
|
||||
s := Auth(SessionID(c), acl.ResourceSubjects, acl.ActionUpdate)
|
||||
|
||||
if s.Invalid() {
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
}
|
||||
|
||||
var f form.Face
|
||||
|
||||
if err := c.BindJSON(&f); err != nil {
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
|
||||
faceId := c.Param("id")
|
||||
m := entity.FindFace(faceId)
|
||||
|
||||
if m == nil {
|
||||
Abort(c, http.StatusNotFound, i18n.ErrFaceNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.SetSubjectUID(f.SubjUID); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
event.SuccessMsg(i18n.MsgPersonSaved)
|
||||
|
||||
c.JSON(http.StatusOK, m)
|
||||
})
|
||||
}
|
86
internal/api/face_test.go
Normal file
86
internal/api/face_test.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSearchFaces(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
SearchFaces(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/faces?count=15")
|
||||
count := gjson.Get(r.Body.String(), "#")
|
||||
assert.LessOrEqual(t, int64(4), count.Int())
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("InvalidRequest", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
SearchFaces(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/faces?xxx=15")
|
||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetFace(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
GetFace(router)
|
||||
// Example:
|
||||
// {"ID":"PN6QO5INYTUSAATOFL43LL2ABAV5ACZK","Src":"","SubjUID":"jqu0xs11qekk9jx8","Samples":5,"SampleRadius":0.8,"Collisions":1,"CollisionRadius":0,"MatchedAt":null,"CreatedAt":"2021-09-18T12:06:39Z","UpdatedAt":"2021-09-18T12:06:39Z"}
|
||||
r := PerformRequest(app, "GET", "/api/v1/faces/TOSCDXCS4VI3PGIUTCNIQCNI6HSFXQVZ")
|
||||
t.Logf("GET /api/v1/faces/TOSCDXCS4VI3PGIUTCNIQCNI6HSFXQVZ: %s", r.Body.String())
|
||||
val := gjson.Get(r.Body.String(), "ID")
|
||||
assert.Equal(t, "TOSCDXCS4VI3PGIUTCNIQCNI6HSFXQVZ", val.String())
|
||||
val2 := gjson.Get(r.Body.String(), "Samples")
|
||||
assert.LessOrEqual(t, int64(4), val2.Int())
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
|
||||
t.Run("Lowercase", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
GetFace(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/faces/PN6QO5INYTUSAATOFL43LL2ABAV5ACzk")
|
||||
val := gjson.Get(r.Body.String(), "ID")
|
||||
assert.Equal(t, "PN6QO5INYTUSAATOFL43LL2ABAV5ACZK", val.String())
|
||||
val2 := gjson.Get(r.Body.String(), "SubjUID")
|
||||
assert.Equal(t, "jqu0xs11qekk9jx8", val2.String())
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
GetFace(router)
|
||||
r := PerformRequestWithBody(app, "GET", "/api/v1/faces/xxx", `{"Name": "Updated01", "Priority": 4, "Uncertainty": 80}`)
|
||||
val := gjson.Get(r.Body.String(), "error")
|
||||
assert.Equal(t, "Face not found", val.String())
|
||||
assert.Equal(t, http.StatusNotFound, r.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateFace(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
UpdateFace(router)
|
||||
r := PerformRequestWithBody(app, "PUT", "/api/v1/faces/PN6QO5INYTUSAATOFL43LL2ABAV5ACzk", `{"SubjUID": "jqu0xs11qekk9jx8"}`)
|
||||
t.Logf("PUT /api/v1/faces/PN6QO5INYTUSAATOFL43LL2ABAV5ACzk: %s", r.Body.String())
|
||||
val := gjson.Get(r.Body.String(), "ID")
|
||||
assert.Equal(t, "PN6QO5INYTUSAATOFL43LL2ABAV5ACZK", val.String())
|
||||
val2 := gjson.Get(r.Body.String(), "SubjUID")
|
||||
assert.Equal(t, "jqu0xs11qekk9jx8", val2.String())
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
UpdateFace(router)
|
||||
r := PerformRequestWithBody(app, "PUT", "/api/v1/faces/xxx", `{"Name": "Updated01", "Priority": 4, "Uncertainty": 80}`)
|
||||
val := gjson.Get(r.Body.String(), "error")
|
||||
assert.Equal(t, "Face not found", val.String())
|
||||
assert.Equal(t, http.StatusNotFound, r.Code)
|
||||
})
|
||||
}
|
|
@ -11,13 +11,14 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/search"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// GetLabels finds and returns labels as JSON.
|
||||
// SearchLabels finds and returns labels as JSON.
|
||||
//
|
||||
// GET /api/v1/labels
|
||||
func GetLabels(router *gin.RouterGroup) {
|
||||
func SearchLabels(router *gin.RouterGroup) {
|
||||
router.GET("/labels", func(c *gin.Context) {
|
||||
s := Auth(SessionID(c), acl.ResourceLabels, acl.ActionSearch)
|
||||
|
||||
|
@ -35,7 +36,7 @@ func GetLabels(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
result, err := query.Labels(f)
|
||||
result, err := search.Labels(f)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
|
|
|
@ -9,10 +9,10 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetLabels(t *testing.T) {
|
||||
func TestSearchLabels(t *testing.T) {
|
||||
t.Run("successful request", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
GetLabels(router)
|
||||
SearchLabels(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/labels?count=15")
|
||||
count := gjson.Get(r.Body.String(), "#")
|
||||
assert.LessOrEqual(t, int64(4), count.Int())
|
||||
|
@ -20,7 +20,7 @@ func TestGetLabels(t *testing.T) {
|
|||
})
|
||||
t.Run("invalid request", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
GetLabels(router)
|
||||
SearchLabels(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/labels?xxx=15")
|
||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||
})
|
||||
|
@ -66,7 +66,7 @@ func TestLikeLabel(t *testing.T) {
|
|||
app, router, _ := NewApiTest()
|
||||
|
||||
// Register routes.
|
||||
GetLabels(router)
|
||||
SearchLabels(router)
|
||||
LikeLabel(router)
|
||||
|
||||
r2 := PerformRequest(app, "GET", "/api/v1/labels?count=3&q=like-label")
|
||||
|
@ -106,7 +106,7 @@ func TestDislikeLabel(t *testing.T) {
|
|||
app, router, _ := NewApiTest()
|
||||
|
||||
// Register routes.
|
||||
GetLabels(router)
|
||||
SearchLabels(router)
|
||||
DislikeLabel(router)
|
||||
|
||||
r2 := PerformRequest(app, "GET", "/api/v1/labels?count=3&q=landscape")
|
||||
|
|
|
@ -7,10 +7,10 @@ import (
|
|||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/search"
|
||||
)
|
||||
|
||||
// GetPhotos searches the pictures index and returns the result as JSON.
|
||||
// SearchPhotos searches the pictures index and returns the result as JSON.
|
||||
//
|
||||
// GET /api/v1/photos
|
||||
//
|
||||
|
@ -26,7 +26,7 @@ import (
|
|||
// before: date Find photos taken before (format: "2006-01-02")
|
||||
// after: date Find photos taken after (format: "2006-01-02")
|
||||
// favorite: bool Find favorites only
|
||||
func GetPhotos(router *gin.RouterGroup) {
|
||||
func SearchPhotos(router *gin.RouterGroup) {
|
||||
router.GET("/photos", func(c *gin.Context) {
|
||||
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionSearch)
|
||||
|
||||
|
@ -58,7 +58,7 @@ func GetPhotos(router *gin.RouterGroup) {
|
|||
f.Review = false
|
||||
}
|
||||
|
||||
result, count, err := query.PhotoSearch(f)
|
||||
result, count, err := search.Photos(f)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("search: %s", err)
|
||||
|
|
|
@ -5,20 +5,20 @@ import (
|
|||
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/search"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
|
||||
geojson "github.com/paulmach/go.geojson"
|
||||
)
|
||||
|
||||
// GetGeo performs a geo search with reduced metadata details for improved performance.
|
||||
// SearchPhotosGeo performs a geo search with reduced metadata details for improved performance.
|
||||
//
|
||||
// GET /api/v1/geo
|
||||
func GetGeo(router *gin.RouterGroup) {
|
||||
func SearchPhotosGeo(router *gin.RouterGroup) {
|
||||
router.GET("/geo", func(c *gin.Context) {
|
||||
s := Auth(SessionID(c), acl.ResourcePhotos, acl.ActionSearch)
|
||||
|
||||
|
@ -36,7 +36,7 @@ func GetGeo(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
photos, err := query.Geo(f)
|
||||
photos, err := search.PhotosGeo(f)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("search: %s", err)
|
|
@ -7,11 +7,11 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetGeo(t *testing.T) {
|
||||
t.Run("get geo", func(t *testing.T) {
|
||||
func TestSearchPhotosGeo(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
|
||||
GetGeo(router)
|
||||
SearchPhotosGeo(router)
|
||||
|
||||
result := PerformRequest(app, "GET", "/api/v1/geo")
|
||||
assert.Equal(t, http.StatusOK, result.Code)
|
|
@ -9,20 +9,19 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetPhotos(t *testing.T) {
|
||||
t.Run("successful request", func(t *testing.T) {
|
||||
func TestSearchPhotos(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
|
||||
GetPhotos(router)
|
||||
SearchPhotos(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/photos?count=10")
|
||||
count := gjson.Get(r.Body.String(), "#")
|
||||
assert.LessOrEqual(t, int64(2), count.Int())
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
|
||||
t.Run("invalid request", func(t *testing.T) {
|
||||
t.Run("InvalidRequest", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
GetPhotos(router)
|
||||
SearchPhotos(router)
|
||||
result := PerformRequest(app, "GET", "/api/v1/photos?xxx=10")
|
||||
assert.Equal(t, http.StatusBadRequest, result.Code)
|
||||
})
|
||||
|
|
|
@ -14,7 +14,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/photoprism"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/search"
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
"github.com/photoprism/photoprism/internal/thumb"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
|
@ -77,7 +77,7 @@ func SharePreview(router *gin.RouterGroup) {
|
|||
f.Count = 12
|
||||
f.Order = "relevance"
|
||||
|
||||
p, count, err := query.PhotoSearch(f)
|
||||
p, count, err := search.Photos(f)
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
|
|
|
@ -10,14 +10,14 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/search"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// GetSubjects finds and returns subjects as JSON.
|
||||
// SearchSubjects finds and returns subjects as JSON.
|
||||
//
|
||||
// GET /api/v1/subjects
|
||||
func GetSubjects(router *gin.RouterGroup) {
|
||||
func SearchSubjects(router *gin.RouterGroup) {
|
||||
router.GET("/subjects", func(c *gin.Context) {
|
||||
s := Auth(SessionID(c), acl.ResourceSubjects, acl.ActionSearch)
|
||||
|
||||
|
@ -35,7 +35,7 @@ func GetSubjects(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
result, err := query.SubjectSearch(f)
|
||||
result, err := search.Subjects(f)
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})
|
||||
|
@ -104,7 +104,11 @@ func UpdateSubject(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
event.SuccessMsg(i18n.MsgSubjectSaved)
|
||||
if m.IsPerson() {
|
||||
event.SuccessMsg(i18n.MsgPersonSaved)
|
||||
} else {
|
||||
event.SuccessMsg(i18n.MsgSubjectSaved)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, m)
|
||||
})
|
||||
|
|
|
@ -9,25 +9,25 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetSubjects(t *testing.T) {
|
||||
t.Run("successful request", func(t *testing.T) {
|
||||
func TestSearchSubjects(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
GetSubjects(router)
|
||||
SearchSubjects(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/subjects?count=10")
|
||||
count := gjson.Get(r.Body.String(), "#")
|
||||
assert.LessOrEqual(t, int64(3), count.Int())
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("invalid request", func(t *testing.T) {
|
||||
t.Run("InvalidRequest", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
GetSubjects(router)
|
||||
SearchSubjects(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/subjects?xxx=10")
|
||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetSubject(t *testing.T) {
|
||||
t.Run("successful request", func(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
GetSubject(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/subjects/jqy1y111h1njaaaa")
|
||||
|
@ -35,7 +35,7 @@ func TestGetSubject(t *testing.T) {
|
|||
assert.Equal(t, "dangling-subject", val.String())
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("invalid request", func(t *testing.T) {
|
||||
t.Run("InvalidRequest", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
GetSubject(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/subjects/xxx1y111h1njaaaa")
|
||||
|
@ -46,13 +46,13 @@ func TestGetSubject(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLikeSubject(t *testing.T) {
|
||||
t.Run("like not existing subject", func(t *testing.T) {
|
||||
t.Run("InvalidSubject", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
LikeSubject(router)
|
||||
r := PerformRequest(app, "POST", "/api/v1/subjects/8775789/like")
|
||||
assert.Equal(t, http.StatusNotFound, r.Code)
|
||||
})
|
||||
t.Run("like existing subject", func(t *testing.T) {
|
||||
t.Run("ExistingSubject", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
|
||||
// Register routes.
|
||||
|
@ -79,13 +79,13 @@ func TestLikeSubject(t *testing.T) {
|
|||
})
|
||||
}
|
||||
func TestDislikeSubject(t *testing.T) {
|
||||
t.Run("dislike not existing subject", func(t *testing.T) {
|
||||
t.Run("InvalidSubject", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
DislikeSubject(router)
|
||||
r := PerformRequest(app, "DELETE", "/api/v1/subjects/8775789/like")
|
||||
assert.Equal(t, http.StatusNotFound, r.Code)
|
||||
})
|
||||
t.Run("dislike existing subject", func(t *testing.T) {
|
||||
t.Run("ExistingSubject", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
|
||||
// Register routes.
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/base32"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
@ -255,6 +256,7 @@ func (m *Face) SetSubjectUID(subjUID string) (err error) {
|
|||
Where("face_id = ?", m.ID).
|
||||
Where("subj_src = ?", SrcAuto).
|
||||
Where("subj_uid <> ?", m.SubjUID).
|
||||
Where("marker_invalid = 0").
|
||||
Updates(Values{"SubjUID": m.SubjUID, "MarkerReview": false}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -329,14 +331,11 @@ func FindFace(id string) *Face {
|
|||
return nil
|
||||
}
|
||||
|
||||
result := Face{}
|
||||
f := Face{}
|
||||
|
||||
db := Db()
|
||||
db = db.Where("id = ?", id)
|
||||
|
||||
if err := db.First(&result).Error; err != nil {
|
||||
if err := Db().Where("id = ?", strings.ToUpper(id)).First(&f).Error; err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &result
|
||||
return &f
|
||||
}
|
||||
|
|
|
@ -525,6 +525,20 @@ func FindMarker(uid string) *Marker {
|
|||
return &result
|
||||
}
|
||||
|
||||
// FindFaceMarker finds the best marker for a given face
|
||||
func FindFaceMarker(faceId string) *Marker {
|
||||
var result Marker
|
||||
|
||||
if err := Db().Where("face_id = ?", faceId).
|
||||
Where("file_hash <> '' AND marker_invalid = 0").
|
||||
Order("q DESC").First(&result).Error; err != nil {
|
||||
log.Warnf("face: no marker for %s", txt.Quote(faceId))
|
||||
return nil
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
// UpdateOrCreateMarker updates a marker in the database or creates a new one if needed.
|
||||
func UpdateOrCreateMarker(m *Marker) (*Marker, error) {
|
||||
const d = 0.07
|
||||
|
|
|
@ -27,6 +27,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"1000003-1": Marker{ //Photo04
|
||||
MarkerUID: "mqu0xs11qekk9jx8",
|
||||
FileUID: "ft2es39w45bnlqdw",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
SubjUID: "jqu0xs11qekk9jx8",
|
||||
MarkerSrc: SrcImage,
|
||||
MarkerType: MarkerLabel,
|
||||
|
@ -40,6 +41,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"1000003-2": Marker{ //Photo04
|
||||
MarkerUID: "mt9k3pw1wowuy3c3",
|
||||
FileUID: "ft2es39w45bnlqdw",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
SubjUID: "lt9k3pw1wowuy3c3",
|
||||
FaceID: "LRG2HJBDZE66LYG7Q5SRFXO2MDTOES52",
|
||||
MarkerName: "Unknown",
|
||||
|
@ -55,6 +57,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"1000003-3": Marker{ //Photo04
|
||||
MarkerUID: "mt9k3pw1wowuy111",
|
||||
FileUID: "ft2es39w45bnlqdw",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
SubjUID: "",
|
||||
MarkerSrc: SrcImage,
|
||||
MarkerType: MarkerLabel,
|
||||
|
@ -69,6 +72,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"1000003-4": Marker{ //Photo04
|
||||
MarkerUID: "mt9k3pw1wowuy222",
|
||||
FileUID: "ft2es39w45bnlqdw",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
SubjUID: "",
|
||||
MarkerSrc: SrcImage,
|
||||
MarkerType: MarkerFace,
|
||||
|
@ -85,6 +89,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"1000003-5": Marker{ //Photo04
|
||||
MarkerUID: "mt9k3pw1wowuy333",
|
||||
FileUID: "ft2es39w45bnlqdw",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
FaceID: FaceFixtures.Get("unknown").ID,
|
||||
SubjUID: "",
|
||||
SubjSrc: SrcAuto,
|
||||
|
@ -103,6 +108,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"1000003-6": Marker{ //Photo04
|
||||
MarkerUID: "mt9k3pw1wowuy444",
|
||||
FileUID: "ft2es39w45bnlqdw",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
FaceID: FaceFixtures.Get("john-doe").ID,
|
||||
FaceDist: 0.2,
|
||||
SubjSrc: SrcAuto,
|
||||
|
@ -122,6 +128,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"ma-ba-1": Marker{ //Photo27
|
||||
MarkerUID: "mt9k3pw1wowuy555",
|
||||
FileUID: "ft2es49qhhinlple",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
FaceID: FaceFixtures.Get("fa-gr").ID,
|
||||
FaceDist: 0.5,
|
||||
SubjSrc: "",
|
||||
|
@ -141,6 +148,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"fa-gr-1": Marker{ //Photo27
|
||||
MarkerUID: "mt9k3pw1wowuy666",
|
||||
FileUID: "ft2es49qhhinlple",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
FaceID: FaceFixtures.Get("fa-gr").ID,
|
||||
FaceDist: 0.6,
|
||||
SubjSrc: SrcAuto,
|
||||
|
@ -160,6 +168,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"fa-gr-2": Marker{ //Photo03
|
||||
MarkerUID: "mt9k3pw1wowuy777",
|
||||
FileUID: "ft2es49w15bnlqdw",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
FaceID: FaceFixtures.Get("fa-gr").ID,
|
||||
FaceDist: 0.6,
|
||||
SubjSrc: SrcAuto,
|
||||
|
@ -179,6 +188,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"fa-gr-3": Marker{ //19800101_000002_D640C559
|
||||
MarkerUID: "mt9k3pw1wowuy888",
|
||||
FileUID: "ft8es39w45bnlqdw",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
FaceID: FaceFixtures.Get("fa-gr").ID,
|
||||
FaceDist: 0.6,
|
||||
SubjSrc: SrcAuto,
|
||||
|
@ -198,6 +208,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"actress-a-1": Marker{ //Photo27
|
||||
MarkerUID: "mt9k3pw1wowuy999",
|
||||
FileUID: "ft2es49qhhinlple",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
FaceID: FaceFixtures.Get("actress-1").ID,
|
||||
CropArea: "045038063041",
|
||||
FaceDist: 0.26852392873736236,
|
||||
|
@ -218,6 +229,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"actress-a-2": Marker{ //Photo03 - non primary file
|
||||
MarkerUID: "mt9k3pw1wowu1000",
|
||||
FileUID: "ft2es49whhbnlqdn",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
FaceID: FaceFixtures.Get("actress-1").ID,
|
||||
CropArea: "046045043065",
|
||||
FaceDist: 0.4507357278575355,
|
||||
|
@ -238,6 +250,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"actress-a-3": Marker{ //19800101_000002_D640C559
|
||||
MarkerUID: "mt9k3pw1wowu1001",
|
||||
FileUID: "ft8es39w45bnlqdw",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
FaceID: FaceFixtures.Get("actress-1").ID,
|
||||
CropArea: "05403304060446",
|
||||
FaceDist: 0.5099754448545762,
|
||||
|
@ -258,6 +271,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"actor-a-1": Marker{ //Photo05
|
||||
MarkerUID: "mt9k3pw1wowu1002",
|
||||
FileUID: "ft3es39w45bnlqdw",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
FaceID: FaceFixtures.Get("actor-1").ID,
|
||||
FaceDist: 0.5223304453393212,
|
||||
SubjSrc: "",
|
||||
|
@ -277,6 +291,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"actor-a-2": Marker{ //Photo02
|
||||
MarkerUID: "mt9k3pw1wowu1003",
|
||||
FileUID: "ft2es39q45bnlqd0",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
FaceID: FaceFixtures.Get("actor-1").ID,
|
||||
FaceDist: 0.5088545446490167,
|
||||
SubjSrc: "",
|
||||
|
@ -296,6 +311,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"actor-a-3": Marker{ //Photo10
|
||||
MarkerUID: "mt9k3pw1wowu1004",
|
||||
FileUID: "fikjs39w45bnlqdw",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
FaceID: FaceFixtures.Get("actor-1").ID,
|
||||
FaceDist: 0.3139983399779298,
|
||||
SubjSrc: "",
|
||||
|
@ -315,6 +331,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"actor-a-4": Marker{ //19800101_000002_D640C559
|
||||
MarkerUID: "mt9k3pw1wowu1005",
|
||||
FileUID: "ft8es39w45bnlqdw",
|
||||
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
|
||||
FaceID: FaceFixtures.Get("actor-1").ID,
|
||||
FaceDist: 0.3139983399779298,
|
||||
SubjSrc: "",
|
||||
|
|
14
internal/form/face.go
Normal file
14
internal/form/face.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package form
|
||||
|
||||
import "github.com/ulule/deepcopier"
|
||||
|
||||
// Face represents a face edit form.
|
||||
type Face struct {
|
||||
SubjUID string `json:"SubjUID"`
|
||||
}
|
||||
|
||||
func NewFace(m interface{}) (f Face, err error) {
|
||||
err = deepcopier.Copy(m).To(&f)
|
||||
|
||||
return f, err
|
||||
}
|
29
internal/form/face_search.go
Normal file
29
internal/form/face_search.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package form
|
||||
|
||||
// FaceSearch represents search form fields for "/api/v1/faces".
|
||||
type FaceSearch struct {
|
||||
Query string `form:"q"`
|
||||
ID string `form:"id"`
|
||||
Subject string `form:"subject"`
|
||||
Unknown bool `form:"unknown"`
|
||||
Markers bool `form:"markers"`
|
||||
Count int `form:"count" binding:"required" serialize:"-"`
|
||||
Offset int `form:"offset" serialize:"-"`
|
||||
Order string `form:"order" serialize:"-"`
|
||||
}
|
||||
|
||||
func (f *FaceSearch) GetQuery() string {
|
||||
return f.Query
|
||||
}
|
||||
|
||||
func (f *FaceSearch) SetQuery(q string) {
|
||||
f.Query = q
|
||||
}
|
||||
|
||||
func (f *FaceSearch) ParseQueryString() error {
|
||||
return ParseQueryString(f)
|
||||
}
|
||||
|
||||
func NewFaceSearch(query string) FaceSearch {
|
||||
return FaceSearch{Query: query}
|
||||
}
|
25
internal/form/face_test.go
Normal file
25
internal/form/face_test.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package form
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewFace(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
var m = struct {
|
||||
SubjUID string `json:"SubjUID"`
|
||||
}{
|
||||
SubjUID: "jqzmd5q3b8o2yxu7",
|
||||
}
|
||||
|
||||
f, err := NewFace(m)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "jqzmd5q3b8o2yxu7", f.SubjUID)
|
||||
})
|
||||
}
|
|
@ -25,6 +25,7 @@ type GeoSearch struct {
|
|||
S2 string `form:"s2"`
|
||||
Olc string `form:"olc"`
|
||||
Dist uint `form:"dist"`
|
||||
Face string `form:"face"` // UIDs
|
||||
Subject string `form:"subject"` // UIDs
|
||||
Subjects string `form:"subjects"` // Text
|
||||
People string `form:"people"` // Alias for Subjects
|
||||
|
|
|
@ -42,12 +42,7 @@ type PhotoSearch struct {
|
|||
Mono bool `form:"mono"`
|
||||
Portrait bool `form:"portrait"`
|
||||
Geo bool `form:"geo"`
|
||||
Subject string `form:"subject"` // UIDs
|
||||
Subjects string `form:"subjects"` // Text
|
||||
People string `form:"people"` // Alias for Subjects
|
||||
Keywords string `form:"keywords"`
|
||||
Album string `form:"album"` // UIDs
|
||||
Albums string `form:"albums"` // Text
|
||||
Label string `form:"label"`
|
||||
Category string `form:"category"` // Moments
|
||||
Country string `form:"country"` // Moments
|
||||
|
@ -55,6 +50,12 @@ type PhotoSearch struct {
|
|||
Year int `form:"year"` // Moments
|
||||
Month int `form:"month"` // Moments
|
||||
Day int `form:"day"` // Moments
|
||||
Face string `form:"face"` // UIDs
|
||||
Subject string `form:"subject"` // UIDs
|
||||
Subjects string `form:"subjects"` // Text
|
||||
People string `form:"people"` // Alias for Subjects
|
||||
Album string `form:"album"` // UIDs
|
||||
Albums string `form:"albums"` // Text
|
||||
Color string `form:"color"`
|
||||
Faces string `form:"faces"` // Find or exclude faces if detected.
|
||||
Quality int `form:"quality"`
|
||||
|
|
|
@ -15,6 +15,8 @@ const (
|
|||
ErrLabelNotFound
|
||||
ErrAlbumNotFound
|
||||
ErrSubjectNotFound
|
||||
ErrPersonNotFound
|
||||
ErrFaceNotFound
|
||||
ErrPublic
|
||||
ErrReadOnly
|
||||
ErrUnauthorized
|
||||
|
@ -62,6 +64,8 @@ const (
|
|||
MsgLabelSaved
|
||||
MsgSubjectSaved
|
||||
MsgSubjectDeleted
|
||||
MsgPersonSaved
|
||||
MsgPersonDeleted
|
||||
MsgFilesUploadedIn
|
||||
MsgSelectionApproved
|
||||
MsgSelectionArchived
|
||||
|
@ -79,15 +83,17 @@ var Messages = MessageMap{
|
|||
ErrSaveFailed: gettext("Changes could not be saved"),
|
||||
ErrDeleteFailed: gettext("Could not be deleted"),
|
||||
ErrAlreadyExists: gettext("%s already exists"),
|
||||
ErrNotFound: gettext("Not found on server, deleted?"),
|
||||
ErrNotFound: gettext("Not found"),
|
||||
ErrFileNotFound: gettext("File not found"),
|
||||
ErrSelectionNotFound: gettext("Selection not found"),
|
||||
ErrEntityNotFound: gettext("Not found on server, deleted?"),
|
||||
ErrEntityNotFound: gettext("Entity not found"),
|
||||
ErrAccountNotFound: gettext("Account not found"),
|
||||
ErrUserNotFound: gettext("User not found"),
|
||||
ErrLabelNotFound: gettext("Label not found"),
|
||||
ErrAlbumNotFound: gettext("Album not found"),
|
||||
ErrSubjectNotFound: gettext("Subject not found"),
|
||||
ErrPersonNotFound: gettext("Person not found"),
|
||||
ErrFaceNotFound: gettext("Face not found"),
|
||||
ErrPublic: gettext("Not available in public mode"),
|
||||
ErrReadOnly: gettext("not available in read-only mode"),
|
||||
ErrUnauthorized: gettext("Please log in and try again"),
|
||||
|
@ -136,6 +142,8 @@ var Messages = MessageMap{
|
|||
MsgLabelSaved: gettext("Label saved"),
|
||||
MsgSubjectSaved: gettext("Subject saved"),
|
||||
MsgSubjectDeleted: gettext("Subject deleted"),
|
||||
MsgPersonSaved: gettext("Person saved"),
|
||||
MsgPersonDeleted: gettext("Person deleted"),
|
||||
MsgFilesUploadedIn: gettext("%d files uploaded in %d s"),
|
||||
MsgSelectionApproved: gettext("Selection approved"),
|
||||
MsgSelectionArchived: gettext("Selection archived"),
|
||||
|
|
|
@ -20,7 +20,7 @@ func BackupAlbums(backupPath string, force bool) (count int, result error) {
|
|||
return count, nil
|
||||
}
|
||||
|
||||
albums, err := query.GetAlbums(0, 9999)
|
||||
albums, err := query.Albums(0, 9999)
|
||||
|
||||
if err != nil {
|
||||
return count, err
|
||||
|
@ -54,7 +54,7 @@ func RestoreAlbums(backupPath string, force bool) (count int, result error) {
|
|||
return count, nil
|
||||
}
|
||||
|
||||
existing, err := query.GetAlbums(0, 1)
|
||||
existing, err := query.Albums(0, 1)
|
||||
|
||||
if err != nil {
|
||||
return count, err
|
||||
|
|
|
@ -203,7 +203,7 @@ func (w *Moments) Start() (err error) {
|
|||
} else {
|
||||
w := txt.Words(f.Label)
|
||||
w = append(w, mom.Label)
|
||||
f.Label = strings.Join(txt.UniqueWords(w), query.Or)
|
||||
f.Label = strings.Join(txt.UniqueWords(w), txt.Or)
|
||||
}
|
||||
|
||||
if err := a.Update("AlbumFilter", f.Serialize()); err != nil {
|
||||
|
|
14
internal/query/account.go
Normal file
14
internal/query/account.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package query
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
)
|
||||
|
||||
// AccountByID finds an account by primary key.
|
||||
func AccountByID(id uint) (result entity.Account, err error) {
|
||||
if err := Db().Where("id = ?", id).First(&result).Error; err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
29
internal/query/account_test.go
Normal file
29
internal/query/account_test.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package query
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAccountByID(t *testing.T) {
|
||||
t.Run("existing account", func(t *testing.T) {
|
||||
r, err := AccountByID(uint(1000001))
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "Test Account2", r.AccName)
|
||||
|
||||
})
|
||||
t.Run("record not found", func(t *testing.T) {
|
||||
r, err := AccountByID(uint(123))
|
||||
|
||||
if err == nil {
|
||||
t.Fatal()
|
||||
}
|
||||
assert.Equal(t, "record not found", err.Error())
|
||||
assert.Empty(t, r)
|
||||
})
|
||||
}
|
|
@ -2,49 +2,19 @@ package query
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/capture"
|
||||
"github.com/photoprism/photoprism/internal/search"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// AlbumResult contains found albums
|
||||
type AlbumResult struct {
|
||||
ID uint `json:"-"`
|
||||
AlbumUID string `json:"UID"`
|
||||
ParentUID string `json:"ParentUID"`
|
||||
Thumb string `json:"Thumb"`
|
||||
ThumbSrc string `json:"ThumbSrc"`
|
||||
AlbumSlug string `json:"Slug"`
|
||||
AlbumType string `json:"Type"`
|
||||
AlbumTitle string `json:"Title"`
|
||||
AlbumLocation string `json:"Location"`
|
||||
AlbumCategory string `json:"Category"`
|
||||
AlbumCaption string `json:"Caption"`
|
||||
AlbumDescription string `json:"Description"`
|
||||
AlbumNotes string `json:"Notes"`
|
||||
AlbumFilter string `json:"Filter"`
|
||||
AlbumOrder string `json:"Order"`
|
||||
AlbumTemplate string `json:"Template"`
|
||||
AlbumPath string `json:"Path"`
|
||||
AlbumCountry string `json:"Country"`
|
||||
AlbumYear int `json:"Year"`
|
||||
AlbumMonth int `json:"Month"`
|
||||
AlbumDay int `json:"Day"`
|
||||
AlbumFavorite bool `json:"Favorite"`
|
||||
AlbumPrivate bool `json:"Private"`
|
||||
PhotoCount int `json:"PhotoCount"`
|
||||
LinkCount int `json:"LinkCount"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
DeletedAt time.Time `json:"DeletedAt,omitempty"`
|
||||
// Albums returns a slice of albums.
|
||||
func Albums(offset, limit int) (results entity.Albums, err error) {
|
||||
err = UnscopedDb().Table("albums").Select("*").Offset(offset).Limit(limit).Find(&results).Error
|
||||
return results, err
|
||||
}
|
||||
|
||||
type AlbumResults []AlbumResult
|
||||
|
||||
// AlbumByUID returns a Album based on the UID.
|
||||
func AlbumByUID(albumUID string) (album entity.Album, err error) {
|
||||
if err := Db().Where("album_uid = ?", albumUID).First(&album).Error; err != nil {
|
||||
|
@ -63,7 +33,7 @@ func AlbumCoverByUID(uid string) (file entity.File, err error) {
|
|||
} else if a.AlbumType != entity.AlbumDefault { // TODO: Optimize
|
||||
f := form.PhotoSearch{Album: a.AlbumUID, Filter: a.AlbumFilter, Order: entity.SortOrderRelevance, Count: 1, Offset: 0, Merged: false}
|
||||
|
||||
if photos, _, err := PhotoSearch(f); err != nil {
|
||||
if photos, _, err := search.Photos(f); err != nil {
|
||||
return file, err
|
||||
} else if len(photos) > 0 {
|
||||
for _, photo := range photos {
|
||||
|
@ -99,103 +69,6 @@ func AlbumCoverByUID(uid string) (file entity.File, err error) {
|
|||
return file, nil
|
||||
}
|
||||
|
||||
// AlbumPhotos returns up to count photos from an album.
|
||||
func AlbumPhotos(a entity.Album, count int) (results PhotoResults, err error) {
|
||||
results, _, err = PhotoSearch(form.PhotoSearch{
|
||||
Album: a.AlbumUID,
|
||||
Filter: a.AlbumFilter,
|
||||
Count: count,
|
||||
Offset: 0,
|
||||
})
|
||||
|
||||
return results, err
|
||||
}
|
||||
|
||||
// AlbumSearch searches albums based on their name.
|
||||
func AlbumSearch(f form.AlbumSearch) (results AlbumResults, err error) {
|
||||
if err := f.ParseQueryString(); err != nil {
|
||||
return results, err
|
||||
}
|
||||
|
||||
defer log.Debug(capture.Time(time.Now(), fmt.Sprintf("albums: search %s", form.Serialize(f, true))))
|
||||
|
||||
// Base query.
|
||||
s := UnscopedDb().Table("albums").
|
||||
Select("albums.*, cp.photo_count, cl.link_count").
|
||||
Joins("LEFT JOIN (SELECT album_uid, count(photo_uid) AS photo_count FROM photos_albums WHERE hidden = 0 AND missing = 0 GROUP BY album_uid) AS cp ON cp.album_uid = albums.album_uid").
|
||||
Joins("LEFT JOIN (SELECT share_uid, count(share_uid) AS link_count FROM links GROUP BY share_uid) AS cl ON cl.share_uid = albums.album_uid").
|
||||
Where("albums.album_type <> 'folder' OR albums.album_path IN (SELECT photo_path FROM photos WHERE photo_private = 0 AND photo_quality > -1 AND deleted_at IS NULL)").
|
||||
Where("albums.deleted_at IS NULL")
|
||||
|
||||
// Limit result count.
|
||||
if f.Count > 0 && f.Count <= MaxResults {
|
||||
s = s.Limit(f.Count).Offset(f.Offset)
|
||||
} else {
|
||||
s = s.Limit(MaxResults).Offset(f.Offset)
|
||||
}
|
||||
|
||||
// Set sort order.
|
||||
switch f.Order {
|
||||
case "slug":
|
||||
s = s.Order("albums.album_favorite DESC, album_slug ASC")
|
||||
default:
|
||||
s = s.Order("albums.album_favorite DESC, albums.album_year DESC, albums.album_month DESC, albums.album_day DESC, albums.album_title, albums.created_at DESC")
|
||||
}
|
||||
|
||||
if f.ID != "" {
|
||||
s = s.Where("albums.album_uid IN (?)", strings.Split(f.ID, Or))
|
||||
|
||||
if result := s.Scan(&results); result.Error != nil {
|
||||
return results, result.Error
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
if f.Query != "" {
|
||||
likeString := "%" + f.Query + "%"
|
||||
s = s.Where("albums.album_title LIKE ? OR albums.album_location LIKE ?", likeString, likeString)
|
||||
}
|
||||
|
||||
if f.Type != "" {
|
||||
s = s.Where("albums.album_type IN (?)", strings.Split(f.Type, Or))
|
||||
}
|
||||
|
||||
if f.Category != "" {
|
||||
s = s.Where("albums.album_category IN (?)", strings.Split(f.Category, Or))
|
||||
}
|
||||
|
||||
if f.Location != "" {
|
||||
s = s.Where("albums.album_location IN (?)", strings.Split(f.Location, Or))
|
||||
}
|
||||
|
||||
if f.Country != "" {
|
||||
s = s.Where("albums.album_country IN (?)", strings.Split(f.Country, Or))
|
||||
}
|
||||
|
||||
if f.Favorite {
|
||||
s = s.Where("albums.album_favorite = 1")
|
||||
}
|
||||
|
||||
if (f.Year > 0 && f.Year <= txt.YearMax) || f.Year == entity.UnknownYear {
|
||||
s = s.Where("albums.album_year = ?", f.Year)
|
||||
}
|
||||
|
||||
if (f.Month >= txt.MonthMin && f.Month <= txt.MonthMax) || f.Month == entity.UnknownMonth {
|
||||
s = s.Where("albums.album_month = ?", f.Month)
|
||||
}
|
||||
|
||||
if (f.Day >= txt.DayMin && f.Month <= txt.DayMax) || f.Day == entity.UnknownDay {
|
||||
s = s.Where("albums.album_day = ?", f.Day)
|
||||
}
|
||||
|
||||
if result := s.Scan(&results); result.Error != nil {
|
||||
return results, result.Error
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// UpdateAlbumDates updates album year, month and day based on indexed photo metadata.
|
||||
func UpdateAlbumDates() error {
|
||||
switch DbDialect() {
|
||||
|
@ -228,9 +101,3 @@ func AlbumEntryFound(uid string) error {
|
|||
return UnscopedDb().Exec(`UPDATE photos_albums SET missing = 0 WHERE photo_uid = ?`, uid).Error
|
||||
}
|
||||
}
|
||||
|
||||
// GetAlbums returns a slice of albums.
|
||||
func GetAlbums(offset, limit int) (results entity.Albums, err error) {
|
||||
err = UnscopedDb().Table("albums").Select("*").Offset(offset).Limit(limit).Find(&results).Error
|
||||
return results, err
|
||||
}
|
||||
|
|
|
@ -3,8 +3,6 @@ package query
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
form "github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
@ -61,152 +59,6 @@ func TestAlbumCoverByUID(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestAlbumPhotos(t *testing.T) {
|
||||
t.Run("search with string", func(t *testing.T) {
|
||||
results, err := AlbumPhotos(entity.AlbumFixtures.Get("april-1990"), 2)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(results) < 2 {
|
||||
t.Errorf("at least 2 results expected: %d", len(results))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAlbumSearch(t *testing.T) {
|
||||
t.Run("search with string", func(t *testing.T) {
|
||||
query := form.NewAlbumSearch("chr")
|
||||
result, err := AlbumSearch(query)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "Christmas 2030", result[0].AlbumTitle)
|
||||
})
|
||||
|
||||
t.Run("search with slug", func(t *testing.T) {
|
||||
query := form.NewAlbumSearch("slug:holiday count:10")
|
||||
result, err := AlbumSearch(query)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "Holiday 2030", result[0].AlbumTitle)
|
||||
})
|
||||
|
||||
t.Run("search with country", func(t *testing.T) {
|
||||
query := form.NewAlbumSearch("country:ca")
|
||||
result, err := AlbumSearch(query)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "April 1990", result[0].AlbumTitle)
|
||||
})
|
||||
|
||||
t.Run("favorites true", func(t *testing.T) {
|
||||
query := form.NewAlbumSearch("favorite:true count:10000")
|
||||
|
||||
result, err := AlbumSearch(query)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "Holiday 2030", result[0].AlbumTitle)
|
||||
})
|
||||
t.Run("empty query", func(t *testing.T) {
|
||||
query := form.NewAlbumSearch("order:slug")
|
||||
|
||||
results, err := AlbumSearch(query)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(results) < 3 {
|
||||
t.Errorf("at least 3 results expected: %d", len(results))
|
||||
}
|
||||
})
|
||||
t.Run("search with invalid query string", func(t *testing.T) {
|
||||
query := form.NewAlbumSearch("xxx:bla")
|
||||
result, err := AlbumSearch(query)
|
||||
assert.Error(t, err, "unknown filter")
|
||||
t.Log(result)
|
||||
})
|
||||
t.Run("search with invalid query string", func(t *testing.T) {
|
||||
query := form.NewAlbumSearch("xxx:bla")
|
||||
result, err := AlbumSearch(query)
|
||||
assert.Error(t, err, "unknown filter")
|
||||
t.Log(result)
|
||||
})
|
||||
t.Run("search for existing ID", func(t *testing.T) {
|
||||
f := form.AlbumSearch{
|
||||
Query: "",
|
||||
ID: "at9lxuqxpogaaba7",
|
||||
Slug: "",
|
||||
Title: "",
|
||||
Favorite: false,
|
||||
Count: 0,
|
||||
Offset: 0,
|
||||
Order: "",
|
||||
}
|
||||
|
||||
result, err := AlbumSearch(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 1, len(result))
|
||||
assert.Equal(t, "christmas-2030", result[0].AlbumSlug)
|
||||
})
|
||||
t.Run("search with multiple filters", func(t *testing.T) {
|
||||
f := form.AlbumSearch{
|
||||
Query: "",
|
||||
Type: "moment",
|
||||
Category: "Fun",
|
||||
Location: "Favorite Park",
|
||||
Title: "Empty Moment",
|
||||
Count: 0,
|
||||
Offset: 0,
|
||||
Order: "",
|
||||
}
|
||||
|
||||
result, err := AlbumSearch(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 1, len(result))
|
||||
assert.Equal(t, "Empty Moment", result[0].AlbumTitle)
|
||||
})
|
||||
t.Run("search for year/month/day", func(t *testing.T) {
|
||||
f := form.AlbumSearch{
|
||||
Year: 2021,
|
||||
Month: 10,
|
||||
Day: 3,
|
||||
Count: 0,
|
||||
Offset: 0,
|
||||
Order: "",
|
||||
}
|
||||
|
||||
result, err := AlbumSearch(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 0, len(result))
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateAlbumDates(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
if err := UpdateAlbumDates(); err != nil {
|
||||
|
@ -233,7 +85,7 @@ func TestAlbumEntryFound(t *testing.T) {
|
|||
|
||||
func TestGetAlbums(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
r, err := GetAlbums(0, 3)
|
||||
r, err := Albums(0, 3)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
|
@ -42,24 +42,8 @@ var log = event.Log
|
|||
const (
|
||||
MySQL = "mysql"
|
||||
SQLite = "sqlite3"
|
||||
Or = "|"
|
||||
And = "&"
|
||||
Plus = " + "
|
||||
OrEn = " or "
|
||||
AndEn = " and "
|
||||
WithEn = " with "
|
||||
InEn = " in "
|
||||
AtEn = " at "
|
||||
Space = " "
|
||||
Empty = ""
|
||||
)
|
||||
|
||||
// MaxResults is max result limit for queries.
|
||||
const MaxResults = 10000
|
||||
|
||||
// SearchRadius is about 1 km.
|
||||
const SearchRadius = 0.009
|
||||
|
||||
// Cols represents a list of database columns.
|
||||
type Cols []string
|
||||
|
||||
|
|
|
@ -2,9 +2,7 @@ package query
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
|
@ -119,60 +117,3 @@ func CreateMarkerSubjects() (affected int64, err error) {
|
|||
|
||||
return affected, err
|
||||
}
|
||||
|
||||
// SearchSubjUIDs finds subject UIDs matching the search string, and removes names from the remaining query.
|
||||
func SearchSubjUIDs(s string) (result []string, names []string, remaining string) {
|
||||
if s == "" {
|
||||
return result, names, s
|
||||
}
|
||||
|
||||
type Matches struct {
|
||||
SubjUID string
|
||||
SubjName string
|
||||
SubjAlias string
|
||||
}
|
||||
|
||||
var matches []Matches
|
||||
|
||||
wheres := LikeAllNames(Cols{"subj_name", "subj_alias"}, s)
|
||||
|
||||
if len(wheres) == 0 {
|
||||
return result, names, s
|
||||
}
|
||||
|
||||
remaining = s
|
||||
|
||||
for _, where := range wheres {
|
||||
var subj []string
|
||||
|
||||
stmt := Db().Model(entity.Subject{})
|
||||
stmt = stmt.Where("?", gorm.Expr(where))
|
||||
|
||||
if err := stmt.Scan(&matches).Error; err != nil {
|
||||
log.Errorf("search: %s while finding subjects", err)
|
||||
} else if len(matches) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, m := range matches {
|
||||
subj = append(subj, m.SubjUID)
|
||||
names = append(names, m.SubjName)
|
||||
|
||||
for _, r := range txt.Words(strings.ToLower(m.SubjName)) {
|
||||
if len(r) > 1 {
|
||||
remaining = strings.ReplaceAll(remaining, r, "")
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range txt.Words(strings.ToLower(m.SubjAlias)) {
|
||||
if len(r) > 1 {
|
||||
remaining = strings.ReplaceAll(remaining, r, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, strings.Join(subj, Or))
|
||||
}
|
||||
|
||||
return result, names, NormalizeSearchQuery(remaining)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package query
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
|
@ -71,25 +70,3 @@ func TestCreateMarkerSubjects(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.LessOrEqual(t, int64(0), affected)
|
||||
}
|
||||
|
||||
func TestSearchSubjUIDs(t *testing.T) {
|
||||
t.Run("john & his | cats", func(t *testing.T) {
|
||||
result, names, remaining := SearchSubjUIDs("john & his | cats")
|
||||
|
||||
if len(result) != 1 {
|
||||
t.Fatal("expected one result")
|
||||
} else {
|
||||
assert.Equal(t, "jqu0xs11qekk9jx8", result[0])
|
||||
assert.Equal(t, "his | cats", remaining)
|
||||
assert.Equal(t, "John Doe", strings.Join(names, ", "))
|
||||
}
|
||||
})
|
||||
t.Run("xxx", func(t *testing.T) {
|
||||
result, _, _ := SearchSubjUIDs("xxx")
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
t.Run("empty string", func(t *testing.T) {
|
||||
result, _, _ := SearchSubjUIDs("")
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
package query
|
||||
package search
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
)
|
||||
|
||||
// AccountSearch returns a list of accounts.
|
||||
func AccountSearch(f form.AccountSearch) (result entity.Accounts, err error) {
|
||||
// Accounts returns a list of accounts.
|
||||
func Accounts(f form.AccountSearch) (result entity.Accounts, err error) {
|
||||
s := Db().Where(&entity.Account{})
|
||||
|
||||
if f.Share {
|
||||
|
@ -35,12 +35,3 @@ func AccountSearch(f form.AccountSearch) (result entity.Accounts, err error) {
|
|||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// AccountByID finds an account by primary key.
|
||||
func AccountByID(id uint) (result entity.Account, err error) {
|
||||
if err := Db().Where("id = ?", id).First(&result).Error; err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package query
|
||||
package search
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
@ -19,7 +19,7 @@ func TestAccounts(t *testing.T) {
|
|||
Offset: 0,
|
||||
Order: "",
|
||||
}
|
||||
r, err := AccountSearch(f)
|
||||
r, err := Accounts(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -44,7 +44,7 @@ func TestAccounts(t *testing.T) {
|
|||
Offset: 0,
|
||||
Order: "",
|
||||
}
|
||||
r, err := AccountSearch(f)
|
||||
r, err := Accounts(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -66,7 +66,7 @@ func TestAccounts(t *testing.T) {
|
|||
Offset: 0,
|
||||
Order: "",
|
||||
}
|
||||
r, err := AccountSearch(f)
|
||||
r, err := Accounts(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -81,25 +81,3 @@ func TestAccounts(t *testing.T) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccountByID(t *testing.T) {
|
||||
t.Run("existing account", func(t *testing.T) {
|
||||
r, err := AccountByID(uint(1000001))
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "Test Account2", r.AccName)
|
||||
|
||||
})
|
||||
t.Run("record not found", func(t *testing.T) {
|
||||
r, err := AccountByID(uint(123))
|
||||
|
||||
if err == nil {
|
||||
t.Fatal()
|
||||
}
|
||||
assert.Equal(t, "record not found", err.Error())
|
||||
assert.Empty(t, r)
|
||||
})
|
||||
}
|
92
internal/search/albums.go
Normal file
92
internal/search/albums.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// Albums searches albums based on their name.
|
||||
func Albums(f form.AlbumSearch) (results AlbumResults, err error) {
|
||||
if err := f.ParseQueryString(); err != nil {
|
||||
return results, err
|
||||
}
|
||||
|
||||
// Base query.
|
||||
s := UnscopedDb().Table("albums").
|
||||
Select("albums.*, cp.photo_count, cl.link_count").
|
||||
Joins("LEFT JOIN (SELECT album_uid, count(photo_uid) AS photo_count FROM photos_albums WHERE hidden = 0 AND missing = 0 GROUP BY album_uid) AS cp ON cp.album_uid = albums.album_uid").
|
||||
Joins("LEFT JOIN (SELECT share_uid, count(share_uid) AS link_count FROM links GROUP BY share_uid) AS cl ON cl.share_uid = albums.album_uid").
|
||||
Where("albums.album_type <> 'folder' OR albums.album_path IN (SELECT photo_path FROM photos WHERE photo_private = 0 AND photo_quality > -1 AND deleted_at IS NULL)").
|
||||
Where("albums.deleted_at IS NULL")
|
||||
|
||||
// Limit result count.
|
||||
if f.Count > 0 && f.Count <= MaxResults {
|
||||
s = s.Limit(f.Count).Offset(f.Offset)
|
||||
} else {
|
||||
s = s.Limit(MaxResults).Offset(f.Offset)
|
||||
}
|
||||
|
||||
// Set sort order.
|
||||
switch f.Order {
|
||||
case "slug":
|
||||
s = s.Order("albums.album_favorite DESC, album_slug ASC")
|
||||
default:
|
||||
s = s.Order("albums.album_favorite DESC, albums.album_year DESC, albums.album_month DESC, albums.album_day DESC, albums.album_title, albums.created_at DESC")
|
||||
}
|
||||
|
||||
if f.ID != "" {
|
||||
s = s.Where("albums.album_uid IN (?)", strings.Split(f.ID, txt.Or))
|
||||
|
||||
if result := s.Scan(&results); result.Error != nil {
|
||||
return results, result.Error
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
if f.Query != "" {
|
||||
likeString := "%" + f.Query + "%"
|
||||
s = s.Where("albums.album_title LIKE ? OR albums.album_location LIKE ?", likeString, likeString)
|
||||
}
|
||||
|
||||
if f.Type != "" {
|
||||
s = s.Where("albums.album_type IN (?)", strings.Split(f.Type, txt.Or))
|
||||
}
|
||||
|
||||
if f.Category != "" {
|
||||
s = s.Where("albums.album_category IN (?)", strings.Split(f.Category, txt.Or))
|
||||
}
|
||||
|
||||
if f.Location != "" {
|
||||
s = s.Where("albums.album_location IN (?)", strings.Split(f.Location, txt.Or))
|
||||
}
|
||||
|
||||
if f.Country != "" {
|
||||
s = s.Where("albums.album_country IN (?)", strings.Split(f.Country, txt.Or))
|
||||
}
|
||||
|
||||
if f.Favorite {
|
||||
s = s.Where("albums.album_favorite = 1")
|
||||
}
|
||||
|
||||
if (f.Year > 0 && f.Year <= txt.YearMax) || f.Year == entity.UnknownYear {
|
||||
s = s.Where("albums.album_year = ?", f.Year)
|
||||
}
|
||||
|
||||
if (f.Month >= txt.MonthMin && f.Month <= txt.MonthMax) || f.Month == entity.UnknownMonth {
|
||||
s = s.Where("albums.album_month = ?", f.Month)
|
||||
}
|
||||
|
||||
if (f.Day >= txt.DayMin && f.Month <= txt.DayMax) || f.Day == entity.UnknownDay {
|
||||
s = s.Where("albums.album_day = ?", f.Day)
|
||||
}
|
||||
|
||||
if result := s.Scan(&results); result.Error != nil {
|
||||
return results, result.Error
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
18
internal/search/albums_photos.go
Normal file
18
internal/search/albums_photos.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
)
|
||||
|
||||
// AlbumPhotos returns up to count photos from an album.
|
||||
func AlbumPhotos(a entity.Album, count int) (results PhotoResults, err error) {
|
||||
results, _, err = Photos(form.PhotoSearch{
|
||||
Album: a.AlbumUID,
|
||||
Filter: a.AlbumFilter,
|
||||
Count: count,
|
||||
Offset: 0,
|
||||
})
|
||||
|
||||
return results, err
|
||||
}
|
39
internal/search/albums_results.go
Normal file
39
internal/search/albums_results.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Album represents an album search result.
|
||||
type Album struct {
|
||||
ID uint `json:"-"`
|
||||
AlbumUID string `json:"UID"`
|
||||
ParentUID string `json:"ParentUID"`
|
||||
Thumb string `json:"Thumb"`
|
||||
ThumbSrc string `json:"ThumbSrc"`
|
||||
AlbumSlug string `json:"Slug"`
|
||||
AlbumType string `json:"Type"`
|
||||
AlbumTitle string `json:"Title"`
|
||||
AlbumLocation string `json:"Location"`
|
||||
AlbumCategory string `json:"Category"`
|
||||
AlbumCaption string `json:"Caption"`
|
||||
AlbumDescription string `json:"Description"`
|
||||
AlbumNotes string `json:"Notes"`
|
||||
AlbumFilter string `json:"Filter"`
|
||||
AlbumOrder string `json:"Order"`
|
||||
AlbumTemplate string `json:"Template"`
|
||||
AlbumPath string `json:"Path"`
|
||||
AlbumCountry string `json:"Country"`
|
||||
AlbumYear int `json:"Year"`
|
||||
AlbumMonth int `json:"Month"`
|
||||
AlbumDay int `json:"Day"`
|
||||
AlbumFavorite bool `json:"Favorite"`
|
||||
AlbumPrivate bool `json:"Private"`
|
||||
PhotoCount int `json:"PhotoCount"`
|
||||
LinkCount int `json:"LinkCount"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
DeletedAt time.Time `json:"DeletedAt,omitempty"`
|
||||
}
|
||||
|
||||
type AlbumResults []Album
|
155
internal/search/albums_test.go
Normal file
155
internal/search/albums_test.go
Normal file
|
@ -0,0 +1,155 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
form "github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAlbumPhotos(t *testing.T) {
|
||||
t.Run("search with string", func(t *testing.T) {
|
||||
results, err := AlbumPhotos(entity.AlbumFixtures.Get("april-1990"), 2)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(results) < 2 {
|
||||
t.Errorf("at least 2 results expected: %d", len(results))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAlbums(t *testing.T) {
|
||||
t.Run("search with string", func(t *testing.T) {
|
||||
query := form.NewAlbumSearch("chr")
|
||||
result, err := Albums(query)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "Christmas 2030", result[0].AlbumTitle)
|
||||
})
|
||||
|
||||
t.Run("search with slug", func(t *testing.T) {
|
||||
query := form.NewAlbumSearch("slug:holiday count:10")
|
||||
result, err := Albums(query)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "Holiday 2030", result[0].AlbumTitle)
|
||||
})
|
||||
|
||||
t.Run("search with country", func(t *testing.T) {
|
||||
query := form.NewAlbumSearch("country:ca")
|
||||
result, err := Albums(query)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "April 1990", result[0].AlbumTitle)
|
||||
})
|
||||
|
||||
t.Run("favorites true", func(t *testing.T) {
|
||||
query := form.NewAlbumSearch("favorite:true count:10000")
|
||||
|
||||
result, err := Albums(query)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "Holiday 2030", result[0].AlbumTitle)
|
||||
})
|
||||
t.Run("empty query", func(t *testing.T) {
|
||||
query := form.NewAlbumSearch("order:slug")
|
||||
|
||||
results, err := Albums(query)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(results) < 3 {
|
||||
t.Errorf("at least 3 results expected: %d", len(results))
|
||||
}
|
||||
})
|
||||
t.Run("search with invalid query string", func(t *testing.T) {
|
||||
query := form.NewAlbumSearch("xxx:bla")
|
||||
result, err := Albums(query)
|
||||
assert.Error(t, err, "unknown filter")
|
||||
t.Log(result)
|
||||
})
|
||||
t.Run("search with invalid query string", func(t *testing.T) {
|
||||
query := form.NewAlbumSearch("xxx:bla")
|
||||
result, err := Albums(query)
|
||||
assert.Error(t, err, "unknown filter")
|
||||
t.Log(result)
|
||||
})
|
||||
t.Run("search for existing ID", func(t *testing.T) {
|
||||
f := form.AlbumSearch{
|
||||
Query: "",
|
||||
ID: "at9lxuqxpogaaba7",
|
||||
Slug: "",
|
||||
Title: "",
|
||||
Favorite: false,
|
||||
Count: 0,
|
||||
Offset: 0,
|
||||
Order: "",
|
||||
}
|
||||
|
||||
result, err := Albums(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 1, len(result))
|
||||
assert.Equal(t, "christmas-2030", result[0].AlbumSlug)
|
||||
})
|
||||
t.Run("search with multiple filters", func(t *testing.T) {
|
||||
f := form.AlbumSearch{
|
||||
Query: "",
|
||||
Type: "moment",
|
||||
Category: "Fun",
|
||||
Location: "Favorite Park",
|
||||
Title: "Empty Moment",
|
||||
Count: 0,
|
||||
Offset: 0,
|
||||
Order: "",
|
||||
}
|
||||
|
||||
result, err := Albums(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 1, len(result))
|
||||
assert.Equal(t, "Empty Moment", result[0].AlbumTitle)
|
||||
})
|
||||
t.Run("search for year/month/day", func(t *testing.T) {
|
||||
f := form.AlbumSearch{
|
||||
Year: 2021,
|
||||
Month: 10,
|
||||
Day: 3,
|
||||
Count: 0,
|
||||
Offset: 0,
|
||||
Order: "",
|
||||
}
|
||||
|
||||
result, err := Albums(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, 0, len(result))
|
||||
})
|
||||
}
|
70
internal/search/faces.go
Normal file
70
internal/search/faces.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// Faces searches faces and returns them.
|
||||
func Faces(f form.FaceSearch) (results FaceResults, err error) {
|
||||
if err := f.ParseQueryString(); err != nil {
|
||||
return results, err
|
||||
}
|
||||
|
||||
// Base query.
|
||||
s := UnscopedDb().Table(entity.Face{}.TableName())
|
||||
|
||||
// Limit result count.
|
||||
if f.Count > 0 && f.Count <= MaxResults {
|
||||
s = s.Limit(f.Count).Offset(f.Offset)
|
||||
} else {
|
||||
s = s.Limit(MaxResults).Offset(f.Offset)
|
||||
}
|
||||
|
||||
// Set sort order.
|
||||
switch f.Order {
|
||||
case "subject":
|
||||
s = s.Order("subj_uid")
|
||||
case "added":
|
||||
s = s.Order(fmt.Sprintf("%s.created_at DESC", entity.Face{}.TableName()))
|
||||
default:
|
||||
s = s.Order("samples DESC")
|
||||
}
|
||||
|
||||
// Find specific IDs?
|
||||
if f.ID != "" {
|
||||
s = s.Where(fmt.Sprintf("%s.id IN (?)", entity.Face{}.TableName()), strings.Split(strings.ToUpper(f.ID), txt.Or))
|
||||
|
||||
if result := s.Scan(&results); result.Error != nil {
|
||||
return results, result.Error
|
||||
} else if f.Markers {
|
||||
// Add markers to results.
|
||||
for i := range results {
|
||||
results[i].Marker = entity.FindFaceMarker(results[i].ID)
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Find unknown faces only?
|
||||
if f.Unknown {
|
||||
s = s.Where("subj_uid = '' OR subj_uid IS NULL")
|
||||
}
|
||||
|
||||
// Perform query.
|
||||
if res := s.Scan(&results); res.Error != nil {
|
||||
return results, res.Error
|
||||
} else if f.Markers {
|
||||
// Add markers to results.
|
||||
for i := range results {
|
||||
results[i].Marker = entity.FindFaceMarker(results[i].ID)
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
25
internal/search/faces_results.go
Normal file
25
internal/search/faces_results.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
)
|
||||
|
||||
// Face represents a face search result.
|
||||
type Face struct {
|
||||
ID string `json:"ID"`
|
||||
FaceSrc string `json:"Src"`
|
||||
SubjUID string `json:"SubjUID"`
|
||||
Samples int `json:"Samples"`
|
||||
SampleRadius float64 `json:"SampleRadius"`
|
||||
Collisions int `json:"Collisions"`
|
||||
CollisionRadius float64 `json:"CollisionRadius"`
|
||||
Marker *entity.Marker `json:"Marker,omitempty"`
|
||||
MatchedAt *time.Time `json:"MatchedAt" yaml:"MatchedAt,omitempty"`
|
||||
CreatedAt time.Time `json:"CreatedAt" yaml:"CreatedAt,omitempty"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt" yaml:"UpdatedAt,omitempty"`
|
||||
}
|
||||
|
||||
// FaceResults represents face search results.
|
||||
type FaceResults []Face
|
18
internal/search/faces_test.go
Normal file
18
internal/search/faces_test.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFaces(t *testing.T) {
|
||||
t.Run("Unknown", func(t *testing.T) {
|
||||
results, err := Faces(form.FaceSearch{Unknown: true, Markers: true})
|
||||
assert.NoError(t, err)
|
||||
t.Logf("Faces: %#v", results)
|
||||
assert.LessOrEqual(t, 1, len(results))
|
||||
})
|
||||
}
|
|
@ -1,25 +1,20 @@
|
|||
package query
|
||||
package search
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gosimple/slug"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/capture"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// Labels searches labels based on their name.
|
||||
func Labels(f form.LabelSearch) (results []LabelResult, err error) {
|
||||
func Labels(f form.LabelSearch) (results []Label, err error) {
|
||||
if err := f.ParseQueryString(); err != nil {
|
||||
return results, err
|
||||
}
|
||||
|
||||
defer log.Debug(capture.Time(time.Now(), fmt.Sprintf("labels: search %s", form.Serialize(f, true))))
|
||||
|
||||
s := UnscopedDb()
|
||||
// s.LogMode(true)
|
||||
|
||||
|
@ -46,7 +41,7 @@ func Labels(f form.LabelSearch) (results []LabelResult, err error) {
|
|||
}
|
||||
|
||||
if f.ID != "" {
|
||||
s = s.Where("labels.label_uid IN (?)", strings.Split(f.ID, Or))
|
||||
s = s.Where("labels.label_uid IN (?)", strings.Split(f.ID, txt.Or))
|
||||
|
||||
if result := s.Scan(&results); result.Error != nil {
|
||||
return results, result.Error
|
|
@ -1,12 +1,11 @@
|
|||
package query
|
||||
package search
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// LabelResult contains found labels
|
||||
type LabelResult struct {
|
||||
// Label
|
||||
// Label represents a label search result.
|
||||
type Label struct {
|
||||
ID uint `json:"ID"`
|
||||
LabelUID string `json:"UID"`
|
||||
Thumb string `json:"Thumb"`
|
|
@ -1,4 +1,4 @@
|
|||
package query
|
||||
package search
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
@ -22,7 +22,7 @@ func TestLabels(t *testing.T) {
|
|||
assert.LessOrEqual(t, 2, len(result))
|
||||
|
||||
for _, r := range result {
|
||||
assert.IsType(t, LabelResult{}, r)
|
||||
assert.IsType(t, Label{}, r)
|
||||
assert.NotEmpty(t, r.ID)
|
||||
assert.NotEmpty(t, r.LabelName)
|
||||
assert.NotEmpty(t, r.LabelSlug)
|
||||
|
@ -49,7 +49,7 @@ func TestLabels(t *testing.T) {
|
|||
assert.LessOrEqual(t, 1, len(result))
|
||||
|
||||
for _, r := range result {
|
||||
assert.IsType(t, LabelResult{}, r)
|
||||
assert.IsType(t, Label{}, r)
|
||||
assert.NotEmpty(t, r.ID)
|
||||
assert.NotEmpty(t, r.LabelName)
|
||||
assert.NotEmpty(t, r.LabelSlug)
|
||||
|
@ -74,7 +74,7 @@ func TestLabels(t *testing.T) {
|
|||
|
||||
for _, r := range result {
|
||||
assert.True(t, r.LabelFavorite)
|
||||
assert.IsType(t, LabelResult{}, r)
|
||||
assert.IsType(t, Label{}, r)
|
||||
assert.NotEmpty(t, r.ID)
|
||||
assert.NotEmpty(t, r.LabelName)
|
||||
assert.NotEmpty(t, r.LabelSlug)
|
|
@ -1,4 +1,4 @@
|
|||
package query
|
||||
package search
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -10,36 +10,16 @@ import (
|
|||
"github.com/jinzhu/inflection"
|
||||
)
|
||||
|
||||
// NormalizeSearchQuery replaces search operator with default symbols.
|
||||
func NormalizeSearchQuery(s string) string {
|
||||
s = strings.ToLower(txt.Clip(s, txt.ClipQuery))
|
||||
s = strings.ReplaceAll(s, OrEn, Or)
|
||||
s = strings.ReplaceAll(s, AndEn, And)
|
||||
s = strings.ReplaceAll(s, WithEn, And)
|
||||
s = strings.ReplaceAll(s, InEn, And)
|
||||
s = strings.ReplaceAll(s, AtEn, And)
|
||||
s = strings.ReplaceAll(s, Plus, And)
|
||||
s = strings.ReplaceAll(s, "%", "*")
|
||||
return strings.Trim(s, "+&|_-=!@$%^(){}\\<>,.;: ")
|
||||
}
|
||||
|
||||
// IsTooShort tests if a search query is too short.
|
||||
func IsTooShort(q string) bool {
|
||||
q = strings.Trim(q, "- '")
|
||||
|
||||
return q != "" && len(q) < 3 && txt.IsLatin(q)
|
||||
}
|
||||
|
||||
// LikeAny returns a single where condition matching the search words.
|
||||
func LikeAny(col, s string, keywords, exact bool) (wheres []string) {
|
||||
if s == "" {
|
||||
return wheres
|
||||
}
|
||||
|
||||
s = strings.ReplaceAll(s, Or, " ")
|
||||
s = strings.ReplaceAll(s, OrEn, " ")
|
||||
s = strings.ReplaceAll(s, AndEn, And)
|
||||
s = strings.ReplaceAll(s, Plus, And)
|
||||
s = strings.ReplaceAll(s, txt.Or, " ")
|
||||
s = strings.ReplaceAll(s, txt.OrEn, " ")
|
||||
s = strings.ReplaceAll(s, txt.AndEn, txt.And)
|
||||
s = strings.ReplaceAll(s, txt.Plus, txt.And)
|
||||
|
||||
var wildcardThreshold int
|
||||
|
||||
|
@ -51,7 +31,7 @@ func LikeAny(col, s string, keywords, exact bool) (wheres []string) {
|
|||
wildcardThreshold = 2
|
||||
}
|
||||
|
||||
for _, k := range strings.Split(s, And) {
|
||||
for _, k := range strings.Split(s, txt.And) {
|
||||
var orWheres []string
|
||||
var words []string
|
||||
|
||||
|
@ -151,19 +131,19 @@ func LikeAllNames(cols Cols, s string) (wheres []string) {
|
|||
return wheres
|
||||
}
|
||||
|
||||
for _, k := range strings.Split(s, And) {
|
||||
for _, k := range strings.Split(s, txt.And) {
|
||||
var orWheres []string
|
||||
|
||||
for _, w := range strings.Split(k, Or) {
|
||||
for _, w := range strings.Split(k, txt.Or) {
|
||||
w = strings.TrimSpace(w)
|
||||
|
||||
if w == Empty || len(w) < 2 && txt.IsLatin(w) {
|
||||
if w == txt.Empty || len(w) < 2 && txt.IsLatin(w) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, c := range cols {
|
||||
if len(w) > 4 {
|
||||
if strings.Contains(w, Space) {
|
||||
if strings.Contains(w, txt.Space) {
|
||||
orWheres = append(orWheres, fmt.Sprintf("%s LIKE '%s%%'", c, w))
|
||||
} else {
|
||||
orWheres = append(orWheres, fmt.Sprintf("%s LIKE '%%%s%%'", c, w))
|
|
@ -1,33 +1,13 @@
|
|||
package query
|
||||
package search
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNormalizeSearchQuery(t *testing.T) {
|
||||
t.Run("Replace", func(t *testing.T) {
|
||||
q := NormalizeSearchQuery("table spoon & usa | img% json OR BILL!")
|
||||
assert.Equal(t, "table spoon & usa | img* json|bill", q)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsTooShort(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
assert.False(t, IsTooShort(""))
|
||||
})
|
||||
t.Run("IsTooShort", func(t *testing.T) {
|
||||
assert.True(t, IsTooShort("aa"))
|
||||
})
|
||||
t.Run("Chinese", func(t *testing.T) {
|
||||
assert.False(t, IsTooShort("李"))
|
||||
})
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
assert.False(t, IsTooShort("foo"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestLikeAny(t *testing.T) {
|
||||
t.Run("and_or_search", func(t *testing.T) {
|
||||
if w := LikeAny("k.keyword", "table spoon & usa | img json", true, false); len(w) != 2 {
|
||||
|
@ -222,7 +202,7 @@ func TestLikeAllNames(t *testing.T) {
|
|||
}
|
||||
})
|
||||
t.Run("Plus", func(t *testing.T) {
|
||||
if w := LikeAllNames(Cols{"name"}, NormalizeSearchQuery("Paul + Paula")); len(w) == 2 {
|
||||
if w := LikeAllNames(Cols{"name"}, txt.NormalizeQuery("Paul + Paula")); len(w) == 2 {
|
||||
assert.Equal(t, "name LIKE 'paul' OR name LIKE 'paul %'", w[0])
|
||||
assert.Equal(t, "name LIKE '%paula%'", w[1])
|
||||
} else {
|
||||
|
@ -230,7 +210,7 @@ func TestLikeAllNames(t *testing.T) {
|
|||
}
|
||||
})
|
||||
t.Run("Ane", func(t *testing.T) {
|
||||
if w := LikeAllNames(Cols{"name"}, NormalizeSearchQuery("Paul and Paula")); len(w) == 2 {
|
||||
if w := LikeAllNames(Cols{"name"}, txt.NormalizeQuery("Paul and Paula")); len(w) == 2 {
|
||||
assert.Equal(t, "name LIKE 'paul' OR name LIKE 'paul %'", w[0])
|
||||
assert.Equal(t, "name LIKE '%paula%'", w[1])
|
||||
} else {
|
||||
|
@ -238,7 +218,7 @@ func TestLikeAllNames(t *testing.T) {
|
|||
}
|
||||
})
|
||||
t.Run("Or", func(t *testing.T) {
|
||||
if w := LikeAllNames(Cols{"name"}, NormalizeSearchQuery("Paul or Paula")); len(w) == 1 {
|
||||
if w := LikeAllNames(Cols{"name"}, txt.NormalizeQuery("Paul or Paula")); len(w) == 1 {
|
||||
assert.Equal(t, "name LIKE 'paul' OR name LIKE 'paul %' OR name LIKE '%paula%'", w[0])
|
||||
} else {
|
||||
t.Fatalf("one where conditions expected: %#v", w)
|
||||
|
@ -278,7 +258,7 @@ func TestAnySlug(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("comma separated", func(t *testing.T) {
|
||||
where := AnySlug("custom_slug", "botanical-garden|landscape|bay", Or)
|
||||
where := AnySlug("custom_slug", "botanical-garden|landscape|bay", txt.Or)
|
||||
assert.Equal(t, "custom_slug = 'botanical-garden' OR custom_slug = 'landscape' OR custom_slug = 'bay'", where)
|
||||
})
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package query
|
||||
package search
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -13,8 +13,8 @@ import (
|
|||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// PhotoSearch searches for photos based on a Form and returns PhotoResults ([]PhotoResult).
|
||||
func PhotoSearch(f form.PhotoSearch) (results PhotoResults, count int, err error) {
|
||||
// Photos searches for photos based on a Form and returns PhotoResults ([]Photo).
|
||||
func Photos(f form.PhotoSearch) (results PhotoResults, count int, err error) {
|
||||
start := time.Now()
|
||||
|
||||
if err := f.ParseQueryString(); err != nil {
|
||||
|
@ -89,7 +89,7 @@ func PhotoSearch(f form.PhotoSearch) (results PhotoResults, count int, err error
|
|||
|
||||
// Shortcut for known photo ids.
|
||||
if f.ID != "" {
|
||||
s = s.Where("photos.photo_uid IN (?)", strings.Split(f.ID, Or))
|
||||
s = s.Where("photos.photo_uid IN (?)", strings.Split(f.ID, txt.Or))
|
||||
s = s.Order("files.file_primary DESC")
|
||||
|
||||
if result := s.Scan(&results); result.Error != nil {
|
||||
|
@ -111,7 +111,7 @@ func PhotoSearch(f form.PhotoSearch) (results PhotoResults, count int, err error
|
|||
var labelIds []uint
|
||||
|
||||
if f.Label != "" {
|
||||
if err := Db().Where(AnySlug("label_slug", f.Label, Or)).Or(AnySlug("custom_slug", f.Label, Or)).Find(&labels).Error; len(labels) == 0 || err != nil {
|
||||
if err := Db().Where(AnySlug("label_slug", f.Label, txt.Or)).Or(AnySlug("custom_slug", f.Label, txt.Or)).Find(&labels).Error; len(labels) == 0 || err != nil {
|
||||
log.Errorf("search: labels %s not found", txt.Quote(f.Label))
|
||||
return results, 0, fmt.Errorf("%s not found", txt.Quote(f.Label))
|
||||
} else {
|
||||
|
@ -133,12 +133,12 @@ func PhotoSearch(f form.PhotoSearch) (results PhotoResults, count int, err error
|
|||
}
|
||||
|
||||
// Clip to reasonable size and normalize operators.
|
||||
f.Query = NormalizeSearchQuery(f.Query)
|
||||
f.Query = txt.NormalizeQuery(f.Query)
|
||||
|
||||
// Modify query if it contains subject names.
|
||||
if f.Query != "" && f.Subject == "" {
|
||||
if subj, names, remaining := SearchSubjUIDs(f.Query); len(subj) > 0 {
|
||||
f.Subject = strings.Join(subj, And)
|
||||
if subj, names, remaining := SubjectUIDs(f.Query); len(subj) > 0 {
|
||||
f.Subject = strings.Join(subj, txt.And)
|
||||
log.Debugf("people: searching for %s", txt.Quote(txt.JoinNames(names)))
|
||||
f.Query = remaining
|
||||
}
|
||||
|
@ -222,11 +222,19 @@ func PhotoSearch(f form.PhotoSearch) (results PhotoResults, count int, err error
|
|||
}
|
||||
}
|
||||
|
||||
// Filter for one or more faces?
|
||||
if f.Face != "" {
|
||||
for _, f := range strings.Split(strings.ToUpper(f.Face), txt.And) {
|
||||
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 WHERE face_id IN (?))",
|
||||
entity.Marker{}.TableName()), strings.Split(f, txt.Or))
|
||||
}
|
||||
}
|
||||
|
||||
// Filter for one or more subjects?
|
||||
if f.Subject != "" {
|
||||
for _, subj := range strings.Split(strings.ToLower(f.Subject), And) {
|
||||
for _, subj := range strings.Split(strings.ToLower(f.Subject), txt.And) {
|
||||
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 WHERE subj_uid IN (?))",
|
||||
entity.Marker{}.TableName()), strings.Split(subj, Or))
|
||||
entity.Marker{}.TableName()), strings.Split(subj, txt.Or))
|
||||
}
|
||||
} else if f.Subjects != "" {
|
||||
for _, where := range LikeAnyWord("s.subj_name", f.Subjects) {
|
||||
|
@ -293,7 +301,7 @@ func PhotoSearch(f form.PhotoSearch) (results PhotoResults, count int, err error
|
|||
}
|
||||
|
||||
if f.Color != "" {
|
||||
s = s.Where("files.file_main_color IN (?)", strings.Split(strings.ToLower(f.Color), Or))
|
||||
s = s.Where("files.file_main_color IN (?)", strings.Split(strings.ToLower(f.Color), txt.Or))
|
||||
}
|
||||
|
||||
if f.Favorite {
|
||||
|
@ -315,21 +323,21 @@ func PhotoSearch(f form.PhotoSearch) (results PhotoResults, count int, err error
|
|||
}
|
||||
|
||||
if f.Country != "" {
|
||||
s = s.Where("photos.photo_country IN (?)", strings.Split(strings.ToLower(f.Country), Or))
|
||||
s = s.Where("photos.photo_country IN (?)", strings.Split(strings.ToLower(f.Country), txt.Or))
|
||||
}
|
||||
|
||||
if f.State != "" {
|
||||
s = s.Where("places.place_state IN (?)", strings.Split(f.State, Or))
|
||||
s = s.Where("places.place_state IN (?)", strings.Split(f.State, txt.Or))
|
||||
}
|
||||
|
||||
if f.Category != "" {
|
||||
s = s.Joins("JOIN cells ON photos.cell_id = cells.id").
|
||||
Where("cells.cell_category IN (?)", strings.Split(strings.ToLower(f.Category), Or))
|
||||
Where("cells.cell_category IN (?)", strings.Split(strings.ToLower(f.Category), txt.Or))
|
||||
}
|
||||
|
||||
// Filter by media type.
|
||||
if f.Type != "" {
|
||||
s = s.Where("photos.photo_type IN (?)", strings.Split(strings.ToLower(f.Type), Or))
|
||||
s = s.Where("photos.photo_type IN (?)", strings.Split(strings.ToLower(f.Type), txt.Or))
|
||||
}
|
||||
|
||||
if f.Video {
|
||||
|
@ -347,41 +355,41 @@ func PhotoSearch(f form.PhotoSearch) (results PhotoResults, count int, err error
|
|||
|
||||
if strings.HasSuffix(p, "/") {
|
||||
s = s.Where("photos.photo_path = ?", p[:len(p)-1])
|
||||
} else if strings.Contains(p, Or) {
|
||||
s = s.Where("photos.photo_path IN (?)", strings.Split(p, Or))
|
||||
} else if strings.Contains(p, txt.Or) {
|
||||
s = s.Where("photos.photo_path IN (?)", strings.Split(p, txt.Or))
|
||||
} else {
|
||||
s = s.Where("photos.photo_path LIKE ?", strings.ReplaceAll(p, "*", "%"))
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(f.Name, Or) {
|
||||
s = s.Where("photos.photo_name IN (?)", strings.Split(f.Name, Or))
|
||||
if strings.Contains(f.Name, txt.Or) {
|
||||
s = s.Where("photos.photo_name IN (?)", strings.Split(f.Name, txt.Or))
|
||||
} else if f.Name != "" {
|
||||
s = s.Where("photos.photo_name LIKE ?", strings.ReplaceAll(fs.StripKnownExt(f.Name), "*", "%"))
|
||||
}
|
||||
|
||||
if strings.Contains(f.Filename, Or) {
|
||||
s = s.Where("files.file_name IN (?)", strings.Split(f.Filename, Or))
|
||||
if strings.Contains(f.Filename, txt.Or) {
|
||||
s = s.Where("files.file_name IN (?)", strings.Split(f.Filename, txt.Or))
|
||||
} else if f.Filename != "" {
|
||||
s = s.Where("files.file_name LIKE ?", strings.ReplaceAll(f.Filename, "*", "%"))
|
||||
}
|
||||
|
||||
if strings.Contains(f.Original, Or) {
|
||||
s = s.Where("photos.original_name IN (?)", strings.Split(f.Original, Or))
|
||||
if strings.Contains(f.Original, txt.Or) {
|
||||
s = s.Where("photos.original_name IN (?)", strings.Split(f.Original, txt.Or))
|
||||
} else if f.Original != "" {
|
||||
s = s.Where("photos.original_name LIKE ?", strings.ReplaceAll(f.Original, "*", "%"))
|
||||
}
|
||||
|
||||
if strings.Contains(f.Title, Or) {
|
||||
s = s.Where("photos.photo_title IN (?)", strings.Split(strings.ToLower(f.Title), Or))
|
||||
if strings.Contains(f.Title, txt.Or) {
|
||||
s = s.Where("photos.photo_title IN (?)", strings.Split(strings.ToLower(f.Title), txt.Or))
|
||||
} else if f.Title != "" {
|
||||
s = s.Where("photos.photo_title LIKE ?", strings.ReplaceAll(strings.ToLower(f.Title), "*", "%"))
|
||||
}
|
||||
|
||||
if strings.Contains(f.Hash, Or) {
|
||||
s = s.Where("files.file_hash IN (?)", strings.Split(strings.ToLower(f.Hash), Or))
|
||||
if strings.Contains(f.Hash, txt.Or) {
|
||||
s = s.Where("files.file_hash IN (?)", strings.Split(strings.ToLower(f.Hash), txt.Or))
|
||||
} else if f.Hash != "" {
|
||||
s = s.Where("files.file_hash IN (?)", strings.Split(strings.ToLower(f.Hash), Or))
|
||||
s = s.Where("files.file_hash IN (?)", strings.Split(strings.ToLower(f.Hash), txt.Or))
|
||||
}
|
||||
|
||||
if f.Portrait {
|
||||
|
@ -416,13 +424,13 @@ func PhotoSearch(f form.PhotoSearch) (results PhotoResults, count int, err error
|
|||
|
||||
// Filter by approx distance to coordinates:
|
||||
if f.Lat != 0 {
|
||||
latMin := f.Lat - SearchRadius*float32(f.Dist)
|
||||
latMax := f.Lat + SearchRadius*float32(f.Dist)
|
||||
latMin := f.Lat - Radius*float32(f.Dist)
|
||||
latMax := f.Lat + Radius*float32(f.Dist)
|
||||
s = s.Where("photos.photo_lat BETWEEN ? AND ?", latMin, latMax)
|
||||
}
|
||||
if f.Lng != 0 {
|
||||
lngMin := f.Lng - SearchRadius*float32(f.Dist)
|
||||
lngMax := f.Lng + SearchRadius*float32(f.Dist)
|
||||
lngMin := f.Lng - Radius*float32(f.Dist)
|
||||
lngMax := f.Lng + Radius*float32(f.Dist)
|
||||
s = s.Where("photos.photo_lng BETWEEN ? AND ?", lngMin, lngMax)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package query
|
||||
package search
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -10,22 +10,19 @@ import (
|
|||
"github.com/jinzhu/gorm"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/capture"
|
||||
"github.com/photoprism/photoprism/pkg/pluscode"
|
||||
"github.com/photoprism/photoprism/pkg/s2"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// Geo searches for photos based on Form values and returns GeoResults ([]GeoResult).
|
||||
func Geo(f form.GeoSearch) (results GeoResults, err error) {
|
||||
// PhotosGeo searches for photos based on Form values and returns GeoResults ([]GeoResult).
|
||||
func PhotosGeo(f form.GeoSearch) (results GeoResults, err error) {
|
||||
start := time.Now()
|
||||
|
||||
if err := f.ParseQueryString(); err != nil {
|
||||
return results, err
|
||||
}
|
||||
|
||||
defer log.Debug(capture.Time(time.Now(), fmt.Sprintf("geo: search %s", form.Serialize(f, true))))
|
||||
|
||||
s := UnscopedDb()
|
||||
|
||||
// s.LogMode(true)
|
||||
|
@ -40,12 +37,12 @@ func Geo(f form.GeoSearch) (results GeoResults, err error) {
|
|||
Where("photos.photo_lat <> 0")
|
||||
|
||||
// Clip to reasonable size and normalize operators.
|
||||
f.Query = NormalizeSearchQuery(f.Query)
|
||||
f.Query = txt.NormalizeQuery(f.Query)
|
||||
|
||||
// Modify query if it contains subject names.
|
||||
if f.Query != "" && f.Subject == "" {
|
||||
if subj, names, remaining := SearchSubjUIDs(f.Query); len(subj) > 0 {
|
||||
f.Subject = strings.Join(subj, And)
|
||||
if subj, names, remaining := SubjectUIDs(f.Query); len(subj) > 0 {
|
||||
f.Subject = strings.Join(subj, txt.And)
|
||||
log.Debugf("search: subject %s", txt.Quote(strings.Join(names, ", ")))
|
||||
f.Query = remaining
|
||||
}
|
||||
|
@ -115,11 +112,19 @@ func Geo(f form.GeoSearch) (results GeoResults, err error) {
|
|||
}
|
||||
}
|
||||
|
||||
// Filter for one or more faces?
|
||||
if f.Face != "" {
|
||||
for _, f := range strings.Split(strings.ToUpper(f.Face), txt.And) {
|
||||
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 WHERE face_id IN (?))",
|
||||
entity.Marker{}.TableName()), strings.Split(f, txt.Or))
|
||||
}
|
||||
}
|
||||
|
||||
// Filter for one or more subjects?
|
||||
if f.Subject != "" {
|
||||
for _, subj := range strings.Split(strings.ToLower(f.Subject), And) {
|
||||
for _, subj := range strings.Split(strings.ToLower(f.Subject), txt.And) {
|
||||
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 WHERE subj_uid IN (?))",
|
||||
entity.Marker{}.TableName()), strings.Split(subj, Or))
|
||||
entity.Marker{}.TableName()), strings.Split(subj, txt.Or))
|
||||
}
|
||||
} else if f.Subjects != "" {
|
||||
for _, where := range LikeAnyWord("s.subj_name", f.Subjects) {
|
||||
|
@ -173,7 +178,7 @@ func Geo(f form.GeoSearch) (results GeoResults, err error) {
|
|||
}
|
||||
|
||||
if f.Color != "" {
|
||||
s = s.Where("files.file_main_color IN (?)", strings.Split(strings.ToLower(f.Color), Or))
|
||||
s = s.Where("files.file_main_color IN (?)", strings.Split(strings.ToLower(f.Color), txt.Or))
|
||||
}
|
||||
|
||||
if f.Favorite {
|
||||
|
@ -181,12 +186,12 @@ func Geo(f form.GeoSearch) (results GeoResults, err error) {
|
|||
}
|
||||
|
||||
if f.Country != "" {
|
||||
s = s.Where("photos.photo_country IN (?)", strings.Split(strings.ToLower(f.Country), Or))
|
||||
s = s.Where("photos.photo_country IN (?)", strings.Split(strings.ToLower(f.Country), txt.Or))
|
||||
}
|
||||
|
||||
// Filter by media type.
|
||||
if f.Type != "" {
|
||||
s = s.Where("photos.photo_type IN (?)", strings.Split(strings.ToLower(f.Type), Or))
|
||||
s = s.Where("photos.photo_type IN (?)", strings.Split(strings.ToLower(f.Type), txt.Or))
|
||||
}
|
||||
|
||||
if f.Video {
|
||||
|
@ -204,15 +209,15 @@ func Geo(f form.GeoSearch) (results GeoResults, err error) {
|
|||
|
||||
if strings.HasSuffix(p, "/") {
|
||||
s = s.Where("photos.photo_path = ?", p[:len(p)-1])
|
||||
} else if strings.Contains(p, Or) {
|
||||
s = s.Where("photos.photo_path IN (?)", strings.Split(p, Or))
|
||||
} else if strings.Contains(p, txt.Or) {
|
||||
s = s.Where("photos.photo_path IN (?)", strings.Split(p, txt.Or))
|
||||
} else {
|
||||
s = s.Where("photos.photo_path LIKE ?", strings.ReplaceAll(p, "*", "%"))
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(f.Name, Or) {
|
||||
s = s.Where("photos.photo_name IN (?)", strings.Split(f.Name, Or))
|
||||
if strings.Contains(f.Name, txt.Or) {
|
||||
s = s.Where("photos.photo_name IN (?)", strings.Split(f.Name, txt.Or))
|
||||
} else if f.Name != "" {
|
||||
s = s.Where("photos.photo_name LIKE ?", strings.ReplaceAll(fs.StripKnownExt(f.Name), "*", "%"))
|
||||
}
|
||||
|
@ -250,13 +255,13 @@ func Geo(f form.GeoSearch) (results GeoResults, err error) {
|
|||
} else {
|
||||
// Filter by approx distance to coordinates:
|
||||
if f.Lat != 0 {
|
||||
latMin := f.Lat - SearchRadius*float32(f.Dist)
|
||||
latMax := f.Lat + SearchRadius*float32(f.Dist)
|
||||
latMin := f.Lat - Radius*float32(f.Dist)
|
||||
latMax := f.Lat + Radius*float32(f.Dist)
|
||||
s = s.Where("photos.photo_lat BETWEEN ? AND ?", latMin, latMax)
|
||||
}
|
||||
if f.Lng != 0 {
|
||||
lngMin := f.Lng - SearchRadius*float32(f.Dist)
|
||||
lngMax := f.Lng + SearchRadius*float32(f.Dist)
|
||||
lngMin := f.Lng - Radius*float32(f.Dist)
|
||||
lngMax := f.Lng + Radius*float32(f.Dist)
|
||||
s = s.Where("photos.photo_lng BETWEEN ? AND ?", lngMin, lngMax)
|
||||
}
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
package query
|
||||
package search
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// GeoResult represents a photo for displaying it on a map.
|
||||
// GeoResult represents a photo geo json search result.
|
||||
type GeoResult struct {
|
||||
ID string `json:"-"`
|
||||
PhotoUID string `json:"UID"`
|
|
@ -1,4 +1,4 @@
|
|||
package query
|
||||
package search
|
||||
|
||||
import (
|
||||
"testing"
|
|
@ -1,4 +1,4 @@
|
|||
package query
|
||||
package search
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
@ -14,7 +14,7 @@ func TestGeo(t *testing.T) {
|
|||
t.Run("form.keywords", func(t *testing.T) {
|
||||
query := form.NewGeoSearch("keywords:bridge")
|
||||
|
||||
if result, err := Geo(query); err != nil {
|
||||
if result, err := PhotosGeo(query); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.GreaterOrEqual(t, len(result), 1)
|
||||
|
@ -23,7 +23,7 @@ func TestGeo(t *testing.T) {
|
|||
t.Run("form.subjects", func(t *testing.T) {
|
||||
query := form.NewGeoSearch("subjects:John")
|
||||
|
||||
if result, err := Geo(query); err != nil {
|
||||
if result, err := PhotosGeo(query); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.GreaterOrEqual(t, len(result), 0)
|
||||
|
@ -32,7 +32,7 @@ func TestGeo(t *testing.T) {
|
|||
t.Run("find_all", func(t *testing.T) {
|
||||
query := form.NewGeoSearch("")
|
||||
|
||||
if result, err := Geo(query); err != nil {
|
||||
if result, err := PhotosGeo(query); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.LessOrEqual(t, 4, len(result))
|
||||
|
@ -41,7 +41,7 @@ func TestGeo(t *testing.T) {
|
|||
|
||||
t.Run("search for bridge", func(t *testing.T) {
|
||||
query := form.NewGeoSearch("Query:bridge Before:3006-01-02")
|
||||
result, err := Geo(query)
|
||||
result, err := PhotosGeo(query)
|
||||
t.Logf("RESULT: %+v", result)
|
||||
|
||||
if err != nil {
|
||||
|
@ -54,7 +54,7 @@ func TestGeo(t *testing.T) {
|
|||
|
||||
t.Run("search for date range", func(t *testing.T) {
|
||||
query := form.NewGeoSearch("After:2014-12-02 Before:3006-01-02")
|
||||
result, err := Geo(query)
|
||||
result, err := PhotosGeo(query)
|
||||
|
||||
// t.Logf("RESULT: %+v", result)
|
||||
|
||||
|
@ -80,7 +80,7 @@ func TestGeo(t *testing.T) {
|
|||
Review: true,
|
||||
}
|
||||
|
||||
result, err := Geo(f)
|
||||
result, err := PhotosGeo(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -108,7 +108,7 @@ func TestGeo(t *testing.T) {
|
|||
Review: false,
|
||||
}
|
||||
|
||||
result, err := Geo(f)
|
||||
result, err := PhotosGeo(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -131,7 +131,7 @@ func TestGeo(t *testing.T) {
|
|||
Review: false,
|
||||
}
|
||||
|
||||
result, err := Geo(f)
|
||||
result, err := PhotosGeo(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -154,7 +154,7 @@ func TestGeo(t *testing.T) {
|
|||
Review: false,
|
||||
}
|
||||
|
||||
result, err := Geo(f)
|
||||
result, err := PhotosGeo(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -166,7 +166,7 @@ func TestGeo(t *testing.T) {
|
|||
Query: "flower",
|
||||
}
|
||||
|
||||
result, err := Geo(f)
|
||||
result, err := PhotosGeo(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -191,7 +191,7 @@ func TestGeo(t *testing.T) {
|
|||
Private: true,
|
||||
}
|
||||
|
||||
result, err := Geo(f)
|
||||
result, err := PhotosGeo(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -209,7 +209,7 @@ func TestGeo(t *testing.T) {
|
|||
Public: true,
|
||||
}
|
||||
|
||||
result, err := Geo(f)
|
||||
result, err := PhotosGeo(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -225,7 +225,7 @@ func TestGeo(t *testing.T) {
|
|||
Archived: true,
|
||||
}
|
||||
|
||||
result, err := Geo(f)
|
||||
result, err := PhotosGeo(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -236,7 +236,7 @@ func TestGeo(t *testing.T) {
|
|||
var f form.GeoSearch
|
||||
f.Query = "faces:true"
|
||||
|
||||
photos, err := Geo(f)
|
||||
photos, err := PhotosGeo(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -248,7 +248,7 @@ func TestGeo(t *testing.T) {
|
|||
var f form.GeoSearch
|
||||
f.Faces = "Yes"
|
||||
|
||||
photos, err := Geo(f)
|
||||
photos, err := PhotosGeo(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -260,7 +260,7 @@ func TestGeo(t *testing.T) {
|
|||
var f form.GeoSearch
|
||||
f.Faces = "No"
|
||||
|
||||
photos, err := Geo(f)
|
||||
photos, err := PhotosGeo(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -272,7 +272,7 @@ func TestGeo(t *testing.T) {
|
|||
var f form.GeoSearch
|
||||
f.Faces = "2"
|
||||
|
||||
photos, err := Geo(f)
|
||||
photos, err := PhotosGeo(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -285,7 +285,7 @@ func TestGeo(t *testing.T) {
|
|||
f.Day = 18
|
||||
f.Month = 4
|
||||
|
||||
photos, err := Geo(f)
|
||||
photos, err := PhotosGeo(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -297,7 +297,7 @@ func TestGeo(t *testing.T) {
|
|||
var f form.GeoSearch
|
||||
f.Query = "Actress"
|
||||
|
||||
photos, err := Geo(f)
|
||||
photos, err := PhotosGeo(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -309,7 +309,7 @@ func TestGeo(t *testing.T) {
|
|||
var f form.GeoSearch
|
||||
f.Albums = "2030"
|
||||
|
||||
photos, err := Geo(f)
|
||||
photos, err := PhotosGeo(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -321,7 +321,7 @@ func TestGeo(t *testing.T) {
|
|||
var f form.GeoSearch
|
||||
f.Path = "1990/04" + "|" + "2015/11"
|
||||
|
||||
photos, err := Geo(f)
|
||||
photos, err := PhotosGeo(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -333,7 +333,7 @@ func TestGeo(t *testing.T) {
|
|||
var f form.GeoSearch
|
||||
f.Name = "20151101_000000_51C501B5" + "|" + "Video"
|
||||
|
||||
photos, err := Geo(f)
|
||||
photos, err := PhotosGeo(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
|
@ -1,4 +1,4 @@
|
|||
package query
|
||||
package search
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -10,8 +10,8 @@ import (
|
|||
"github.com/ulule/deepcopier"
|
||||
)
|
||||
|
||||
// PhotoResult contains found photos and their main file plus other meta data.
|
||||
type PhotoResult struct {
|
||||
// Photo represents a photo search result.
|
||||
type Photo struct {
|
||||
ID uint `json:"-"`
|
||||
CompositeID string `json:"ID"`
|
||||
UUID string `json:"DocumentID,omitempty"`
|
||||
|
@ -98,7 +98,7 @@ type PhotoResult struct {
|
|||
Files []entity.File `json:"Files"`
|
||||
}
|
||||
|
||||
type PhotoResults []PhotoResult
|
||||
type PhotoResults []Photo
|
||||
|
||||
// UIDs returns a slice of photo UIDs.
|
||||
func (m PhotoResults) UIDs() []string {
|
||||
|
@ -113,7 +113,7 @@ func (m PhotoResults) UIDs() []string {
|
|||
|
||||
func (m PhotoResults) Merged() (PhotoResults, int, error) {
|
||||
count := len(m)
|
||||
merged := make([]PhotoResult, 0, count)
|
||||
merged := make([]Photo, 0, count)
|
||||
|
||||
var lastId uint
|
||||
var i int
|
||||
|
@ -146,7 +146,7 @@ func (m PhotoResults) Merged() (PhotoResults, int, error) {
|
|||
}
|
||||
|
||||
// ShareBase returns a meaningful file name for sharing.
|
||||
func (m *PhotoResult) ShareBase(seq int) string {
|
||||
func (m *Photo) ShareBase(seq int) string {
|
||||
var name string
|
||||
|
||||
if m.PhotoTitle != "" {
|
|
@ -1,4 +1,4 @@
|
|||
package query
|
||||
package search
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
@ -8,7 +8,7 @@ import (
|
|||
)
|
||||
|
||||
func TestPhotosResults_Merged(t *testing.T) {
|
||||
result1 := PhotoResult{
|
||||
result1 := Photo{
|
||||
ID: 111111,
|
||||
CreatedAt: time.Time{},
|
||||
UpdatedAt: time.Time{},
|
||||
|
@ -67,7 +67,7 @@ func TestPhotosResults_Merged(t *testing.T) {
|
|||
Files: nil,
|
||||
}
|
||||
|
||||
result2 := PhotoResult{
|
||||
result2 := Photo{
|
||||
ID: 22222,
|
||||
CreatedAt: time.Time{},
|
||||
UpdatedAt: time.Time{},
|
||||
|
@ -137,7 +137,7 @@ func TestPhotosResults_Merged(t *testing.T) {
|
|||
t.Log(merged)
|
||||
}
|
||||
func TestPhotosResults_UIDs(t *testing.T) {
|
||||
result1 := PhotoResult{
|
||||
result1 := Photo{
|
||||
ID: 111111,
|
||||
CreatedAt: time.Time{},
|
||||
UpdatedAt: time.Time{},
|
||||
|
@ -196,7 +196,7 @@ func TestPhotosResults_UIDs(t *testing.T) {
|
|||
Files: nil,
|
||||
}
|
||||
|
||||
result2 := PhotoResult{
|
||||
result2 := Photo{
|
||||
ID: 22222,
|
||||
CreatedAt: time.Time{},
|
||||
UpdatedAt: time.Time{},
|
||||
|
@ -263,7 +263,7 @@ func TestPhotosResults_UIDs(t *testing.T) {
|
|||
|
||||
func TestPhotosResult_ShareFileName(t *testing.T) {
|
||||
t.Run("with photo title", func(t *testing.T) {
|
||||
result1 := PhotoResult{
|
||||
result1 := Photo{
|
||||
ID: 111111,
|
||||
CreatedAt: time.Time{},
|
||||
UpdatedAt: time.Time{},
|
||||
|
@ -326,7 +326,7 @@ func TestPhotosResult_ShareFileName(t *testing.T) {
|
|||
assert.Contains(t, r, "20131111-090718-Phototitle123")
|
||||
})
|
||||
t.Run("without photo title", func(t *testing.T) {
|
||||
result1 := PhotoResult{
|
||||
result1 := Photo{
|
||||
ID: 111111,
|
||||
CreatedAt: time.Time{},
|
||||
UpdatedAt: time.Time{},
|
||||
|
@ -390,7 +390,7 @@ func TestPhotosResult_ShareFileName(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("seq > 0", func(t *testing.T) {
|
||||
result1 := PhotoResult{
|
||||
result1 := Photo{
|
||||
ID: 111111,
|
||||
CreatedAt: time.Time{},
|
||||
UpdatedAt: time.Time{},
|
|
@ -1,4 +1,4 @@
|
|||
package query
|
||||
package search
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
@ -10,7 +10,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/form"
|
||||
)
|
||||
|
||||
func TestPhotoSearch(t *testing.T) {
|
||||
func TestPhotos(t *testing.T) {
|
||||
t.Run("Chinese", func(t *testing.T) {
|
||||
var frm form.PhotoSearch
|
||||
|
||||
|
@ -18,7 +18,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
frm.Count = 10
|
||||
frm.Offset = 0
|
||||
|
||||
_, _, err := PhotoSearch(frm)
|
||||
_, _, err := Photos(frm)
|
||||
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
@ -29,7 +29,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
frm.Count = 10
|
||||
frm.Offset = 0
|
||||
|
||||
photos, _, err := PhotoSearch(frm)
|
||||
photos, _, err := Photos(frm)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -38,7 +38,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
assert.LessOrEqual(t, 3, len(photos))
|
||||
|
||||
for _, r := range photos {
|
||||
assert.IsType(t, PhotoResult{}, r)
|
||||
assert.IsType(t, Photo{}, r)
|
||||
assert.NotEmpty(t, r.ID)
|
||||
assert.NotEmpty(t, r.CameraID)
|
||||
assert.NotEmpty(t, r.LensID)
|
||||
|
@ -57,7 +57,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
frm.ID = "pt9jtdre2lvl0yh7"
|
||||
frm.Merged = true
|
||||
|
||||
photos, _, err := PhotoSearch(frm)
|
||||
photos, _, err := Photos(frm)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -73,7 +73,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
frm.ID = "pt9jtdre2lvl0yh7"
|
||||
frm.Merged = false
|
||||
|
||||
photos, _, err := PhotoSearch(frm)
|
||||
photos, _, err := Photos(frm)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -87,7 +87,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
frm.Count = 10
|
||||
frm.Offset = 0
|
||||
|
||||
photos, _, err := PhotoSearch(frm)
|
||||
photos, _, err := Photos(frm)
|
||||
|
||||
assert.Equal(t, "dog not found", err.Error())
|
||||
assert.Empty(t, photos)
|
||||
|
@ -99,7 +99,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
frm.Count = 10
|
||||
frm.Offset = 0
|
||||
|
||||
photos, _, err := PhotoSearch(frm)
|
||||
photos, _, err := Photos(frm)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -113,7 +113,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
frm.Count = 10
|
||||
frm.Offset = 0
|
||||
|
||||
photos, _, err := PhotoSearch(frm)
|
||||
photos, _, err := Photos(frm)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, photos)
|
||||
|
@ -130,7 +130,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
frm.Offset = 0
|
||||
frm.Geo = true
|
||||
|
||||
photos, _, err := PhotoSearch(frm)
|
||||
photos, _, err := Photos(frm)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -147,7 +147,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
frm.Geo = true
|
||||
frm.Error = false
|
||||
|
||||
photos, _, err := PhotoSearch(frm)
|
||||
photos, _, err := Photos(frm)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -162,7 +162,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
frm.Count = 5000
|
||||
frm.Offset = 0
|
||||
|
||||
photos, _, err := PhotoSearch(frm)
|
||||
photos, _, err := Photos(frm)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -177,7 +177,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
frm.Count = 5000
|
||||
frm.Offset = 0
|
||||
|
||||
photos, _, err := PhotoSearch(frm)
|
||||
photos, _, err := Photos(frm)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -193,7 +193,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Offset = 0
|
||||
f.Archived = true
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -209,7 +209,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Private = true
|
||||
f.Error = true
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -224,7 +224,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Offset = 0
|
||||
f.Public = true
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -239,7 +239,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Offset = 0
|
||||
f.Review = true
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -255,7 +255,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Quality = 3
|
||||
f.Private = false
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -270,7 +270,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Offset = 0
|
||||
f.Error = true
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -285,7 +285,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Offset = 0
|
||||
f.Camera = 1000003
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -300,7 +300,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Offset = 0
|
||||
f.Color = "blue"
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -314,7 +314,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Count = 10
|
||||
f.Offset = 0
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -328,7 +328,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Count = 10
|
||||
f.Offset = 0
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -343,7 +343,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Count = 10
|
||||
f.Offset = 0
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -359,7 +359,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Count = 10
|
||||
f.Offset = 0
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -368,13 +368,28 @@ func TestPhotoSearch(t *testing.T) {
|
|||
//t.Logf("results: %+v", photos)
|
||||
assert.GreaterOrEqual(t, len(photos), 4)
|
||||
})
|
||||
t.Run("form.face", func(t *testing.T) {
|
||||
var f form.PhotoSearch
|
||||
f.Query = "face:PN6QO5INYTUSAATOFL43LL2ABAV5ACZK"
|
||||
f.Count = 10
|
||||
f.Offset = 0
|
||||
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
//t.Logf("results: %+v", photos)
|
||||
assert.Equal(t, 1, len(photos))
|
||||
})
|
||||
t.Run("form.subject", func(t *testing.T) {
|
||||
var f form.PhotoSearch
|
||||
f.Query = "subject:jqu0xs11qekk9jx8"
|
||||
f.Count = 10
|
||||
f.Offset = 0
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -389,7 +404,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Count = 10
|
||||
f.Offset = 0
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -404,7 +419,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Count = 10
|
||||
f.Offset = 0
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -419,7 +434,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Count = 3
|
||||
f.Offset = 0
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -435,7 +450,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Count = 10
|
||||
f.Offset = 0
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -452,7 +467,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Offset = 0
|
||||
f.Archived = true
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -466,7 +481,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Count = 3
|
||||
f.Offset = 0
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -482,7 +497,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Offset = 0
|
||||
f.Error = true
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -497,7 +512,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Count = 10
|
||||
f.Offset = 0
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -511,7 +526,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Count = 10
|
||||
f.Offset = 0
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -526,7 +541,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Count = 10
|
||||
f.Offset = 0
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -540,7 +555,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Count = 10
|
||||
f.Offset = 0
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -555,7 +570,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Offset = 0
|
||||
f.Merged = true
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -569,7 +584,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Count = 5000
|
||||
f.Offset = 0
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -586,7 +601,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Year = 2790
|
||||
f.Album = "at9lxuqxpogaaba8"
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -598,7 +613,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Query = ""
|
||||
f.Albums = "Berlin"
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -609,7 +624,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
var f form.PhotoSearch
|
||||
f.State = "KwaZulu-Natal"
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -621,7 +636,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
var f form.PhotoSearch
|
||||
f.Category = "botanical garden"
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -634,7 +649,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
var f form.PhotoSearch
|
||||
f.Label = "botanical-garden|nature|landscape|park"
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -647,7 +662,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
var f form.PhotoSearch
|
||||
f.Primary = true
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -660,7 +675,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
var f form.PhotoSearch
|
||||
f.Query = "landscape"
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -681,7 +696,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Path = "/xxx/xxx/"
|
||||
f.Order = entity.SortOrderName
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -705,7 +720,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Filter = ""
|
||||
f.Order = entity.SortOrderAdded
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -721,7 +736,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
frm.Offset = 0
|
||||
frm.Order = entity.SortOrderEdited
|
||||
|
||||
photos, _, err := PhotoSearch(frm)
|
||||
photos, _, err := Photos(frm)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -730,7 +745,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
assert.GreaterOrEqual(t, len(photos), 1)
|
||||
|
||||
for _, r := range photos {
|
||||
assert.IsType(t, PhotoResult{}, r)
|
||||
assert.IsType(t, Photo{}, r)
|
||||
assert.NotEmpty(t, r.ID)
|
||||
assert.NotEmpty(t, r.CameraID)
|
||||
assert.NotEmpty(t, r.LensID)
|
||||
|
@ -750,7 +765,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
frm.Stackable = false
|
||||
frm.Unstacked = true
|
||||
|
||||
photos, _, err := PhotoSearch(frm)
|
||||
photos, _, err := Photos(frm)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -759,7 +774,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
assert.GreaterOrEqual(t, len(photos), 1)
|
||||
|
||||
for _, r := range photos {
|
||||
assert.IsType(t, PhotoResult{}, r)
|
||||
assert.IsType(t, Photo{}, r)
|
||||
assert.NotEmpty(t, r.ID)
|
||||
assert.NotEmpty(t, r.CameraID)
|
||||
assert.NotEmpty(t, r.LensID)
|
||||
|
@ -780,7 +795,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
frm.Title = "xxx|photowitheditedatdate"
|
||||
frm.Hash = "xxx|pcad9a68fa6acc5c5ba965adf6ec465ca42fd887"
|
||||
|
||||
photos, _, err := PhotoSearch(frm)
|
||||
photos, _, err := Photos(frm)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -789,7 +804,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
assert.GreaterOrEqual(t, len(photos), 1)
|
||||
|
||||
for _, r := range photos {
|
||||
assert.IsType(t, PhotoResult{}, r)
|
||||
assert.IsType(t, Photo{}, r)
|
||||
assert.NotEmpty(t, r.ID)
|
||||
assert.NotEmpty(t, r.CameraID)
|
||||
assert.NotEmpty(t, r.LensID)
|
||||
|
@ -805,7 +820,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
f.Count = 10
|
||||
f.Offset = 0
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -817,7 +832,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
var f form.PhotoSearch
|
||||
f.Faces = "Yes"
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -829,7 +844,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
var f form.PhotoSearch
|
||||
f.Faces = "No"
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -841,7 +856,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
var f form.PhotoSearch
|
||||
f.Faces = "2"
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -853,7 +868,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
var f form.PhotoSearch
|
||||
f.Filename = "1990/04/Quality1FavoriteTrue.jpg"
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -865,7 +880,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
var f form.PhotoSearch
|
||||
f.Original = "my-videos/IMG_88888" + "|" + "Vacation/exampleFileNameOriginal"
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -877,7 +892,7 @@ func TestPhotoSearch(t *testing.T) {
|
|||
var f form.PhotoSearch
|
||||
f.Stack = true
|
||||
|
||||
photos, _, err := PhotoSearch(f)
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
69
internal/search/search.go
Normal file
69
internal/search/search.go
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
|
||||
Package search performs common index search queries.
|
||||
|
||||
Copyright (c) 2018 - 2021 Michael Mayer <hello@photoprism.org>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
PhotoPrism® is a registered trademark of Michael Mayer. You may use it as required
|
||||
to describe our software, run your own server, for educational purposes, but not for
|
||||
offering commercial goods, products, or services without prior written permission.
|
||||
In other words, please ask.
|
||||
|
||||
Feel free to send an e-mail to hello@photoprism.org if you have questions,
|
||||
want to support our work, or just want to say hello.
|
||||
|
||||
Additional information can be found in our Developer Guide:
|
||||
https://docs.photoprism.org/developer-guide/
|
||||
|
||||
*/
|
||||
package search
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
)
|
||||
|
||||
var log = event.Log
|
||||
|
||||
// MaxResults is max result limit for queries.
|
||||
const MaxResults = 10000
|
||||
|
||||
// Radius is about 1 km.
|
||||
const Radius = 0.009
|
||||
|
||||
// Cols represents a list of database columns.
|
||||
type Cols []string
|
||||
|
||||
// Query searches given an originals path and a db instance.
|
||||
type Query struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// Count represents the total number of search results.
|
||||
type Count struct {
|
||||
Total int
|
||||
}
|
||||
|
||||
// Db returns a database connection instance.
|
||||
func Db() *gorm.DB {
|
||||
return entity.Db()
|
||||
}
|
||||
|
||||
// UnscopedDb returns an unscoped database connection instance.
|
||||
func UnscopedDb() *gorm.DB {
|
||||
return entity.Db().Unscoped()
|
||||
}
|
25
internal/search/search_test.go
Normal file
25
internal/search/search_test.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
log = logrus.StandardLogger()
|
||||
log.SetLevel(logrus.DebugLevel)
|
||||
|
||||
if err := os.Remove(".test.db"); err == nil {
|
||||
log.Debugln("removed .test.db")
|
||||
}
|
||||
|
||||
db := entity.InitTestDb(os.Getenv("PHOTOPRISM_TEST_DRIVER"), os.Getenv("PHOTOPRISM_TEST_DSN"))
|
||||
defer db.Close()
|
||||
|
||||
code := m.Run()
|
||||
|
||||
os.Exit(code)
|
||||
}
|
|
@ -1,44 +1,22 @@
|
|||
package query
|
||||
package search
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/capture"
|
||||
)
|
||||
|
||||
// SubjectResult represents a subject search result.
|
||||
type SubjectResult struct {
|
||||
SubjUID string `json:"UID"`
|
||||
MarkerUID string `json:"MarkerUID"`
|
||||
MarkerSrc string `json:"MarkerSrc,omitempty"`
|
||||
SubjType string `json:"Type"`
|
||||
SubjSlug string `json:"Slug"`
|
||||
SubjName string `json:"Name"`
|
||||
SubjAlias string `json:"Alias"`
|
||||
SubjFavorite bool `json:"Favorite"`
|
||||
SubjPrivate bool `json:"Private"`
|
||||
SubjExcluded bool `json:"Excluded"`
|
||||
FileCount int `json:"FileCount"`
|
||||
FileHash string `json:"FileHash"`
|
||||
CropArea string `json:"CropArea"`
|
||||
}
|
||||
|
||||
// SubjectResults represents subject search results.
|
||||
type SubjectResults []SubjectResult
|
||||
|
||||
// SubjectSearch searches subjects and returns them.
|
||||
func SubjectSearch(f form.SubjectSearch) (results SubjectResults, err error) {
|
||||
// Subjects searches subjects and returns them.
|
||||
func Subjects(f form.SubjectSearch) (results SubjectResults, err error) {
|
||||
if err := f.ParseQueryString(); err != nil {
|
||||
return results, err
|
||||
}
|
||||
|
||||
defer log.Debug(capture.Time(time.Now(), fmt.Sprintf("subjects: search %s", form.Serialize(f, true))))
|
||||
|
||||
// Base query.
|
||||
s := UnscopedDb().Table(entity.Subject{}.TableName()).
|
||||
Select(fmt.Sprintf("%s.*, m.file_hash, m.crop_area", entity.Subject{}.TableName()))
|
||||
|
@ -68,7 +46,7 @@ func SubjectSearch(f form.SubjectSearch) (results SubjectResults, err error) {
|
|||
}
|
||||
|
||||
if f.ID != "" {
|
||||
s = s.Where(fmt.Sprintf("%s.subj_uid IN (?)", entity.Subject{}.TableName()), strings.Split(f.ID, Or))
|
||||
s = s.Where(fmt.Sprintf("%s.subj_uid IN (?)", entity.Subject{}.TableName()), strings.Split(f.ID, txt.Or))
|
||||
|
||||
if result := s.Scan(&results); result.Error != nil {
|
||||
return results, result.Error
|
||||
|
@ -88,7 +66,7 @@ func SubjectSearch(f form.SubjectSearch) (results SubjectResults, err error) {
|
|||
}
|
||||
|
||||
if f.Type != "" {
|
||||
s = s.Where("subj_type IN (?)", strings.Split(f.Type, Or))
|
||||
s = s.Where("subj_type IN (?)", strings.Split(f.Type, txt.Or))
|
||||
}
|
||||
|
||||
if f.Favorite {
|
||||
|
@ -112,3 +90,60 @@ func SubjectSearch(f form.SubjectSearch) (results SubjectResults, err error) {
|
|||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// SubjectUIDs finds subject UIDs matching the search string, and removes names from the remaining query.
|
||||
func SubjectUIDs(s string) (result []string, names []string, remaining string) {
|
||||
if s == "" {
|
||||
return result, names, s
|
||||
}
|
||||
|
||||
type Matches struct {
|
||||
SubjUID string
|
||||
SubjName string
|
||||
SubjAlias string
|
||||
}
|
||||
|
||||
var matches []Matches
|
||||
|
||||
wheres := LikeAllNames(Cols{"subj_name", "subj_alias"}, s)
|
||||
|
||||
if len(wheres) == 0 {
|
||||
return result, names, s
|
||||
}
|
||||
|
||||
remaining = s
|
||||
|
||||
for _, where := range wheres {
|
||||
var subj []string
|
||||
|
||||
stmt := Db().Model(entity.Subject{})
|
||||
stmt = stmt.Where("?", gorm.Expr(where))
|
||||
|
||||
if err := stmt.Scan(&matches).Error; err != nil {
|
||||
log.Errorf("search: %s while finding subjects", err)
|
||||
} else if len(matches) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, m := range matches {
|
||||
subj = append(subj, m.SubjUID)
|
||||
names = append(names, m.SubjName)
|
||||
|
||||
for _, r := range txt.Words(strings.ToLower(m.SubjName)) {
|
||||
if len(r) > 1 {
|
||||
remaining = strings.ReplaceAll(remaining, r, "")
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range txt.Words(strings.ToLower(m.SubjAlias)) {
|
||||
if len(r) > 1 {
|
||||
remaining = strings.ReplaceAll(remaining, r, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, strings.Join(subj, txt.Or))
|
||||
}
|
||||
|
||||
return result, names, txt.NormalizeQuery(remaining)
|
||||
}
|
21
internal/search/subjects_results.go
Normal file
21
internal/search/subjects_results.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package search
|
||||
|
||||
// Subject represents a subject search result.
|
||||
type Subject struct {
|
||||
SubjUID string `json:"UID"`
|
||||
MarkerUID string `json:"MarkerUID"`
|
||||
MarkerSrc string `json:"MarkerSrc,omitempty"`
|
||||
SubjType string `json:"Type"`
|
||||
SubjSlug string `json:"Slug"`
|
||||
SubjName string `json:"Name"`
|
||||
SubjAlias string `json:"Alias"`
|
||||
SubjFavorite bool `json:"Favorite"`
|
||||
SubjPrivate bool `json:"Private"`
|
||||
SubjExcluded bool `json:"Excluded"`
|
||||
FileCount int `json:"FileCount"`
|
||||
FileHash string `json:"FileHash"`
|
||||
CropArea string `json:"CropArea"`
|
||||
}
|
||||
|
||||
// SubjectResults represents subject search results.
|
||||
type SubjectResults []Subject
|
|
@ -1,21 +1,19 @@
|
|||
package query
|
||||
package search
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSubjectSearch(t *testing.T) {
|
||||
func TestSubjects(t *testing.T) {
|
||||
t.Run("FindAll", func(t *testing.T) {
|
||||
results, err := SubjectSearch(form.SubjectSearch{Type: entity.SubjPerson})
|
||||
results, err := Subjects(form.SubjectSearch{Type: entity.SubjPerson})
|
||||
assert.NoError(t, err)
|
||||
// t.Logf("Subjects: %#v", results)
|
||||
assert.LessOrEqual(t, 3, len(results))
|
||||
})
|
||||
|
||||
}
|
|
@ -42,20 +42,28 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||
// JSON-REST API Version 1
|
||||
v1 := router.Group(conf.BaseUri(config.ApiUri))
|
||||
{
|
||||
api.GetStatus(v1)
|
||||
api.GetErrors(v1)
|
||||
|
||||
// Config options.
|
||||
api.GetConfig(v1)
|
||||
api.GetConfigOptions(v1)
|
||||
api.SaveConfigOptions(v1)
|
||||
|
||||
// User profile and settings.
|
||||
api.GetSettings(v1)
|
||||
api.SaveSettings(v1)
|
||||
|
||||
api.ChangePassword(v1)
|
||||
api.CreateSession(v1)
|
||||
api.DeleteSession(v1)
|
||||
|
||||
// External account management.
|
||||
api.SearchAccounts(v1)
|
||||
api.GetAccount(v1)
|
||||
api.GetAccountFolders(v1)
|
||||
api.ShareWithAccount(v1)
|
||||
api.CreateAccount(v1)
|
||||
api.DeleteAccount(v1)
|
||||
api.UpdateAccount(v1)
|
||||
|
||||
// Thumbnails and downloads.
|
||||
api.GetThumb(v1)
|
||||
api.GetThumbCrop(v1)
|
||||
api.GetDownload(v1)
|
||||
|
@ -63,11 +71,12 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||
api.CreateZip(v1)
|
||||
api.DownloadZip(v1)
|
||||
|
||||
api.GetGeo(v1)
|
||||
// Photos.
|
||||
api.SearchPhotos(v1)
|
||||
api.SearchPhotosGeo(v1)
|
||||
api.GetPhoto(v1)
|
||||
api.GetPhotoYaml(v1)
|
||||
api.UpdatePhoto(v1)
|
||||
api.GetPhotos(v1)
|
||||
api.GetPhotoDownload(v1)
|
||||
api.GetPhotoLinks(v1)
|
||||
api.CreatePhotoLink(v1)
|
||||
|
@ -87,47 +96,14 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||
api.PhotoPrimary(v1)
|
||||
api.PhotoUnstack(v1)
|
||||
|
||||
api.Upload(v1)
|
||||
api.StartImport(v1)
|
||||
api.CancelImport(v1)
|
||||
api.StartIndexing(v1)
|
||||
api.CancelIndexing(v1)
|
||||
|
||||
api.BatchPhotosApprove(v1)
|
||||
api.BatchPhotosArchive(v1)
|
||||
api.BatchPhotosRestore(v1)
|
||||
api.BatchPhotosPrivate(v1)
|
||||
api.BatchPhotosDelete(v1)
|
||||
api.BatchAlbumsDelete(v1)
|
||||
api.BatchLabelsDelete(v1)
|
||||
|
||||
api.GetSubjects(v1)
|
||||
api.GetSubject(v1)
|
||||
api.UpdateSubject(v1)
|
||||
api.LikeSubject(v1)
|
||||
api.DislikeSubject(v1)
|
||||
|
||||
api.LabelCover(v1)
|
||||
api.GetLabels(v1)
|
||||
api.UpdateLabel(v1)
|
||||
api.GetLabelLinks(v1)
|
||||
api.CreateLabelLink(v1)
|
||||
api.UpdateLabelLink(v1)
|
||||
api.DeleteLabelLink(v1)
|
||||
api.LikeLabel(v1)
|
||||
api.DislikeLabel(v1)
|
||||
|
||||
api.FolderCover(v1)
|
||||
api.GetFoldersOriginals(v1)
|
||||
api.GetFoldersImport(v1)
|
||||
|
||||
api.AlbumCover(v1)
|
||||
// Albums.
|
||||
api.SearchAlbums(v1)
|
||||
api.GetAlbum(v1)
|
||||
api.AlbumCover(v1)
|
||||
api.CreateAlbum(v1)
|
||||
api.UpdateAlbum(v1)
|
||||
api.DeleteAlbum(v1)
|
||||
api.DownloadAlbum(v1)
|
||||
api.GetAlbums(v1)
|
||||
api.GetAlbumLinks(v1)
|
||||
api.CreateAlbumLink(v1)
|
||||
api.UpdateAlbumLink(v1)
|
||||
|
@ -138,18 +114,55 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||
api.AddPhotosToAlbum(v1)
|
||||
api.RemovePhotosFromAlbum(v1)
|
||||
|
||||
api.GetAccounts(v1)
|
||||
api.GetAccount(v1)
|
||||
api.GetAccountFolders(v1)
|
||||
api.ShareWithAccount(v1)
|
||||
api.CreateAccount(v1)
|
||||
api.DeleteAccount(v1)
|
||||
api.UpdateAccount(v1)
|
||||
// Labels.
|
||||
api.SearchLabels(v1)
|
||||
api.LabelCover(v1)
|
||||
api.UpdateLabel(v1)
|
||||
api.GetLabelLinks(v1)
|
||||
api.CreateLabelLink(v1)
|
||||
api.UpdateLabelLink(v1)
|
||||
api.DeleteLabelLink(v1)
|
||||
api.LikeLabel(v1)
|
||||
api.DislikeLabel(v1)
|
||||
|
||||
api.SendFeedback(v1)
|
||||
// Folders.
|
||||
api.FolderCover(v1)
|
||||
api.GetFoldersOriginals(v1)
|
||||
api.GetFoldersImport(v1)
|
||||
|
||||
// People and other subjects.
|
||||
api.SearchSubjects(v1)
|
||||
api.GetSubject(v1)
|
||||
api.UpdateSubject(v1)
|
||||
api.LikeSubject(v1)
|
||||
api.DislikeSubject(v1)
|
||||
|
||||
// Faces.
|
||||
api.SearchFaces(v1)
|
||||
api.GetFace(v1)
|
||||
api.UpdateFace(v1)
|
||||
|
||||
// Indexing and importing.
|
||||
api.Upload(v1)
|
||||
api.StartImport(v1)
|
||||
api.CancelImport(v1)
|
||||
api.StartIndexing(v1)
|
||||
api.CancelIndexing(v1)
|
||||
|
||||
// Batch operations.
|
||||
api.BatchPhotosApprove(v1)
|
||||
api.BatchPhotosArchive(v1)
|
||||
api.BatchPhotosRestore(v1)
|
||||
api.BatchPhotosPrivate(v1)
|
||||
api.BatchPhotosDelete(v1)
|
||||
api.BatchAlbumsDelete(v1)
|
||||
api.BatchLabelsDelete(v1)
|
||||
|
||||
// Other.
|
||||
api.GetSvg(v1)
|
||||
|
||||
api.GetStatus(v1)
|
||||
api.GetErrors(v1)
|
||||
api.SendFeedback(v1)
|
||||
api.Websocket(v1)
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/remote"
|
||||
"github.com/photoprism/photoprism/internal/remote/webdav"
|
||||
"github.com/photoprism/photoprism/internal/search"
|
||||
"github.com/photoprism/photoprism/internal/thumb"
|
||||
)
|
||||
|
||||
|
@ -53,7 +54,7 @@ func (worker *Share) Start() (err error) {
|
|||
}
|
||||
|
||||
// Find accounts for which sharing is enabled
|
||||
accounts, err := query.AccountSearch(f)
|
||||
accounts, err := search.Accounts(f)
|
||||
|
||||
// Upload newly shared files
|
||||
for _, a := range accounts {
|
||||
|
|
|
@ -10,8 +10,8 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/mutex"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/remote"
|
||||
"github.com/photoprism/photoprism/internal/search"
|
||||
)
|
||||
|
||||
// Sync represents a sync worker.
|
||||
|
@ -59,7 +59,7 @@ func (worker *Sync) Start() (err error) {
|
|||
Sync: true,
|
||||
}
|
||||
|
||||
accounts, err := query.AccountSearch(f)
|
||||
accounts, err := search.Accounts(f)
|
||||
|
||||
for _, a := range accounts {
|
||||
if a.AccType != remote.ServiceWebDAV {
|
||||
|
|
|
@ -36,7 +36,7 @@ func PrefixedToken(lat, lng float64) string {
|
|||
return Prefix(Token(lat, lng))
|
||||
}
|
||||
|
||||
// Range returns a token range for finding nearby locations.
|
||||
// PrefixedRange returns a token range for finding nearby locations.
|
||||
func PrefixedRange(token string, levelUp int) (min, max string) {
|
||||
min, max = Range(token, levelUp)
|
||||
|
||||
|
|
38
pkg/txt/query.go
Normal file
38
pkg/txt/query.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package txt
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
Or = "|"
|
||||
And = "&"
|
||||
Plus = " + "
|
||||
OrEn = " or "
|
||||
AndEn = " and "
|
||||
WithEn = " with "
|
||||
InEn = " in "
|
||||
AtEn = " at "
|
||||
Space = " "
|
||||
Empty = ""
|
||||
)
|
||||
|
||||
// NormalizeQuery replaces search operator with default symbols.
|
||||
func NormalizeQuery(s string) string {
|
||||
s = strings.ToLower(Clip(s, ClipQuery))
|
||||
s = strings.ReplaceAll(s, OrEn, Or)
|
||||
s = strings.ReplaceAll(s, AndEn, And)
|
||||
s = strings.ReplaceAll(s, WithEn, And)
|
||||
s = strings.ReplaceAll(s, InEn, And)
|
||||
s = strings.ReplaceAll(s, AtEn, And)
|
||||
s = strings.ReplaceAll(s, Plus, And)
|
||||
s = strings.ReplaceAll(s, "%", "*")
|
||||
return strings.Trim(s, "+&|_-=!@$%^(){}\\<>,.;: ")
|
||||
}
|
||||
|
||||
// QueryTooShort tests if a search query is too short.
|
||||
func QueryTooShort(q string) bool {
|
||||
q = strings.Trim(q, "- '")
|
||||
|
||||
return q != Empty && len(q) < 3 && IsLatin(q)
|
||||
}
|
29
pkg/txt/query_test.go
Normal file
29
pkg/txt/query_test.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package txt
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNormalizeQuery(t *testing.T) {
|
||||
t.Run("Replace", func(t *testing.T) {
|
||||
q := NormalizeQuery("table spoon & usa | img% json OR BILL!")
|
||||
assert.Equal(t, "table spoon & usa | img* json|bill", q)
|
||||
})
|
||||
}
|
||||
|
||||
func TestQueryTooShort(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
assert.False(t, QueryTooShort(""))
|
||||
})
|
||||
t.Run("IsTooShort", func(t *testing.T) {
|
||||
assert.True(t, QueryTooShort("aa"))
|
||||
})
|
||||
t.Run("Chinese", func(t *testing.T) {
|
||||
assert.False(t, QueryTooShort("李"))
|
||||
})
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
assert.False(t, QueryTooShort("foo"))
|
||||
})
|
||||
}
|
Loading…
Reference in a new issue