mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 08:44:04 +01:00
Backend: Improve labels, keywords and caching
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
3
Makefile
3
Makefile
@@ -72,6 +72,9 @@ build-js:
|
|||||||
build-go:
|
build-go:
|
||||||
rm -f $(BINARY_NAME)
|
rm -f $(BINARY_NAME)
|
||||||
scripts/build.sh debug $(BINARY_NAME)
|
scripts/build.sh debug $(BINARY_NAME)
|
||||||
|
build-race:
|
||||||
|
rm -f $(BINARY_NAME)
|
||||||
|
scripts/build.sh race $(BINARY_NAME)
|
||||||
build-static:
|
build-static:
|
||||||
rm -f $(BINARY_NAME)
|
rm -f $(BINARY_NAME)
|
||||||
scripts/build.sh static $(BINARY_NAME)
|
scripts/build.sh static $(BINARY_NAME)
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func GetFolders(router *gin.RouterGroup, conf *config.Config, urlPath, rootName,
|
|||||||
if cached {
|
if cached {
|
||||||
if cacheData, ok := gc.Get(cacheKey); ok {
|
if cacheData, ok := gc.Get(cacheKey); ok {
|
||||||
log.Debugf("cache hit for %s [%s]", cacheKey, time.Since(start))
|
log.Debugf("cache hit for %s [%s]", cacheKey, time.Since(start))
|
||||||
c.JSON(http.StatusOK, cacheData.(FoldersResponse))
|
c.JSON(http.StatusOK, cacheData.(*FoldersResponse))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@ func GetFolders(router *gin.RouterGroup, conf *config.Config, urlPath, rootName,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cached {
|
if cached {
|
||||||
gc.Set(cacheKey, resp, time.Minute*5)
|
gc.Set(cacheKey, &resp, time.Minute*5)
|
||||||
log.Debugf("cached %s [%s]", cacheKey, time.Since(start))
|
log.Debugf("cached %s [%s]", cacheKey, time.Since(start))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/photoprism/photoprism/internal/classify"
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/entity"
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
"github.com/photoprism/photoprism/internal/event"
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
@@ -114,11 +115,11 @@ func RemovePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if label.LabelSrc == entity.SrcManual {
|
if label.LabelSrc == classify.SrcManual || label.LabelSrc == classify.SrcKeyword{
|
||||||
entity.Db().Delete(&label)
|
logError("label", entity.Db().Delete(&label).Error)
|
||||||
} else {
|
} else {
|
||||||
label.Uncertainty = 100
|
label.Uncertainty = 100
|
||||||
entity.Db().Save(&label)
|
logError("label", entity.Db().Save(&label).Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
p, err := query.PhotoPreloadByUID(c.Param("uid"))
|
p, err := query.PhotoPreloadByUID(c.Param("uid"))
|
||||||
@@ -128,6 +129,8 @@ func RemovePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logError("label", p.RemoveKeyword(label.Label.LabelName))
|
||||||
|
|
||||||
if err := p.Save(); err != nil {
|
if err := p.Save(); err != nil {
|
||||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func GetThumbnail(router *gin.RouterGroup, conf *config.Config) {
|
|||||||
if cacheData, ok := gc.Get(cacheKey); ok {
|
if cacheData, ok := gc.Get(cacheKey); ok {
|
||||||
log.Debugf("cache hit for %s [%s]", cacheKey, time.Since(start))
|
log.Debugf("cache hit for %s [%s]", cacheKey, time.Since(start))
|
||||||
|
|
||||||
cached := cacheData.(ThumbCache)
|
cached := cacheData.(*ThumbCache)
|
||||||
|
|
||||||
if !fs.FileExists(cached.FileName) {
|
if !fs.FileExists(cached.FileName) {
|
||||||
log.Errorf("thumbnail: %s not found", fileHash)
|
log.Errorf("thumbnail: %s not found", fileHash)
|
||||||
@@ -137,7 +137,7 @@ func GetThumbnail(router *gin.RouterGroup, conf *config.Config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cache thumbnail filename.
|
// Cache thumbnail filename.
|
||||||
gc.Set(cacheKey, ThumbCache{thumbnail, f.ShareFileName()}, time.Hour*24)
|
gc.Set(cacheKey, &ThumbCache{thumbnail, f.ShareFileName()}, time.Hour*24)
|
||||||
log.Debugf("cached %s [%s]", cacheKey, time.Since(start))
|
log.Debugf("cached %s [%s]", cacheKey, time.Since(start))
|
||||||
|
|
||||||
if c.Query("download") != "" {
|
if c.Query("download") != "" {
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ const (
|
|||||||
SrcManual = "manual"
|
SrcManual = "manual"
|
||||||
SrcLocation = "location"
|
SrcLocation = "location"
|
||||||
SrcImage = "image"
|
SrcImage = "image"
|
||||||
|
SrcKeyword = "keyword"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ type Labels []Label
|
|||||||
func (l Labels) Len() int { return len(l) }
|
func (l Labels) Len() int { return len(l) }
|
||||||
func (l Labels) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
|
func (l Labels) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
|
||||||
func (l Labels) Less(i, j int) bool {
|
func (l Labels) Less(i, j int) bool {
|
||||||
if l[i].Uncertainty >= 100 {
|
if l[i].Uncertainty >= 100 || l[i].Source == SrcKeyword {
|
||||||
return false
|
return false
|
||||||
} else if l[j].Uncertainty >= 100 {
|
} else if l[j].Uncertainty >= 100 || l[j].Source == SrcKeyword {
|
||||||
return true
|
return true
|
||||||
} else if l[i].Priority == l[j].Priority {
|
} else if l[i].Priority == l[j].Priority {
|
||||||
return l[i].Uncertainty < l[j].Uncertainty
|
return l[i].Uncertainty < l[j].Uncertainty
|
||||||
@@ -36,7 +36,7 @@ func (l Labels) AppendLabel(label Label) Labels {
|
|||||||
// Keywords returns all keywords contains in Labels and their categories
|
// Keywords returns all keywords contains in Labels and their categories
|
||||||
func (l Labels) Keywords() (result []string) {
|
func (l Labels) Keywords() (result []string) {
|
||||||
for _, label := range l {
|
for _, label := range l {
|
||||||
if label.Uncertainty >= 100 {
|
if label.Uncertainty >= 100 || label.Source == SrcKeyword {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,19 @@ func FirstOrCreateLabel(m *Label) *Label {
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindLabel returns an existing row if exists.
|
||||||
|
func FindLabel(s string) *Label {
|
||||||
|
labelSlug := slug.Make(txt.Clip(s, txt.ClipSlug))
|
||||||
|
|
||||||
|
result := Label{}
|
||||||
|
|
||||||
|
if err := Db().Where("label_slug = ? OR custom_slug = ?", labelSlug, labelSlug).First(&result).Error; err == nil {
|
||||||
|
return &result
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// AfterCreate sets the New column used for database callback
|
// AfterCreate sets the New column used for database callback
|
||||||
func (m *Label) AfterCreate(scope *gorm.Scope) error {
|
func (m *Label) AfterCreate(scope *gorm.Scope) error {
|
||||||
m.New = true
|
m.New = true
|
||||||
|
|||||||
@@ -102,12 +102,16 @@ func SavePhotoForm(model Photo, form form.Photo, geoApi string) error {
|
|||||||
model.Details.Keywords = strings.Join(txt.UniqueWords(w), ", ")
|
model.Details.Keywords = strings.Join(txt.UniqueWords(w), ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := model.SyncKeywordLabels(); err != nil {
|
||||||
|
log.Errorf("photo: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := model.UpdateTitle(model.ClassifyLabels()); err != nil {
|
if err := model.UpdateTitle(model.ClassifyLabels()); err != nil {
|
||||||
log.Warn(err)
|
log.Warn(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := model.IndexKeywords(); err != nil {
|
if err := model.IndexKeywords(); err != nil {
|
||||||
log.Error(err)
|
log.Errorf("photo: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
edited := time.Now().UTC()
|
edited := time.Now().UTC()
|
||||||
@@ -146,7 +150,7 @@ func (m *Photo) Save() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := m.IndexKeywords(); err != nil {
|
if err := m.IndexKeywords(); err != nil {
|
||||||
log.Error(err)
|
log.Errorf("photo: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
m.PhotoQuality = m.QualityScore()
|
m.PhotoQuality = m.QualityScore()
|
||||||
@@ -216,10 +220,39 @@ func (m *Photo) BeforeSave(scope *gorm.Scope) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveKeyword removes a word from photo keywords.
|
||||||
|
func (m *Photo) RemoveKeyword(w string) error {
|
||||||
|
if !m.DetailsLoaded() {
|
||||||
|
return fmt.Errorf("can't remove keyword, details not loaded")
|
||||||
|
}
|
||||||
|
|
||||||
|
words := txt.RemoveFromWords(txt.Words(m.Details.Keywords), w)
|
||||||
|
|
||||||
|
m.Details.Keywords = strings.Join(words, ", ")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncKeywordLabels maintains the label / photo relationship for existing labels and keywords.
|
||||||
|
func (m *Photo) SyncKeywordLabels() error {
|
||||||
|
keywords := txt.UniqueKeywords(m.Details.Keywords)
|
||||||
|
|
||||||
|
var labelIds []uint
|
||||||
|
|
||||||
|
for _, w := range keywords {
|
||||||
|
if label := FindLabel(w); label != nil {
|
||||||
|
labelIds = append(labelIds, label.ID)
|
||||||
|
FirstOrCreatePhotoLabel(NewPhotoLabel(m.ID, label.ID, 5, classify.SrcKeyword))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Db().Where("label_src = ? AND photo_id = ? AND label_id NOT IN (?)", classify.SrcKeyword, m.ID, labelIds).Delete(&PhotoLabel{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
// IndexKeywords adds given keywords to the photo entry
|
// IndexKeywords adds given keywords to the photo entry
|
||||||
func (m *Photo) IndexKeywords() error {
|
func (m *Photo) IndexKeywords() error {
|
||||||
if !m.DetailsLoaded() {
|
if !m.DetailsLoaded() {
|
||||||
return fmt.Errorf("photo: can't index keywords, details not loaded for %s", m.PhotoUID)
|
return fmt.Errorf("can't index keywords, details not loaded")
|
||||||
}
|
}
|
||||||
|
|
||||||
db := Db()
|
db := Db()
|
||||||
@@ -240,7 +273,7 @@ func (m *Photo) IndexKeywords() error {
|
|||||||
kw := FirstOrCreateKeyword(NewKeyword(w))
|
kw := FirstOrCreateKeyword(NewKeyword(w))
|
||||||
|
|
||||||
if kw == nil {
|
if kw == nil {
|
||||||
log.Errorf("photo: index keyword should not be nil - bug?")
|
log.Errorf("index keyword should not be nil - bug?")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ func (m *Photo) Maintain() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := m.IndexKeywords(); err != nil {
|
if err := m.IndexKeywords(); err != nil {
|
||||||
log.Error(err)
|
log.Errorf("photo: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
m.PhotoQuality = m.QualityScore()
|
m.PhotoQuality = m.QualityScore()
|
||||||
|
|||||||
@@ -54,9 +54,9 @@ func FindLocation(id string) (result Location, err error) {
|
|||||||
|
|
||||||
if hit, ok := cache.Get(id); ok {
|
if hit, ok := cache.Get(id); ok {
|
||||||
log.Debugf("api: cache hit for lat %f, lng %f (%s)", lat, lng, ApiName)
|
log.Debugf("api: cache hit for lat %f, lng %f (%s)", lat, lng, ApiName)
|
||||||
result = hit.(Location)
|
cached := hit.(*Location)
|
||||||
result.Cached = true
|
cached.Cached = true
|
||||||
return result, nil
|
return *cached, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
url := fmt.Sprintf(ReverseLookupURL, id)
|
url := fmt.Sprintf(ReverseLookupURL, id)
|
||||||
@@ -101,7 +101,7 @@ func FindLocation(id string) (result Location, err error) {
|
|||||||
return result, fmt.Errorf("api: no result for %s (%s)", id, ApiName)
|
return result, fmt.Errorf("api: no result for %s (%s)", id, ApiName)
|
||||||
}
|
}
|
||||||
|
|
||||||
cache.Set(id, result, gc.DefaultExpiration)
|
cache.Set(id, &result, gc.DefaultExpiration)
|
||||||
|
|
||||||
result.Cached = false
|
result.Cached = false
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/karrick/godirwalk"
|
"github.com/karrick/godirwalk"
|
||||||
@@ -249,10 +248,6 @@ func (c *Convert) ToJpeg(image *MediaFile, hidden bool) (*MediaFile, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unclear if this is really necessary here, but safe is safe.
|
|
||||||
runtime.LockOSThread()
|
|
||||||
defer runtime.UnlockOSThread()
|
|
||||||
|
|
||||||
if useMutex {
|
if useMutex {
|
||||||
// Make sure only one command is executed at a time.
|
// Make sure only one command is executed at a time.
|
||||||
// See https://photo.stackexchange.com/questions/105969/darktable-cli-fails-because-of-locked-database-file
|
// See https://photo.stackexchange.com/questions/105969/darktable-cli-fails-because-of-locked-database-file
|
||||||
|
|||||||
@@ -462,8 +462,12 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := photo.SyncKeywordLabels(); err != nil {
|
||||||
|
log.Errorf("index: %s for %s", err, quotedName)
|
||||||
|
}
|
||||||
|
|
||||||
if err := photo.IndexKeywords(); err != nil {
|
if err := photo.IndexKeywords(); err != nil {
|
||||||
log.Errorf("%s for %s", err, quotedName)
|
log.Errorf("index: %s for %s", err, quotedName)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if photo.PhotoQuality >= 0 {
|
if photo.PhotoQuality >= 0 {
|
||||||
|
|||||||
@@ -636,6 +636,10 @@ func (m *MediaFile) decodeDimensions() error {
|
|||||||
if m.IsJpeg() {
|
if m.IsJpeg() {
|
||||||
file, err := os.Open(m.FileName())
|
file, err := os.Open(m.FileName())
|
||||||
|
|
||||||
|
if err != nil || file == nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
size, _, err := image.DecodeConfig(file)
|
size, _, err := image.DecodeConfig(file)
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ func Geo(f form.GeoSearch) (results GeoResults, err error) {
|
|||||||
Joins(`JOIN files ON files.photo_id = photos.id AND
|
Joins(`JOIN files ON files.photo_id = photos.id AND
|
||||||
files.file_missing = 0 AND files.file_primary AND files.deleted_at IS NULL`).
|
files.file_missing = 0 AND files.file_primary AND files.deleted_at IS NULL`).
|
||||||
Where("photos.deleted_at IS NULL").
|
Where("photos.deleted_at IS NULL").
|
||||||
Where("photos.photo_lat <> 0")
|
Where("photos.photo_lat <> 0").
|
||||||
|
Group("photos.id, files.id")
|
||||||
|
|
||||||
f.Query = txt.Clip(f.Query, txt.ClipKeyword)
|
f.Query = txt.Clip(f.Query, txt.ClipKeyword)
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ func PhotoSearch(f form.PhotoSearch) (results PhotoResults, count int, err error
|
|||||||
Joins("JOIN cameras ON photos.camera_id = cameras.id").
|
Joins("JOIN cameras ON photos.camera_id = cameras.id").
|
||||||
Joins("JOIN lenses ON photos.lens_id = lenses.id").
|
Joins("JOIN lenses ON photos.lens_id = lenses.id").
|
||||||
Joins("JOIN places ON photos.place_uid = places.place_uid").
|
Joins("JOIN places ON photos.place_uid = places.place_uid").
|
||||||
Where("files.file_type = 'jpg' OR files.file_video = 1")
|
Where("files.file_type = 'jpg' OR files.file_video = 1").
|
||||||
|
Group("photos.id, files.id")
|
||||||
|
|
||||||
// Shortcut for known photo ids.
|
// Shortcut for known photo ids.
|
||||||
if f.ID != "" {
|
if f.ID != "" {
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ func (c Client) CreateDir(dir string) error {
|
|||||||
func (c Client) Upload(from, to string) error {
|
func (c Client) Upload(from, to string) error {
|
||||||
file, err := os.Open(from)
|
file, err := os.Open(from)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil || file == nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ func InitDatabase(port uint, password string) error {
|
|||||||
|
|
||||||
db, err := sql.Open("mysql", fmt.Sprintf("root:@tcp(localhost:%d)/", port))
|
db, err := sql.Open("mysql", fmt.Sprintf("root:@tcp(localhost:%d)/", port))
|
||||||
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug(err.Error())
|
log.Debug(err.Error())
|
||||||
log.Debug("database login as root with password")
|
log.Debug("database login as root with password")
|
||||||
@@ -23,6 +21,8 @@ func InitDatabase(port uint, password string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
if password != "" {
|
if password != "" {
|
||||||
log.Debug("set database password")
|
log.Debug("set database password")
|
||||||
|
|
||||||
|
|||||||
@@ -31,11 +31,12 @@ func Zip(filename string, files []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func AddToZip(zipWriter *zip.Writer, filename string) error {
|
func AddToZip(zipWriter *zip.Writer, filename string) error {
|
||||||
|
|
||||||
fileToZip, err := os.Open(filename)
|
fileToZip, err := os.Open(filename)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer fileToZip.Close()
|
defer fileToZip.Close()
|
||||||
|
|
||||||
// Get the file information
|
// Get the file information
|
||||||
@@ -64,6 +65,7 @@ func AddToZip(zipWriter *zip.Writer, filename string) error {
|
|||||||
// Extract Zip file in destination directory
|
// Extract Zip file in destination directory
|
||||||
func Unzip(src, dest string) (fileNames []string, err error) {
|
func Unzip(src, dest string) (fileNames []string, err error) {
|
||||||
r, err := zip.OpenReader(src)
|
r, err := zip.OpenReader(src)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fileNames, err
|
return fileNames, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,28 @@ func UniqueWords(words []string) (results []string) {
|
|||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveFromWords removes words from a string slice and returns the sorted result.
|
||||||
|
func RemoveFromWords(words []string, remove string) (results []string) {
|
||||||
|
remove = strings.ToLower(remove)
|
||||||
|
last := ""
|
||||||
|
|
||||||
|
SortCaseInsensitive(words)
|
||||||
|
|
||||||
|
for _, w := range words {
|
||||||
|
w = strings.ToLower(w)
|
||||||
|
|
||||||
|
if len(w) < 3 || w == last || strings.Contains(remove, w){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
last = w
|
||||||
|
|
||||||
|
results = append(results, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
// UniqueKeywords returns a slice of unique and sorted keywords without stopwords.
|
// UniqueKeywords returns a slice of unique and sorted keywords without stopwords.
|
||||||
func UniqueKeywords(s string) (results []string) {
|
func UniqueKeywords(s string) (results []string) {
|
||||||
last := ""
|
last := ""
|
||||||
|
|||||||
@@ -126,3 +126,14 @@ func TestUniqueKeywords(t *testing.T) {
|
|||||||
assert.Equal(t, []string(nil), result)
|
assert.Equal(t, []string(nil), result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRemoveFromWords(t *testing.T) {
|
||||||
|
t.Run("brown apple", func(t *testing.T) {
|
||||||
|
result := RemoveFromWords([]string{"lazy", "jpg", "Brown", "apple", "brown", "new-york", "JPG"}, "brown apple")
|
||||||
|
assert.Equal(t, []string{"jpg", "lazy", "new-york"}, result)
|
||||||
|
})
|
||||||
|
t.Run("empty", func(t *testing.T) {
|
||||||
|
result := RemoveFromWords([]string{"lazy", "jpg", "Brown", "apple"}, "")
|
||||||
|
assert.Equal(t, []string{"apple", "brown", "jpg", "lazy"}, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ if [[ $1 == "debug" ]]; then
|
|||||||
go build -ldflags "-X main.version=${PHOTOPRISM_DATE}-${PHOTOPRISM_VERSION}-${PHOTOPRISM_OS}-${PHOTOPRISM_ARCH}-DEBUG" -o $2 cmd/photoprism/photoprism.go
|
go build -ldflags "-X main.version=${PHOTOPRISM_DATE}-${PHOTOPRISM_VERSION}-${PHOTOPRISM_OS}-${PHOTOPRISM_ARCH}-DEBUG" -o $2 cmd/photoprism/photoprism.go
|
||||||
du -h $2
|
du -h $2
|
||||||
echo "Done."
|
echo "Done."
|
||||||
|
elif [[ $1 == "race" ]]; then
|
||||||
|
echo "Building with data race detector..."
|
||||||
|
go build -race -ldflags "-X main.version=${PHOTOPRISM_DATE}-${PHOTOPRISM_VERSION}-${PHOTOPRISM_OS}-${PHOTOPRISM_ARCH}-DEBUG" -o $2 cmd/photoprism/photoprism.go
|
||||||
|
du -h $2
|
||||||
|
echo "Done."
|
||||||
elif [[ $1 == "static" ]]; then
|
elif [[ $1 == "static" ]]; then
|
||||||
echo "Building static production binary..."
|
echo "Building static production binary..."
|
||||||
go build -a -v -ldflags "-linkmode external -extldflags \"-static -L /usr/lib -ltensorflow\" -s -w -X main.version=${PHOTOPRISM_DATE}-${PHOTOPRISM_VERSION}-${PHOTOPRISM_OS}-${PHOTOPRISM_ARCH}" -o $2 cmd/photoprism/photoprism.go
|
go build -a -v -ldflags "-linkmode external -extldflags \"-static -L /usr/lib -ltensorflow\" -s -w -X main.version=${PHOTOPRISM_DATE}-${PHOTOPRISM_VERSION}-${PHOTOPRISM_OS}-${PHOTOPRISM_ARCH}" -o $2 cmd/photoprism/photoprism.go
|
||||||
|
|||||||
Reference in New Issue
Block a user