From b38dac8e8cabdd123afa7403d84dccd9cb401c74 Mon Sep 17 00:00:00 2001
From: Michael Mayer <michael@photoprism.app>
Date: Wed, 28 Dec 2022 17:50:08 +0100
Subject: [PATCH] Search: Find pictures by Exif UID, XMP Document ID or
 Instance ID #3035

Signed-off-by: Michael Mayer <michael@photoprism.app>
---
 internal/form/search_photos.go     | 3 ++-
 internal/form/search_photos_geo.go | 3 ++-
 internal/search/photos.go          | 9 +++++++++
 internal/search/photos_geo.go      | 9 +++++++++
 4 files changed, 22 insertions(+), 2 deletions(-)

diff --git a/internal/form/search_photos.go b/internal/form/search_photos.go
index eed1835a1..f6f7d0015 100644
--- a/internal/form/search_photos.go
+++ b/internal/form/search_photos.go
@@ -11,7 +11,8 @@ type SearchPhotos struct {
 	Query     string    `form:"q"`
 	Scope     string    `form:"s" serialize:"-" example:"s:ariqwb43p5dh9h13" notes:"Limits the results to one album or another scope, if specified"`
 	Filter    string    `form:"filter" serialize:"-" notes:"-"`
-	UID       string    `form:"uid" example:"uid:pqbcf5j446s0futy" notes:"Search for specific files or photos, only exact matches"`
+	ID        string    `form:"id" example:"id:123e4567-e89b-..." notes:"Finds pictures by Exif UID, XMP Document ID or Instance ID"`
+	UID       string    `form:"uid" example:"uid:pqbcf5j446s0futy" notes:"Limits results to the specified internal unique IDs"`
 	Type      string    `form:"type" example:"type:raw" notes:"Media Type (image, video, raw, live, animated); OR search with |"`
 	Path      string    `form:"path" example:"path:2020/Holiday" notes:"Path Name, OR search with |, supports * wildcards"`
 	Folder    string    `form:"folder" example:"folder:\"*/2020\"" notes:"Path Name, OR search with |, supports * wildcards"` // Alias for Path
diff --git a/internal/form/search_photos_geo.go b/internal/form/search_photos_geo.go
index 3886ffc46..c8fe1f49d 100644
--- a/internal/form/search_photos_geo.go
+++ b/internal/form/search_photos_geo.go
@@ -11,7 +11,8 @@ type SearchPhotosGeo struct {
 	Query     string    `form:"q"`
 	Scope     string    `form:"s" serialize:"-" example:"s:ariqwb43p5dh9h13" notes:"Limits the results to one album or another scope, if specified"`
 	Filter    string    `form:"filter" serialize:"-" notes:"-"`
-	UID       string    `form:"uid" example:"uid:pqbcf5j446s0futy" notes:"Search for specific files or photos, only exact matches"`
+	ID        string    `form:"id" example:"id:123e4567-e89b-..." notes:"Finds pictures by Exif UID, XMP Document ID or Instance ID"`
+	UID       string    `form:"uid" example:"uid:pqbcf5j446s0futy" notes:"Limits results to the specified internal unique IDs"`
 	Near      string    `form:"near"`
 	Type      string    `form:"type"`
 	Path      string    `form:"path"`
diff --git a/internal/search/photos.go b/internal/search/photos.go
index 812c08724..7afabe23d 100644
--- a/internal/search/photos.go
+++ b/internal/search/photos.go
@@ -217,6 +217,15 @@ func searchPhotos(f form.SearchPhotos, sess *entity.Session, resultCols string)
 		}
 	}
 
+	// Find Unique Image ID (Exif), Document ID, or Instance ID (XMP)?
+	if txt.NotEmpty(f.ID) {
+		for _, id := range SplitAnd(strings.ToLower(f.ID)) {
+			if ids := SplitOr(id); len(ids) > 0 {
+				s = s.Where("files.instance_id IN (?) OR photos.uuid IN (?)", ids, ids)
+			}
+		}
+	}
+
 	// Filter by label, label category and keywords.
 	var categories []entity.Category
 	var labels []entity.Label
diff --git a/internal/search/photos_geo.go b/internal/search/photos_geo.go
index 99c91ad9b..120c11982 100644
--- a/internal/search/photos_geo.go
+++ b/internal/search/photos_geo.go
@@ -175,6 +175,15 @@ func UserPhotosGeo(f form.SearchPhotosGeo, sess *entity.Session) (results GeoRes
 		}
 	}
 
+	// Find Unique Image ID (Exif), Document ID, or Instance ID (XMP)?
+	if txt.NotEmpty(f.ID) {
+		for _, id := range SplitAnd(strings.ToLower(f.ID)) {
+			if ids := SplitOr(id); len(ids) > 0 {
+				s = s.Where("files.instance_id IN (?) OR photos.uuid IN (?)", ids, ids)
+			}
+		}
+	}
+
 	// Set search filters based on search terms.
 	if terms := txt.SearchTerms(f.Query); f.Query != "" && len(terms) == 0 {
 		if f.Title == "" {