parent
ccb27454a6
commit
c48310f077
31 changed files with 688 additions and 257 deletions
|
@ -119,7 +119,7 @@ export default [
|
|||
path: "/moments",
|
||||
component: Albums,
|
||||
meta: { title: $gettext("Moments"), auth: true },
|
||||
props: { view: "moment", staticFilter: { type: "moment" } },
|
||||
props: { view: "moment", staticFilter: { type: "moment", order: "moment" } },
|
||||
},
|
||||
{
|
||||
name: "moment",
|
||||
|
@ -132,7 +132,7 @@ export default [
|
|||
path: "/albums",
|
||||
component: Albums,
|
||||
meta: { title: $gettext("Albums"), auth: true },
|
||||
props: { view: "album", staticFilter: { type: "album" } },
|
||||
props: { view: "album", staticFilter: { type: "album", order: "relevance" } },
|
||||
},
|
||||
{
|
||||
name: "album",
|
||||
|
@ -145,7 +145,7 @@ export default [
|
|||
path: "/calendar",
|
||||
component: Albums,
|
||||
meta: { title: $gettext("Calendar"), auth: true },
|
||||
props: { view: "month", staticFilter: { type: "month" } },
|
||||
props: { view: "month", staticFilter: { type: "month", order: "newest" } },
|
||||
},
|
||||
{
|
||||
name: "month",
|
||||
|
@ -158,7 +158,7 @@ export default [
|
|||
path: "/folders",
|
||||
component: Albums,
|
||||
meta: { title: $gettext("Folders"), auth: true },
|
||||
props: { view: "folder", staticFilter: { type: "folder", order: "default" } },
|
||||
props: { view: "folder", staticFilter: { type: "folder", order: "newest" } },
|
||||
},
|
||||
{
|
||||
name: "folder",
|
||||
|
@ -225,7 +225,7 @@ export default [
|
|||
path: "/states",
|
||||
component: Albums,
|
||||
meta: { title: $gettext("Places"), auth: true },
|
||||
props: { view: "state", staticFilter: { type: "state" } },
|
||||
props: { view: "state", staticFilter: { type: "state", order: "place" } },
|
||||
},
|
||||
{
|
||||
name: "state",
|
||||
|
|
8
go.mod
8
go.mod
|
@ -21,7 +21,7 @@ require (
|
|||
github.com/go-playground/validator/v10 v10.9.0 // indirect
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/open-location-code/go v0.0.0-20211110234603-604ed00fe9d8
|
||||
github.com/google/open-location-code/go v0.0.0-20211115190122-6707912175c3
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/gosimple/slug v1.11.2
|
||||
github.com/h2non/filetype v1.1.1
|
||||
|
@ -38,7 +38,6 @@ require (
|
|||
github.com/manifoldco/promptui v0.9.0
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-sqlite3 v2.0.1+incompatible // indirect
|
||||
github.com/melihmucuk/geocache v0.0.0-20160621165317-521b336a001c
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/montanaflynn/stats v0.6.6
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
|
@ -46,7 +45,6 @@ require (
|
|||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/sevlyar/go-daemon v0.1.5
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/studio-b12/gowebdav v0.0.0-20211106090535-29e74efa701f
|
||||
|
@ -55,9 +53,9 @@ require (
|
|||
github.com/ulule/deepcopier v0.0.0-20200430083143-45decc6639b6
|
||||
github.com/urfave/cli v1.22.5
|
||||
go4.org v0.0.0-20201209231011-d4a079459e60 // indirect
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa
|
||||
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect
|
||||
golang.org/x/net v0.0.0-20211111083644-e5c967477495
|
||||
golang.org/x/net v0.0.0-20211116231205-47ca1ff31462
|
||||
golang.org/x/sys v0.0.0-20211109065445-02f5c0300f6e // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
gonum.org/v1/gonum v0.9.3
|
||||
|
|
14
go.sum
14
go.sum
|
@ -151,10 +151,10 @@ github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
|||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/open-location-code/go v0.0.0-20211109014933-06433367679b h1:6rfkSqY/nWZGdgpfCLumEAh3Remb/v1eyrGnFt5dCIs=
|
||||
github.com/google/open-location-code/go v0.0.0-20211109014933-06433367679b/go.mod h1:eJfRN6aj+kR/rnua/rw9jAgYhqoMHldQkdTi+sePRKk=
|
||||
github.com/google/open-location-code/go v0.0.0-20211110234603-604ed00fe9d8 h1:+C1yt4bGEM1u3akLWEDqtNhRV28xyCrPscLEgE3NGYc=
|
||||
github.com/google/open-location-code/go v0.0.0-20211110234603-604ed00fe9d8/go.mod h1:eJfRN6aj+kR/rnua/rw9jAgYhqoMHldQkdTi+sePRKk=
|
||||
github.com/google/open-location-code/go v0.0.0-20211115190122-6707912175c3 h1:wXfRNEdg7/vPWFXtECTJulGgxygx0xZqlB0g3JMJlXs=
|
||||
github.com/google/open-location-code/go v0.0.0-20211115190122-6707912175c3/go.mod h1:eJfRN6aj+kR/rnua/rw9jAgYhqoMHldQkdTi+sePRKk=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
|
@ -225,8 +225,6 @@ github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC
|
|||
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||
github.com/mattn/go-sqlite3 v2.0.1+incompatible h1:xQ15muvnzGBHpIpdrNi1DA5x0+TcBZzsIDwmw9uTHzw=
|
||||
github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/melihmucuk/geocache v0.0.0-20160621165317-521b336a001c h1:1ErTnOL2d0OvfUABvEjGcPM8cKSLxYZpJiYS4BfQ3o4=
|
||||
github.com/melihmucuk/geocache v0.0.0-20160621165317-521b336a001c/go.mod h1:CX2bLGC22DrgJTaYvKt+lOi3BACGNA60hbFXh2iWebs=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
|
@ -265,8 +263,6 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
|
|||
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
|
||||
github.com/sevlyar/go-daemon v0.1.5 h1:Zy/6jLbM8CfqJ4x4RPr7MJlSKt90f00kNM1D401C+Qk=
|
||||
github.com/sevlyar/go-daemon v0.1.5/go.mod h1:6dJpPatBT9eUwM5VCw9Bt6CdX9Tk6UWvhW3MebLDRKE=
|
||||
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
|
@ -314,6 +310,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
|||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI=
|
||||
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
|
@ -377,10 +375,10 @@ golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/
|
|||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211108170745-6635138e15ea h1:FosBMXtOc8Tp9Hbo4ltl1WJSrTVewZU8MPnTPY2HdH8=
|
||||
golang.org/x/net v0.0.0-20211108170745-6635138e15ea/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211111083644-e5c967477495 h1:cjxxlQm6d4kYbhpZ2ghvmI8xnq0AG+jXmzrhzfkyu5A=
|
||||
golang.org/x/net v0.0.0-20211111083644-e5c967477495/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211116231205-47ca1ff31462 h1:2vmJlzGKvQ7e/X9XT0XydeWDxmqx8DnegiIMRT+5ssI=
|
||||
golang.org/x/net v0.0.0-20211116231205-47ca1ff31462/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
|
|
@ -2,7 +2,6 @@ package api
|
|||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
@ -41,6 +40,8 @@ func SaveAlbumAsYaml(a entity.Album) {
|
|||
}
|
||||
}
|
||||
|
||||
// SearchAlbums finds albums and returns them as JSON.
|
||||
//
|
||||
// GET /api/v1/albums
|
||||
func SearchAlbums(router *gin.RouterGroup) {
|
||||
router.GET("/albums", func(c *gin.Context) {
|
||||
|
@ -81,6 +82,8 @@ func SearchAlbums(router *gin.RouterGroup) {
|
|||
})
|
||||
}
|
||||
|
||||
// GetAlbum returns album details as JSON.
|
||||
//
|
||||
// GET /api/v1/albums/:uid
|
||||
func GetAlbum(router *gin.RouterGroup) {
|
||||
router.GET("/albums/:uid", func(c *gin.Context) {
|
||||
|
@ -103,6 +106,8 @@ func GetAlbum(router *gin.RouterGroup) {
|
|||
})
|
||||
}
|
||||
|
||||
// CreateAlbum adds a new album.
|
||||
//
|
||||
// POST /api/v1/albums
|
||||
func CreateAlbum(router *gin.RouterGroup) {
|
||||
router.POST("/albums", func(c *gin.Context) {
|
||||
|
@ -142,6 +147,8 @@ func CreateAlbum(router *gin.RouterGroup) {
|
|||
})
|
||||
}
|
||||
|
||||
// UpdateAlbum updates album metadata like title and description.
|
||||
//
|
||||
// PUT /api/v1/albums/:uid
|
||||
func UpdateAlbum(router *gin.RouterGroup) {
|
||||
router.PUT("/albums/:uid", func(c *gin.Context) {
|
||||
|
@ -192,6 +199,8 @@ func UpdateAlbum(router *gin.RouterGroup) {
|
|||
})
|
||||
}
|
||||
|
||||
// DeleteAlbum deletes an existing album.
|
||||
//
|
||||
// DELETE /api/v1/albums/:uid
|
||||
func DeleteAlbum(router *gin.RouterGroup) {
|
||||
router.DELETE("/albums/:uid", func(c *gin.Context) {
|
||||
|
@ -229,6 +238,8 @@ func DeleteAlbum(router *gin.RouterGroup) {
|
|||
})
|
||||
}
|
||||
|
||||
// LikeAlbum sets the favorite flag for an album.
|
||||
//
|
||||
// POST /api/v1/albums/:uid/like
|
||||
//
|
||||
// Parameters:
|
||||
|
@ -265,6 +276,8 @@ func LikeAlbum(router *gin.RouterGroup) {
|
|||
})
|
||||
}
|
||||
|
||||
// DislikeAlbum removes the favorite flag from an album.
|
||||
//
|
||||
// DELETE /api/v1/albums/:uid/like
|
||||
//
|
||||
// Parameters:
|
||||
|
@ -301,6 +314,8 @@ func DislikeAlbum(router *gin.RouterGroup) {
|
|||
})
|
||||
}
|
||||
|
||||
// CloneAlbums creates a new album containing pictures from other albums.
|
||||
//
|
||||
// POST /api/v1/albums/:uid/clone
|
||||
func CloneAlbums(router *gin.RouterGroup) {
|
||||
router.POST("/albums/:uid/clone", func(c *gin.Context) {
|
||||
|
@ -357,6 +372,8 @@ func CloneAlbums(router *gin.RouterGroup) {
|
|||
})
|
||||
}
|
||||
|
||||
// AddPhotosToAlbum adds photos to an album.
|
||||
//
|
||||
// POST /api/v1/albums/:uid/photos
|
||||
func AddPhotosToAlbum(router *gin.RouterGroup) {
|
||||
router.POST("/albums/:uid/photos", func(c *gin.Context) {
|
||||
|
@ -410,6 +427,8 @@ func AddPhotosToAlbum(router *gin.RouterGroup) {
|
|||
})
|
||||
}
|
||||
|
||||
// RemovePhotosFromAlbum removes photos from an album.
|
||||
//
|
||||
// DELETE /api/v1/albums/:uid/photos
|
||||
func RemovePhotosFromAlbum(router *gin.RouterGroup) {
|
||||
router.DELETE("/albums/:uid/photos", func(c *gin.Context) {
|
||||
|
@ -459,6 +478,8 @@ func RemovePhotosFromAlbum(router *gin.RouterGroup) {
|
|||
})
|
||||
}
|
||||
|
||||
// DownloadAlbum streams the album contents as zip archive.
|
||||
//
|
||||
// GET /api/v1/albums/:uid/dl
|
||||
func DownloadAlbum(router *gin.RouterGroup) {
|
||||
router.GET("/albums/:uid/dl", func(c *gin.Context) {
|
||||
|
@ -482,13 +503,7 @@ func DownloadAlbum(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
albumName := strings.Title(a.AlbumSlug)
|
||||
|
||||
if len(albumName) < 2 {
|
||||
albumName = fmt.Sprintf("photoprism-album-%s", a.AlbumUID)
|
||||
}
|
||||
|
||||
zipFileName := fmt.Sprintf("%s.zip", albumName)
|
||||
zipFileName := a.ZipName()
|
||||
|
||||
AddDownloadHeader(c, zipFileName)
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ func TestGetConfig(t *testing.T) {
|
|||
GetConfig(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/config")
|
||||
val := gjson.Get(r.Body.String(), "flags")
|
||||
assert.Equal(t, "public debug experimental settings", val.String())
|
||||
assert.Equal(t, "public debug sponsor experimental settings", val.String())
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ import (
|
|||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/manifoldco/promptui"
|
||||
|
||||
"github.com/dustin/go-humanize/english"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
|
@ -15,11 +17,11 @@ import (
|
|||
// PlacesCommand registers the places subcommands.
|
||||
var PlacesCommand = cli.Command{
|
||||
Name: "places",
|
||||
Usage: "Location information subcommands",
|
||||
Usage: "Geolocation management subcommands",
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "update",
|
||||
Usage: "Fetches updated location data",
|
||||
Usage: "Updates the index with the latest geodata from our backend",
|
||||
Action: placesUpdateAction,
|
||||
},
|
||||
},
|
||||
|
@ -29,6 +31,16 @@ var PlacesCommand = cli.Command{
|
|||
func placesUpdateAction(ctx *cli.Context) error {
|
||||
start := time.Now()
|
||||
|
||||
confirmPrompt := promptui.Prompt{
|
||||
Label: "Interrupting the update may result in inconsistent data. Proceed?",
|
||||
IsConfirm: true,
|
||||
}
|
||||
|
||||
if _, err := confirmPrompt.Run(); err != nil {
|
||||
// Abort.
|
||||
return nil
|
||||
}
|
||||
|
||||
conf := config.NewConfig(ctx)
|
||||
service.SetConfig(conf)
|
||||
|
||||
|
@ -49,7 +61,7 @@ func placesUpdateAction(ctx *cli.Context) error {
|
|||
} else {
|
||||
elapsed := time.Since(start)
|
||||
|
||||
log.Infof("updated %s in %s", english.Plural(len(updated), "location", "locations"), elapsed)
|
||||
log.Infof("updated %s in %s", english.Plural(len(updated), "place", "places"), elapsed)
|
||||
}
|
||||
|
||||
conf.Shutdown()
|
||||
|
|
|
@ -133,6 +133,10 @@ func (c *Config) Flags() (flags []string) {
|
|||
flags = append(flags, "debug")
|
||||
}
|
||||
|
||||
if c.Sponsor() {
|
||||
flags = append(flags, "sponsor")
|
||||
}
|
||||
|
||||
if c.Experimental() {
|
||||
flags = append(flags, "experimental")
|
||||
}
|
||||
|
@ -245,7 +249,7 @@ func (c *Config) GuestConfig() ClientConfig {
|
|||
Faces: true,
|
||||
Classification: true,
|
||||
},
|
||||
Flags: "readonly public shared",
|
||||
Flags: strings.Join(c.Flags(), " "),
|
||||
Mode: "guest",
|
||||
Name: c.Name(),
|
||||
BaseUri: c.BaseUri(""),
|
||||
|
|
|
@ -33,7 +33,7 @@ func TestConfig_Flags(t *testing.T) {
|
|||
config.settings.UI.Scrollbar = false
|
||||
|
||||
result := config.Flags()
|
||||
assert.Equal(t, []string{"public", "debug", "experimental", "readonly", "settings", "hide-scrollbar"}, result)
|
||||
assert.Equal(t, []string{"public", "debug", "sponsor", "experimental", "readonly", "settings", "hide-scrollbar"}, result)
|
||||
|
||||
config.options.Experimental = false
|
||||
config.options.ReadOnly = false
|
||||
|
|
|
@ -39,6 +39,11 @@ var once sync.Once
|
|||
var LowMem = false
|
||||
var TotalMem uint64
|
||||
|
||||
const MsgFreeBeer = "Help us make a difference and become a sponsor today!"
|
||||
const MsgFundingInfo = "Visit https://docs.photoprism.org/funding/ to learn more."
|
||||
const MsgSponsorCommand = "Since running this command puts additional load on our infrastructure," +
|
||||
" we unfortunately can't offer it for free."
|
||||
|
||||
const ApiUri = "/api/v1"
|
||||
const StaticUri = "/static"
|
||||
const DefaultWakeupInterval = int(15 * 60)
|
||||
|
@ -170,6 +175,12 @@ func (c *Config) Init() error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Show funding info?
|
||||
if !c.Sponsor() {
|
||||
log.Info(MsgFreeBeer)
|
||||
log.Info(MsgFundingInfo)
|
||||
}
|
||||
|
||||
if insensitive, err := c.CaseInsensitive(); err != nil {
|
||||
return err
|
||||
} else if insensitive {
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
package crop
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestThumbFileName(t *testing.T) {
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
package crop
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestName_Jpeg(t *testing.T) {
|
||||
|
|
|
@ -448,6 +448,17 @@ func (m *Album) Title() string {
|
|||
return m.AlbumTitle
|
||||
}
|
||||
|
||||
// ZipName returns the zip download filename.
|
||||
func (m *Album) ZipName() string {
|
||||
s := slug.Make(m.AlbumTitle)
|
||||
|
||||
if len(s) < 2 {
|
||||
s = fmt.Sprintf("photoprism-album-%s", m.AlbumUID)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s.zip", s)
|
||||
}
|
||||
|
||||
// AddPhotos adds photos to an existing album.
|
||||
func (m *Album) AddPhotos(UIDs []string) (added PhotoAlbums) {
|
||||
for _, uid := range UIDs {
|
||||
|
|
|
@ -18,6 +18,8 @@ var cellMutex = sync.Mutex{}
|
|||
type Cell struct {
|
||||
ID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;" json:"ID" yaml:"ID"`
|
||||
CellName string `gorm:"type:VARCHAR(200);" json:"Name" yaml:"Name,omitempty"`
|
||||
CellStreet string `gorm:"type:VARCHAR(100);" json:"Street" yaml:"Street,omitempty"`
|
||||
CellPostcode string `gorm:"type:VARCHAR(50);" json:"Postcode" yaml:"Postcode,omitempty"`
|
||||
CellCategory string `gorm:"type:VARCHAR(50);" json:"Category" yaml:"Category,omitempty"`
|
||||
PlaceID string `gorm:"type:VARBINARY(42);default:'zz'" json:"-" yaml:"PlaceID"`
|
||||
Place *Place `gorm:"PRELOAD:true" json:"Place" yaml:"-"`
|
||||
|
@ -36,6 +38,8 @@ var UnknownLocation = Cell{
|
|||
Place: &UnknownPlace,
|
||||
PlaceID: UnknownID,
|
||||
CellName: "",
|
||||
CellStreet: "",
|
||||
CellPostcode: "",
|
||||
CellCategory: "",
|
||||
}
|
||||
|
||||
|
@ -55,14 +59,14 @@ func NewCell(lat, lng float32) *Cell {
|
|||
|
||||
// Refresh updates the index by retrieving the latest data from an external API.
|
||||
func (m *Cell) Refresh(api string) (err error) {
|
||||
start := time.Now()
|
||||
|
||||
// Unknown?
|
||||
if m.Unknown() {
|
||||
// Skip.
|
||||
return nil
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
// Initialize.
|
||||
l := &maps.Location{
|
||||
ID: s2.NormalizeToken(m.ID),
|
||||
|
@ -79,44 +83,50 @@ func (m *Cell) Refresh(api string) (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
cellTable := Cell{}.TableName()
|
||||
placeTable := Place{}.TableName()
|
||||
oldPlaceID := m.PlaceID
|
||||
|
||||
place := Place{}
|
||||
cellMutex.Lock()
|
||||
defer cellMutex.Unlock()
|
||||
|
||||
// Find existing place by label.
|
||||
if err := UnscopedDb().Where("place_label = ?", l.Label()).First(&place).Error; err != nil {
|
||||
log.Tracef("places: %s for cell %s", err, m.ID)
|
||||
place = Place{ID: m.ID}
|
||||
} else {
|
||||
log.Tracef("places: found matching place %s for cell %s", place.ID, m.ID)
|
||||
place := Place{
|
||||
ID: l.PlaceID(),
|
||||
PlaceLabel: l.Label(),
|
||||
PlaceDistrict: l.District(),
|
||||
PlaceCity: l.City(),
|
||||
PlaceState: l.State(),
|
||||
PlaceCountry: l.CountryCode(),
|
||||
PlaceKeywords: l.KeywordString(),
|
||||
PhotoCount: 1,
|
||||
}
|
||||
|
||||
// Update place.
|
||||
if place.ID == "" {
|
||||
// Do nothing.
|
||||
} else if res := UnscopedDb().Table(placeTable).Where("id = ?", place.ID).UpdateColumns(Values{
|
||||
"place_label": l.Label(),
|
||||
"place_city": l.City(),
|
||||
"place_district": l.District(),
|
||||
"place_state": l.State(),
|
||||
"place_country": l.CountryCode(),
|
||||
"place_keywords": l.KeywordString(),
|
||||
}); res.Error != nil {
|
||||
log.Tracef("places: %s for cell %s", err, m.ID)
|
||||
} else if res.RowsAffected > 0 {
|
||||
// Update cell place id, name, and category.
|
||||
log.Tracef("places: updating place, name, and category for cell %s", m.ID)
|
||||
err = UnscopedDb().Table(cellTable).Where("id = ?", m.ID).
|
||||
UpdateColumns(Values{"cell_name": l.Name(), "cell_category": l.Category(), "place_id": place.ID}).Error
|
||||
// Create or update place.
|
||||
if err = place.Save(); err != nil {
|
||||
log.Warnf("place: failed updating %s [%s]", place.ID, time.Since(start))
|
||||
} else {
|
||||
// Update cell name and category.
|
||||
log.Tracef("places: updating name and category for cell %s", m.ID)
|
||||
err = UnscopedDb().Table(cellTable).Where("id = ?", m.ID).
|
||||
UpdateColumns(Values{"cell_name": l.Name(), "cell_category": l.Category()}).Error
|
||||
m.Place = &place
|
||||
m.PlaceID = l.PlaceID()
|
||||
log.Tracef("place: updated %s [%s]", place.ID, time.Since(start))
|
||||
}
|
||||
|
||||
log.Debugf("places: refreshed cell %s [%s]", txt.Quote(m.ID), time.Since(start))
|
||||
m.CellName = l.Name()
|
||||
m.CellStreet = l.Street()
|
||||
m.CellPostcode = l.Postcode()
|
||||
m.CellCategory = l.Category()
|
||||
|
||||
// Update cell.
|
||||
err = m.Save()
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("place: failed updating %s [%s]", m.ID, time.Since(start))
|
||||
return err
|
||||
} else if oldPlaceID != m.PlaceID {
|
||||
err = UnscopedDb().Table(Photo{}.TableName()).
|
||||
Where("place_id = ?", oldPlaceID).
|
||||
UpdateColumn("place_id", m.PlaceID).
|
||||
Error
|
||||
}
|
||||
|
||||
log.Debugf("place: updated %s [%s]", m.ID, time.Since(start))
|
||||
|
||||
return err
|
||||
}
|
||||
|
@ -127,7 +137,7 @@ func (m *Cell) Find(api string) error {
|
|||
db := Db()
|
||||
|
||||
if err := db.Preload("Place").First(m, "id = ?", m.ID).Error; err == nil {
|
||||
log.Debugf("location: found cell %s", m.ID)
|
||||
log.Debugf("place: found cell %s", m.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -139,12 +149,13 @@ func (m *Cell) Find(api string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if found := FindPlace(l.PrefixedToken(), l.Label()); found != nil {
|
||||
if found := FindPlace(l.PlaceID(), l.Label()); found != nil {
|
||||
m.Place = found
|
||||
} else {
|
||||
place := &Place{
|
||||
ID: l.PrefixedToken(),
|
||||
ID: l.PlaceID(),
|
||||
PlaceLabel: l.Label(),
|
||||
PlaceDistrict: l.District(),
|
||||
PlaceCity: l.City(),
|
||||
PlaceState: l.State(),
|
||||
PlaceCountry: l.CountryCode(),
|
||||
|
@ -157,33 +168,35 @@ func (m *Cell) Find(api string) error {
|
|||
"count": 1,
|
||||
})
|
||||
|
||||
log.Infof("location: added place %s [%s]", place.ID, time.Since(start))
|
||||
log.Infof("place: added %s [%s]", place.ID, time.Since(start))
|
||||
|
||||
m.Place = place
|
||||
} else if found := FindPlace(l.PrefixedToken(), l.Label()); found != nil {
|
||||
} else if found := FindPlace(l.PlaceID(), l.Label()); found != nil {
|
||||
m.Place = found
|
||||
} else {
|
||||
log.Errorf("location: %s (create place %s)", createErr, place.ID)
|
||||
log.Errorf("place: %s (create %s)", createErr, place.ID)
|
||||
m.Place = &UnknownPlace
|
||||
}
|
||||
}
|
||||
|
||||
m.PlaceID = m.Place.ID
|
||||
m.CellName = l.Name()
|
||||
m.CellStreet = l.Street()
|
||||
m.CellPostcode = l.Postcode()
|
||||
m.CellCategory = l.Category()
|
||||
|
||||
cellMutex.Lock()
|
||||
defer cellMutex.Unlock()
|
||||
|
||||
if createErr := db.Create(m).Error; createErr == nil {
|
||||
log.Debugf("location: added cell %s [%s]", m.ID, time.Since(start))
|
||||
log.Debugf("place: added cell %s [%s]", m.ID, time.Since(start))
|
||||
return nil
|
||||
} else if findErr := db.Preload("Place").First(m, "id = ?", m.ID).Error; findErr != nil {
|
||||
log.Errorf("location: %s (create cell %s)", createErr, m.ID)
|
||||
log.Errorf("location: %s (find cell %s)", findErr, m.ID)
|
||||
log.Errorf("place: %s (create cell %s)", createErr, m.ID)
|
||||
log.Errorf("place: %s (find cell %s)", findErr, m.ID)
|
||||
return createErr
|
||||
} else {
|
||||
log.Debugf("location: found cell %s [%s]", m.ID, time.Since(start))
|
||||
log.Debugf("place: found cell %s [%s]", m.ID, time.Since(start))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -194,15 +207,25 @@ func (m *Cell) Create() error {
|
|||
return Db().Create(m).Error
|
||||
}
|
||||
|
||||
// Save updates the existing or inserts a new row.
|
||||
func (m *Cell) Save() error {
|
||||
return Db().Save(m).Error
|
||||
}
|
||||
|
||||
// Delete removes the entity from the index.
|
||||
func (m *Cell) Delete() (err error) {
|
||||
return UnscopedDb().Delete(m).Error
|
||||
}
|
||||
|
||||
// FirstOrCreateCell fetches an existing row, inserts a new row or nil in case of errors.
|
||||
func FirstOrCreateCell(m *Cell) *Cell {
|
||||
if m.ID == "" {
|
||||
log.Errorf("location: cell must not be empty")
|
||||
log.Errorf("place: cell must not be empty")
|
||||
return nil
|
||||
}
|
||||
|
||||
if m.PlaceID == "" {
|
||||
log.Errorf("location: place must not be empty (find or create cell %s)", m.ID)
|
||||
log.Errorf("place: id must not be empty (find or create cell %s)", m.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -215,7 +238,7 @@ func FirstOrCreateCell(m *Cell) *Cell {
|
|||
} else if err := Db().Where("id = ?", m.ID).Preload("Place").First(&result).Error; err == nil {
|
||||
return &result
|
||||
} else {
|
||||
log.Errorf("location: %s (find or create cell %s)", createErr, m.ID)
|
||||
log.Errorf("place: %s (find or create cell %s)", createErr, m.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -224,15 +247,17 @@ func FirstOrCreateCell(m *Cell) *Cell {
|
|||
// Keywords returns search keywords for a location.
|
||||
func (m *Cell) Keywords() (result []string) {
|
||||
if m.Place == nil {
|
||||
log.Errorf("location: place for cell %s is nil - you might have found a bug", m.ID)
|
||||
log.Errorf("place: info for cell %s is nil - you might have found a bug", m.ID)
|
||||
return result
|
||||
}
|
||||
|
||||
result = append(result, txt.Keywords(txt.ReplaceSpaces(m.District(), "-"))...)
|
||||
result = append(result, txt.Keywords(txt.ReplaceSpaces(m.City(), "-"))...)
|
||||
result = append(result, txt.Keywords(txt.ReplaceSpaces(m.State(), "-"))...)
|
||||
result = append(result, txt.Keywords(txt.ReplaceSpaces(m.CountryName(), "-"))...)
|
||||
result = append(result, txt.Keywords(m.Category())...)
|
||||
result = append(result, txt.Keywords(m.Name())...)
|
||||
result = append(result, txt.Keywords(m.Street())...)
|
||||
result = append(result, txt.Keywords(m.Category())...)
|
||||
result = append(result, txt.Words(m.Place.PlaceKeywords)...)
|
||||
|
||||
result = txt.UniqueWords(result)
|
||||
|
@ -255,6 +280,26 @@ func (m *Cell) NoName() bool {
|
|||
return m.CellName == ""
|
||||
}
|
||||
|
||||
// Street returns the street name if any.
|
||||
func (m *Cell) Street() string {
|
||||
return m.CellStreet
|
||||
}
|
||||
|
||||
// NoStreet checks if the location has a street.
|
||||
func (m *Cell) NoStreet() bool {
|
||||
return m.CellStreet == ""
|
||||
}
|
||||
|
||||
// Postcode returns the postcode if any.
|
||||
func (m *Cell) Postcode() string {
|
||||
return m.CellPostcode
|
||||
}
|
||||
|
||||
// NoPostcode checks if the location has a postcode.
|
||||
func (m *Cell) NoPostcode() bool {
|
||||
return m.CellPostcode == ""
|
||||
}
|
||||
|
||||
// Category returns the location category
|
||||
func (m *Cell) Category() string {
|
||||
return m.CellCategory
|
||||
|
@ -270,7 +315,12 @@ func (m *Cell) Label() string {
|
|||
return m.Place.Label()
|
||||
}
|
||||
|
||||
// City returns the location place city
|
||||
// District returns the district name if any.
|
||||
func (m *Cell) District() string {
|
||||
return m.Place.District()
|
||||
}
|
||||
|
||||
// City returns the location city name if any.
|
||||
func (m *Cell) City() string {
|
||||
return m.Place.City()
|
||||
}
|
||||
|
|
|
@ -64,11 +64,17 @@ const (
|
|||
// Sort Orders
|
||||
|
||||
const (
|
||||
SortOrderRelevance = "relevance"
|
||||
SortOrderCount = "count"
|
||||
SortOrderAdded = "added"
|
||||
SortOrderEdited = "edited"
|
||||
SortOrderNewest = "newest"
|
||||
SortOrderOldest = "oldest"
|
||||
SortOrderPlace = "place"
|
||||
SortOrderMoment = "moment"
|
||||
SortOrderName = "name"
|
||||
SortOrderPath = "path"
|
||||
SortOrderSlug = "slug"
|
||||
SortOrderCategory = "category"
|
||||
SortOrderSimilar = "similar"
|
||||
SortOrderRelevance = "relevance"
|
||||
SortOrderEdited = "edited"
|
||||
)
|
||||
|
|
|
@ -22,6 +22,43 @@ type DbProvider interface {
|
|||
Db() *gorm.DB
|
||||
}
|
||||
|
||||
// RecreateTable drops and recreates the database table for a clean start.
|
||||
func RecreateTable(models ...interface{}) (err error) {
|
||||
n := len(models)
|
||||
|
||||
// Return if no models were provided.
|
||||
if n < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Drop existing tables.
|
||||
if err = Db().DropTable(models...).Error; err != nil {
|
||||
return fmt.Errorf("%s (drop table)", err)
|
||||
}
|
||||
|
||||
done := 0
|
||||
|
||||
// Create dropped tables.
|
||||
for i := 0; i < 15; i++ {
|
||||
for m := range models {
|
||||
if err = Db().CreateTable(models[m]).Error; err != nil {
|
||||
log.Debugf("entity: %s (create table)", err)
|
||||
} else {
|
||||
done++
|
||||
}
|
||||
|
||||
if done >= n {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Wait a second to avoid timing issues.
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// IsDialect returns true if the given sql dialect is used.
|
||||
func IsDialect(name string) bool {
|
||||
return name == Db().Dialect().GetName()
|
||||
|
|
32
internal/entity/db_test.go
Normal file
32
internal/entity/db_test.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestEntity is an entity dedicated to test database management functionality.
|
||||
type TestEntity struct {
|
||||
ID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;" json:"TestID" yaml:"TestID"`
|
||||
TestLabel string `gorm:"type:VARCHAR(400);unique_index;" json:"Label" yaml:"Label"`
|
||||
TestCount int `gorm:"default:1" json:"Count" yaml:"-"`
|
||||
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
|
||||
}
|
||||
|
||||
// TableName returns the entity database table name.
|
||||
func (TestEntity) TableName() string {
|
||||
return "test_ignore"
|
||||
}
|
||||
|
||||
func TestRecreateTable(t *testing.T) {
|
||||
t.Run("TestEntity", func(t *testing.T) {
|
||||
if err := Db().CreateTable(TestEntity{}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := RecreateTable(TestEntity{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -6,6 +6,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/maps"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
var placeMutex = sync.Mutex{}
|
||||
|
@ -13,12 +14,12 @@ var placeMutex = sync.Mutex{}
|
|||
// Place used to associate photos to places
|
||||
type Place struct {
|
||||
ID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;" json:"PlaceID" yaml:"PlaceID"`
|
||||
PlaceLabel string `gorm:"type:VARBINARY(512);unique_index;" json:"Label" yaml:"Label"`
|
||||
PlaceCity string `gorm:"type:VARCHAR(128);" json:"City" yaml:"City,omitempty"`
|
||||
PlaceState string `gorm:"type:VARCHAR(128);" json:"State" yaml:"State,omitempty"`
|
||||
PlaceDistrict string `gorm:"type:VARCHAR(128);" json:"District" yaml:"District,omitempty"`
|
||||
PlaceLabel string `gorm:"type:VARCHAR(400);unique_index;" json:"Label" yaml:"Label"`
|
||||
PlaceDistrict string `gorm:"type:VARCHAR(100);index;" json:"District" yaml:"District,omitempty"`
|
||||
PlaceCity string `gorm:"type:VARCHAR(100);index;" json:"City" yaml:"City,omitempty"`
|
||||
PlaceState string `gorm:"type:VARCHAR(100);index;" json:"State" yaml:"State,omitempty"`
|
||||
PlaceCountry string `gorm:"type:VARBINARY(2);" json:"Country" yaml:"Country,omitempty"`
|
||||
PlaceKeywords string `gorm:"type:VARCHAR(255);" json:"Keywords" yaml:"Keywords,omitempty"`
|
||||
PlaceKeywords string `gorm:"type:VARCHAR(300);" json:"Keywords" yaml:"Keywords,omitempty"`
|
||||
PlaceFavorite bool `json:"Favorite" yaml:"Favorite,omitempty"`
|
||||
PhotoCount int `gorm:"default:1" json:"PhotoCount" yaml:"-"`
|
||||
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
|
||||
|
@ -34,8 +35,8 @@ func (Place) TableName() string {
|
|||
var UnknownPlace = Place{
|
||||
ID: UnknownID,
|
||||
PlaceLabel: "Unknown",
|
||||
PlaceCity: "Unknown",
|
||||
PlaceDistrict: "Unknown",
|
||||
PlaceCity: "Unknown",
|
||||
PlaceState: "Unknown",
|
||||
PlaceCountry: UnknownID,
|
||||
PlaceKeywords: "",
|
||||
|
@ -54,7 +55,7 @@ func FindPlace(id string, label string) *Place {
|
|||
|
||||
if label == "" {
|
||||
if err := Db().Where("id = ?", id).First(&place).Error; err != nil {
|
||||
log.Debugf("places: failed finding %s", id)
|
||||
log.Debugf("place: %s no found", txt.Quote(id))
|
||||
return nil
|
||||
} else {
|
||||
return &place
|
||||
|
@ -85,15 +86,28 @@ func (m *Place) Create() error {
|
|||
return Db().Create(m).Error
|
||||
}
|
||||
|
||||
// Save updates the existing or inserts a new row.
|
||||
func (m *Place) Save() error {
|
||||
placeMutex.Lock()
|
||||
defer placeMutex.Unlock()
|
||||
|
||||
return Db().Save(m).Error
|
||||
}
|
||||
|
||||
// Delete removes the entity from the index.
|
||||
func (m *Place) Delete() (err error) {
|
||||
return UnscopedDb().Delete(m).Error
|
||||
}
|
||||
|
||||
// FirstOrCreatePlace fetches an existing row, inserts a new row or nil in case of errors.
|
||||
func FirstOrCreatePlace(m *Place) *Place {
|
||||
if m.ID == "" {
|
||||
log.Errorf("places: place must not be empty (find or create)")
|
||||
log.Errorf("place: id must not be empty (find or create)")
|
||||
return nil
|
||||
}
|
||||
|
||||
if m.PlaceLabel == "" {
|
||||
log.Errorf("places: label must not be empty (find or create place %s)", m.ID)
|
||||
log.Errorf("place: label must not be empty (find or create place %s)", m.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -106,7 +120,7 @@ func FirstOrCreatePlace(m *Place) *Place {
|
|||
} else if err := Db().Where("id = ? OR place_label = ?", m.ID, m.PlaceLabel).First(&result).Error; err == nil {
|
||||
return &result
|
||||
} else {
|
||||
log.Errorf("places: %s (create place %s)", createErr, m.ID)
|
||||
log.Errorf("place: %s (create %s)", createErr, m.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -122,7 +136,12 @@ func (m Place) Label() string {
|
|||
return m.PlaceLabel
|
||||
}
|
||||
|
||||
// City returns place City
|
||||
// District returns the place district name if any.
|
||||
func (m Place) District() string {
|
||||
return m.PlaceDistrict
|
||||
}
|
||||
|
||||
// City returns place city if any.
|
||||
func (m Place) City() string {
|
||||
return m.PlaceCity
|
||||
}
|
||||
|
|
|
@ -18,6 +18,8 @@ type Location struct {
|
|||
LocLat float64 `json:"lat"`
|
||||
LocLng float64 `json:"lng"`
|
||||
LocName string `json:"name"`
|
||||
LocStreet string `json:"street"`
|
||||
LocPostcode string `json:"postcode"`
|
||||
LocCategory string `json:"category"`
|
||||
Place Place `json:"place"`
|
||||
Cached bool `json:"-"`
|
||||
|
@ -31,20 +33,6 @@ var UserAgent = "PhotoPrism/0.0.0"
|
|||
var ReverseLookupURL = "https://places.photoprism.app/v1/location/%s"
|
||||
var client = &http.Client{Timeout: 60 * time.Second}
|
||||
|
||||
func NewLocation(id string, lat, lng float64, name, category string, place Place, cached bool) *Location {
|
||||
result := &Location{
|
||||
ID: id,
|
||||
LocLat: lat,
|
||||
LocLng: lng,
|
||||
LocName: name,
|
||||
LocCategory: category,
|
||||
Place: place,
|
||||
Cached: cached,
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func FindLocation(id string) (result Location, err error) {
|
||||
if len(id) > 16 || len(id) == 0 {
|
||||
return result, fmt.Errorf("invalid cell %s (%s)", id, ApiName)
|
||||
|
@ -119,14 +107,26 @@ func FindLocation(id string) (result Location, err error) {
|
|||
return result, nil
|
||||
}
|
||||
|
||||
func (l Location) CellID() (result string) {
|
||||
func (l Location) CellID() string {
|
||||
return l.ID
|
||||
}
|
||||
|
||||
func (l Location) PlaceID() string {
|
||||
return l.Place.PlaceID
|
||||
}
|
||||
|
||||
func (l Location) Name() (result string) {
|
||||
return strings.SplitN(l.LocName, "/", 2)[0]
|
||||
}
|
||||
|
||||
func (l Location) Street() (result string) {
|
||||
return strings.SplitN(l.LocStreet, "/", 2)[0]
|
||||
}
|
||||
|
||||
func (l Location) Postcode() (result string) {
|
||||
return strings.SplitN(l.LocPostcode, "/", 2)[0]
|
||||
}
|
||||
|
||||
func (l Location) Category() (result string) {
|
||||
return l.LocCategory
|
||||
}
|
||||
|
|
|
@ -40,8 +40,18 @@ func TestFindLocation(t *testing.T) {
|
|||
t.Log(l)
|
||||
})
|
||||
t.Run("cached true", func(t *testing.T) {
|
||||
var p = NewPlace("1", "", "", "", "", "de", "")
|
||||
location := NewLocation("1e95998417cc", 52.51961810676184, 13.40806264572578, "TestLocation", "test", p, true)
|
||||
location := Location{
|
||||
ID: "1e95998417cc",
|
||||
LocLat: 52.51961810676184,
|
||||
LocLng: 13.40806264572578,
|
||||
LocName: "TestLocation",
|
||||
LocStreet: "",
|
||||
LocPostcode: "",
|
||||
LocCategory: "test",
|
||||
Place: Place{PlaceID: "1"},
|
||||
Cached: true,
|
||||
}
|
||||
|
||||
l, err := FindLocation(location.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -57,8 +67,17 @@ func TestFindLocation(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLocationGetters(t *testing.T) {
|
||||
var p = NewPlace("1", "testLabel", "Berlin", "", "Berlin", "de", "foobar")
|
||||
location := NewLocation("1e95998417cc", 52.51961810676184, 13.40806264572578, "TestLocation", "test", p, true)
|
||||
location := Location{
|
||||
ID: "1e95998417cc",
|
||||
LocLat: 52.51961810676184,
|
||||
LocLng: 13.40806264572578,
|
||||
LocName: "TestLocation",
|
||||
LocStreet: "",
|
||||
LocPostcode: "",
|
||||
LocCategory: "test",
|
||||
Place: Place{PlaceID: "1", LocLabel: "testLabel", LocDistrict: "Berlin", LocCity: "", LocState: "Berlin", LocCountry: "de", LocKeywords: "foobar"},
|
||||
Cached: true,
|
||||
}
|
||||
t.Run("wrong id", func(t *testing.T) {
|
||||
assert.Equal(t, "1e95998417cc", location.CellID())
|
||||
assert.Equal(t, "TestLocation", location.Name())
|
||||
|
@ -66,7 +85,8 @@ func TestLocationGetters(t *testing.T) {
|
|||
assert.Equal(t, "testLabel", location.Label())
|
||||
assert.Equal(t, "Berlin", location.State())
|
||||
assert.Equal(t, "de", location.CountryCode())
|
||||
assert.Equal(t, "Berlin", location.City())
|
||||
assert.Equal(t, "Berlin", location.District())
|
||||
assert.Equal(t, "", location.City())
|
||||
assert.Equal(t, 52.51961810676184, location.Latitude())
|
||||
assert.Equal(t, 13.40806264572578, location.Longitude())
|
||||
assert.Equal(t, "places", location.Source())
|
||||
|
@ -75,9 +95,18 @@ func TestLocationGetters(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLocation_State(t *testing.T) {
|
||||
location := Location{
|
||||
ID: "54903ee07f74",
|
||||
LocLat: 47.6129432,
|
||||
LocLng: -122.4821475,
|
||||
LocName: "TestLocation",
|
||||
LocStreet: "",
|
||||
LocPostcode: "",
|
||||
LocCategory: "test",
|
||||
Place: Place{PlaceID: "549ed22c0434", LocLabel: "Seattle, WA", LocDistrict: "Berlin", LocCity: "Seattle", LocState: "WA", LocCountry: "us", LocKeywords: "foobar"},
|
||||
Cached: true,
|
||||
}
|
||||
t.Run("Washington", func(t *testing.T) {
|
||||
var p = NewPlace("549ed22c0434", "Seattle, WA", "Seattle", "", "WA", "us", "")
|
||||
location := NewLocation("54903ee07f74", 47.6129432, -122.4821475, "", "", p, true)
|
||||
assert.Equal(t, "54903ee07f74", location.CellID())
|
||||
assert.Equal(t, "Seattle, WA", location.Label())
|
||||
assert.Equal(t, "Washington", location.State())
|
||||
|
@ -88,9 +117,18 @@ func TestLocation_State(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLocation_District(t *testing.T) {
|
||||
location := Location{
|
||||
ID: "54903ee07f74",
|
||||
LocLat: 47.6129432,
|
||||
LocLng: -122.4821475,
|
||||
LocName: "TestLocation",
|
||||
LocStreet: "",
|
||||
LocPostcode: "",
|
||||
LocCategory: "test",
|
||||
Place: Place{PlaceID: "549ed22c0434", LocLabel: "Seattle, WA", LocDistrict: "Foo", LocCity: "Seattle", LocState: "WA", LocCountry: "us", LocKeywords: "foobar"},
|
||||
Cached: true,
|
||||
}
|
||||
t.Run("Washington", func(t *testing.T) {
|
||||
var p = NewPlace("549ed22c0434", "Seattle, WA", "Seattle", "Foo", "WA", "us", "")
|
||||
location := NewLocation("54903ee07f74", 47.6129432, -122.4821475, "", "", p, true)
|
||||
assert.Equal(t, "54903ee07f74", location.CellID())
|
||||
assert.Equal(t, "Seattle, WA", location.Label())
|
||||
assert.Equal(t, "Foo", location.District())
|
||||
|
|
|
@ -4,23 +4,9 @@ package places
|
|||
type Place struct {
|
||||
PlaceID string `json:"id"`
|
||||
LocLabel string `json:"label"`
|
||||
LocCity string `json:"city"`
|
||||
LocDistrict string `json:"district"`
|
||||
LocCity string `json:"city"`
|
||||
LocState string `json:"state"`
|
||||
LocCountry string `json:"country"`
|
||||
LocKeywords string `json:"keywords"`
|
||||
}
|
||||
|
||||
func NewPlace(id, label, city, district, state, country, keywords string) Place {
|
||||
result := Place{
|
||||
PlaceID: id,
|
||||
LocLabel: label,
|
||||
LocCity: city,
|
||||
LocDistrict: district,
|
||||
LocState: state,
|
||||
LocCountry: country,
|
||||
LocKeywords: keywords,
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/hub/places"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/s2"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
@ -13,44 +12,33 @@ import (
|
|||
// Location represents a geolocation.
|
||||
type Location struct {
|
||||
ID string
|
||||
placeID string
|
||||
LocName string
|
||||
LocStreet string
|
||||
LocPostcode string
|
||||
LocCategory string
|
||||
LocLabel string
|
||||
LocCity string
|
||||
LocDistrict string
|
||||
LocCity string
|
||||
LocState string
|
||||
LocCountry string
|
||||
LocSource string
|
||||
LocKeywords []string
|
||||
LocSource string
|
||||
}
|
||||
|
||||
type LocationSource interface {
|
||||
CellID() string
|
||||
CountryCode() string
|
||||
Category() string
|
||||
PlaceID() string
|
||||
Name() string
|
||||
City() string
|
||||
Street() string
|
||||
Category() string
|
||||
Postcode() string
|
||||
District() string
|
||||
City() string
|
||||
State() string
|
||||
Source() string
|
||||
CountryCode() string
|
||||
Keywords() []string
|
||||
}
|
||||
|
||||
func NewLocation(id, name, category, label, city, district, state, country, source string, keywords []string) *Location {
|
||||
result := &Location{
|
||||
ID: id,
|
||||
LocName: name,
|
||||
LocCategory: category,
|
||||
LocLabel: label,
|
||||
LocCity: city,
|
||||
LocDistrict: district,
|
||||
LocCountry: country,
|
||||
LocState: txt.NormalizeState(state, country),
|
||||
LocSource: source,
|
||||
LocKeywords: keywords,
|
||||
}
|
||||
|
||||
return result
|
||||
Source() string
|
||||
}
|
||||
|
||||
func (l *Location) QueryApi(api string) error {
|
||||
|
@ -69,14 +57,17 @@ func (l *Location) QueryPlaces() error {
|
|||
return err
|
||||
}
|
||||
|
||||
l.placeID = s.PlaceID()
|
||||
l.LocSource = s.Source()
|
||||
l.LocName = s.Name()
|
||||
l.LocCity = s.City()
|
||||
l.LocDistrict = s.District()
|
||||
l.LocState = s.State()
|
||||
l.LocCountry = s.CountryCode()
|
||||
l.LocStreet = s.Street()
|
||||
l.LocPostcode = s.Postcode()
|
||||
l.LocCategory = s.Category()
|
||||
l.LocLabel = s.Label()
|
||||
l.LocDistrict = s.District()
|
||||
l.LocCity = s.City()
|
||||
l.LocState = s.State()
|
||||
l.LocCountry = s.CountryCode()
|
||||
l.LocKeywords = s.Keywords()
|
||||
|
||||
return nil
|
||||
|
@ -86,6 +77,14 @@ func (l *Location) Unknown() bool {
|
|||
return l.ID == ""
|
||||
}
|
||||
|
||||
func (l Location) PlaceID() string {
|
||||
if l.placeID != "" {
|
||||
return s2.Prefix(l.placeID)
|
||||
}
|
||||
|
||||
return l.PrefixedToken()
|
||||
}
|
||||
|
||||
func (l Location) S2Token() string {
|
||||
return l.ID
|
||||
}
|
||||
|
@ -95,45 +94,53 @@ func (l Location) PrefixedToken() string {
|
|||
}
|
||||
|
||||
func (l Location) Name() string {
|
||||
return txt.Shorten(l.LocName, txt.ClipTitle, txt.Ellipsis)
|
||||
return txt.Clip(l.LocName, 200)
|
||||
}
|
||||
|
||||
func (l Location) Street() string {
|
||||
return txt.Clip(l.LocStreet, 100)
|
||||
}
|
||||
|
||||
func (l Location) Postcode() string {
|
||||
return txt.Clip(l.LocPostcode, 50)
|
||||
}
|
||||
|
||||
func (l Location) Category() string {
|
||||
return txt.Shorten(l.LocCategory, txt.ClipKeyword, txt.Ellipsis)
|
||||
return txt.Clip(l.LocCategory, 50)
|
||||
}
|
||||
|
||||
func (l Location) Label() string {
|
||||
return txt.Shorten(l.LocLabel, txt.ClipLabel, txt.Ellipsis)
|
||||
return txt.Clip(l.LocLabel, 400)
|
||||
}
|
||||
|
||||
func (l Location) City() string {
|
||||
return txt.Shorten(l.LocCity, txt.ClipPlace, txt.Ellipsis)
|
||||
return txt.Clip(l.LocCity, 100)
|
||||
}
|
||||
|
||||
func (l Location) District() string {
|
||||
return txt.Shorten(l.LocDistrict, txt.ClipPlace, txt.Ellipsis)
|
||||
return txt.Clip(l.LocDistrict, 100)
|
||||
}
|
||||
|
||||
func (l Location) CountryCode() string {
|
||||
return txt.Clip(l.LocCountry, txt.ClipCountryCode)
|
||||
return txt.Clip(l.LocCountry, 2)
|
||||
}
|
||||
|
||||
func (l Location) State() string {
|
||||
return txt.Shorten(txt.NormalizeState(l.LocState, l.CountryCode()), txt.ClipPlace, txt.Ellipsis)
|
||||
return txt.Clip(txt.NormalizeState(l.LocState, l.CountryCode()), 100)
|
||||
}
|
||||
|
||||
func (l Location) CountryName() string {
|
||||
return CountryNames[l.LocCountry]
|
||||
}
|
||||
|
||||
func (l Location) Source() string {
|
||||
return l.LocSource
|
||||
}
|
||||
|
||||
func (l Location) Keywords() []string {
|
||||
return l.LocKeywords
|
||||
}
|
||||
|
||||
func (l Location) KeywordString() string {
|
||||
return txt.Clip(strings.Join(l.LocKeywords, ", "), txt.ClipVarchar)
|
||||
return txt.Clip(strings.Join(l.LocKeywords, ", "), 300)
|
||||
}
|
||||
|
||||
func (l Location) Source() string {
|
||||
return l.LocSource
|
||||
}
|
||||
|
|
|
@ -3,24 +3,79 @@ package maps
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/s2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/s2"
|
||||
)
|
||||
|
||||
func TestLocation_QueryPlaces(t *testing.T) {
|
||||
t.Run("U Berliner Rathaus", func(t *testing.T) {
|
||||
t.Run("BerlinerRathaus", func(t *testing.T) {
|
||||
lat := 52.51961810676184
|
||||
lng := 13.40806264572578
|
||||
id := s2.Token(lat, lng)
|
||||
|
||||
l := NewLocation(id, "", "", "", "", "", "", "", "", []string{})
|
||||
l := Location{ID: id}
|
||||
|
||||
if err := l.QueryPlaces(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("BerlinerRathaus: %#v", l)
|
||||
|
||||
assert.Equal(t, "Mitte, Berlin, Germany", l.LocLabel)
|
||||
})
|
||||
t.Run("BerlinerFernsehturm", func(t *testing.T) {
|
||||
lat := 52.5208
|
||||
lng := 13.40953
|
||||
id := s2.Token(lat, lng)
|
||||
|
||||
l := Location{ID: id}
|
||||
|
||||
if err := l.QueryPlaces(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("BerlinerFernsehturm: %#v", l)
|
||||
|
||||
assert.Equal(t, "Berliner Fernsehturm", l.LocName)
|
||||
assert.Equal(t, "Berlin", l.LocState)
|
||||
assert.Equal(t, "Berlin", l.LocCity)
|
||||
assert.Equal(t, "Panoramastraße", l.LocStreet)
|
||||
assert.Equal(t, "10178", l.LocPostcode)
|
||||
assert.Equal(t, "Mitte", l.LocDistrict)
|
||||
assert.Equal(t, "Mitte, Berlin, Germany", l.LocLabel)
|
||||
})
|
||||
t.Run("NorthAtlanticOcean", func(t *testing.T) {
|
||||
id := "0a3c25fcffad"
|
||||
|
||||
l := Location{ID: id}
|
||||
|
||||
if err := l.QueryPlaces(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "", l.LocName)
|
||||
assert.Equal(t, "", l.LocState)
|
||||
assert.Equal(t, "ocean", l.LocCategory)
|
||||
assert.Equal(t, "North Atlantic Ocean", l.LocDistrict)
|
||||
assert.Equal(t, "North Atlantic Ocean", l.LocLabel)
|
||||
})
|
||||
t.Run("SouthPacificOcean", func(t *testing.T) {
|
||||
id := "9aa986feefb4"
|
||||
|
||||
l := Location{ID: id}
|
||||
|
||||
if err := l.QueryPlaces(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "", l.LocName)
|
||||
assert.Equal(t, "ocean", l.LocCategory)
|
||||
assert.Equal(t, "", l.LocState)
|
||||
assert.Equal(t, "South Pacific Ocean", l.LocDistrict)
|
||||
assert.Equal(t, "ec", l.LocCountry)
|
||||
assert.Equal(t, "South Pacific Ocean, Ecuador", l.LocLabel)
|
||||
assert.Equal(t, "places", l.LocSource)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLocation_Unknown(t *testing.T) {
|
||||
|
@ -29,7 +84,7 @@ func TestLocation_Unknown(t *testing.T) {
|
|||
lng := 0.0
|
||||
id := s2.Token(lat, lng)
|
||||
|
||||
l := NewLocation(id, "", "", "", "", "", "", "", "", []string{})
|
||||
l := Location{ID: id}
|
||||
|
||||
assert.Equal(t, true, l.Unknown())
|
||||
})
|
||||
|
@ -38,7 +93,7 @@ func TestLocation_Unknown(t *testing.T) {
|
|||
lng := 29.148046666666666
|
||||
id := s2.Token(lat, lng)
|
||||
|
||||
l := NewLocation(id, "", "", "", "", "", "", "", "", []string{})
|
||||
l := Location{ID: id}
|
||||
|
||||
assert.Equal(t, false, l.Unknown())
|
||||
})
|
||||
|
@ -46,7 +101,7 @@ func TestLocation_Unknown(t *testing.T) {
|
|||
|
||||
func TestLocation_S2Token(t *testing.T) {
|
||||
t.Run("123", func(t *testing.T) {
|
||||
l := NewLocation("123", "Indian ocean", "", "", "Nürnberg", "", "Bayern", "de", "", []string{})
|
||||
l := Location{ID: "123"}
|
||||
|
||||
assert.Equal(t, "123", l.S2Token())
|
||||
})
|
||||
|
@ -54,7 +109,7 @@ func TestLocation_S2Token(t *testing.T) {
|
|||
|
||||
func TestLocation_PrefixedToken(t *testing.T) {
|
||||
t.Run("123", func(t *testing.T) {
|
||||
l := NewLocation("123", "Indian ocean", "", "", "Nürnberg", "", "Bayern", "de", "", []string{})
|
||||
l := Location{ID: "123"}
|
||||
|
||||
assert.Equal(t, s2.TokenPrefix+"123", l.PrefixedToken())
|
||||
})
|
||||
|
@ -62,7 +117,7 @@ func TestLocation_PrefixedToken(t *testing.T) {
|
|||
|
||||
func TestLocation_Name(t *testing.T) {
|
||||
t.Run("Christkindlesmarkt", func(t *testing.T) {
|
||||
l := NewLocation("", "Christkindlesmarkt", "", "", "Nürnberg", "", "Bayern", "de", "", []string{})
|
||||
l := Location{ID: "123", LocName: "Christkindlesmarkt"}
|
||||
|
||||
assert.Equal(t, "Christkindlesmarkt", l.Name())
|
||||
})
|
||||
|
@ -70,7 +125,7 @@ func TestLocation_Name(t *testing.T) {
|
|||
|
||||
func TestLocation_City(t *testing.T) {
|
||||
t.Run("Nürnberg", func(t *testing.T) {
|
||||
l := NewLocation("", "Christkindlesmarkt", "", "", "Nürnberg", "", "Bayern", "de", "", []string{})
|
||||
l := Location{ID: "123", LocCity: "Nürnberg"}
|
||||
|
||||
assert.Equal(t, "Nürnberg", l.City())
|
||||
})
|
||||
|
@ -78,7 +133,7 @@ func TestLocation_City(t *testing.T) {
|
|||
|
||||
func TestLocation_State(t *testing.T) {
|
||||
t.Run("Bayern", func(t *testing.T) {
|
||||
l := NewLocation("", "Christkindlesmarkt", "", "", "Nürnberg", "", "Bayern", "de", "", []string{})
|
||||
l := Location{ID: "123", LocState: "Bayern"}
|
||||
|
||||
assert.Equal(t, "Bayern", l.State())
|
||||
})
|
||||
|
@ -86,7 +141,7 @@ func TestLocation_State(t *testing.T) {
|
|||
|
||||
func TestLocation_Category(t *testing.T) {
|
||||
t.Run("test", func(t *testing.T) {
|
||||
l := NewLocation("", "Christkindlesmarkt", "test", "", "Nürnberg", "", "Bayern", "de", "", []string{})
|
||||
l := Location{ID: "123", LocCategory: "test"}
|
||||
|
||||
assert.Equal(t, "test", l.Category())
|
||||
})
|
||||
|
@ -94,15 +149,15 @@ func TestLocation_Category(t *testing.T) {
|
|||
|
||||
func TestLocation_Source(t *testing.T) {
|
||||
t.Run("source", func(t *testing.T) {
|
||||
l := NewLocation("", "Christkindlesmarkt", "", "", "Nürnberg", "", "Bayern", "de", "source", []string{})
|
||||
l := Location{ID: "123", LocSource: "mySource"}
|
||||
|
||||
assert.Equal(t, "source", l.Source())
|
||||
assert.Equal(t, "mySource", l.Source())
|
||||
})
|
||||
}
|
||||
|
||||
func TestLocation_Place(t *testing.T) {
|
||||
t.Run("test-label", func(t *testing.T) {
|
||||
l := NewLocation("", "Christkindlesmarkt", "", "test-label", "Nürnberg", "", "Bayern", "de", "", []string{})
|
||||
l := Location{ID: "123", LocLabel: "test-label"}
|
||||
|
||||
assert.Equal(t, "test-label", l.Label())
|
||||
})
|
||||
|
@ -110,7 +165,7 @@ func TestLocation_Place(t *testing.T) {
|
|||
|
||||
func TestLocation_CountryCode(t *testing.T) {
|
||||
t.Run("de", func(t *testing.T) {
|
||||
l := NewLocation("", "Christkindlesmarkt", "test", "test-label", "Nürnberg", "", "Bayern", "de", "", []string{})
|
||||
l := Location{ID: "123", LocCountry: "de"}
|
||||
|
||||
assert.Equal(t, "de", l.CountryCode())
|
||||
})
|
||||
|
@ -118,14 +173,15 @@ func TestLocation_CountryCode(t *testing.T) {
|
|||
|
||||
func TestLocation_CountryName(t *testing.T) {
|
||||
t.Run("Germany", func(t *testing.T) {
|
||||
l := NewLocation("", "Christkindlesmarkt", "test", "test-label", "Nürnberg", "", "Bayern", "de", "", []string{})
|
||||
l := Location{ID: "123", LocCountry: "de"}
|
||||
|
||||
assert.Equal(t, "Germany", l.CountryName())
|
||||
})
|
||||
}
|
||||
|
||||
func TestLocation_QueryApi(t *testing.T) {
|
||||
l := NewLocation("3", "Christkindlesmarkt", "test", "test-label", "Nürnberg", "", "Bayern", "de", "", []string{})
|
||||
l := Location{ID: "3", LocCountry: "de"}
|
||||
|
||||
t.Run("xxx", func(t *testing.T) {
|
||||
api := l.QueryApi("xxx")
|
||||
assert.Error(t, api, "maps: reverse lookup disabled")
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"math"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
|
@ -29,6 +28,17 @@ func NewMoments(conf *config.Config) *Moments {
|
|||
return instance
|
||||
}
|
||||
|
||||
// MigrateSlug updates deprecated moment slugs if needed.
|
||||
func (w *Moments) MigrateSlug(m query.Moment, albumType string) {
|
||||
if m.Slug() == m.TitleSlug() {
|
||||
return
|
||||
}
|
||||
|
||||
if a := entity.FindAlbumBySlug(m.TitleSlug(), albumType); a != nil {
|
||||
logWarn("moments", a.Update("album_slug", m.Slug()))
|
||||
}
|
||||
}
|
||||
|
||||
// Start creates albums based on popular locations, dates and categories.
|
||||
func (w *Moments) Start() (err error) {
|
||||
defer func() {
|
||||
|
@ -102,6 +112,8 @@ func (w *Moments) Start() (err error) {
|
|||
log.Errorf("moments: %s", err.Error())
|
||||
} else {
|
||||
for _, mom := range results {
|
||||
w.MigrateSlug(mom, entity.AlbumMonth)
|
||||
|
||||
if a := entity.FindAlbumBySlug(mom.Slug(), entity.AlbumMonth); a != nil {
|
||||
if !a.Deleted() {
|
||||
log.Tracef("moments: %s already exists (%s)", txt.Quote(a.AlbumTitle), a.AlbumFilter)
|
||||
|
@ -125,6 +137,8 @@ func (w *Moments) Start() (err error) {
|
|||
log.Errorf("moments: %s", err.Error())
|
||||
} else {
|
||||
for _, mom := range results {
|
||||
w.MigrateSlug(mom, entity.AlbumMoment)
|
||||
|
||||
f := form.PhotoSearch{
|
||||
Country: mom.Country,
|
||||
Year: strconv.Itoa(mom.Year),
|
||||
|
@ -156,6 +170,8 @@ func (w *Moments) Start() (err error) {
|
|||
log.Errorf("moments: %s", err.Error())
|
||||
} else {
|
||||
for _, mom := range results {
|
||||
w.MigrateSlug(mom, entity.AlbumState)
|
||||
|
||||
f := form.PhotoSearch{
|
||||
Country: mom.Country,
|
||||
State: mom.State,
|
||||
|
@ -186,27 +202,19 @@ func (w *Moments) Start() (err error) {
|
|||
log.Errorf("moments: %s", err.Error())
|
||||
} else {
|
||||
for _, mom := range results {
|
||||
w.MigrateSlug(mom, entity.AlbumMoment)
|
||||
|
||||
f := form.PhotoSearch{
|
||||
Label: mom.Label,
|
||||
Public: true,
|
||||
}
|
||||
|
||||
if a := entity.FindAlbumBySlug(mom.Slug(), entity.AlbumMoment); a != nil {
|
||||
log.Tracef("moments: %s already exists (%s)", txt.Quote(mom.Title()), f.Serialize())
|
||||
|
||||
if f.Serialize() == a.AlbumFilter || a.DeletedAt != nil {
|
||||
// Nothing to do.
|
||||
if a.DeletedAt != nil || f.Serialize() == a.AlbumFilter {
|
||||
log.Tracef("moments: %s already exists (%s)", txt.Quote(a.AlbumTitle), a.AlbumFilter)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := form.Unserialize(&f, a.AlbumFilter); err != nil {
|
||||
log.Errorf("moments: %s", err.Error())
|
||||
} else {
|
||||
w := txt.Words(f.Label)
|
||||
w = append(w, mom.Label)
|
||||
f.Label = strings.Join(txt.UniqueWords(w), txt.Or)
|
||||
}
|
||||
|
||||
if err := a.Update("AlbumFilter", f.Serialize()); err != nil {
|
||||
log.Errorf("moments: %s", err.Error())
|
||||
} else {
|
||||
|
|
|
@ -31,18 +31,18 @@ func NewPlaces(conf *config.Config) *Places {
|
|||
func (w *Places) Start() (updated []string, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("places: %s (panic)\nstack: %s", r, debug.Stack())
|
||||
err = fmt.Errorf("index: %s (update places)\nstack: %s", r, debug.Stack())
|
||||
log.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := mutex.MainWorker.Start(); err != nil {
|
||||
// Already running.
|
||||
log.Warnf("places: %s (start)", err.Error())
|
||||
// A worker is already running.
|
||||
log.Warnf("index: %s (update places)", err.Error())
|
||||
return []string{}, err
|
||||
} else if !w.conf.Sponsor() && !w.conf.Test() {
|
||||
// Only for sponsors as this puts load on our API.
|
||||
log.Warnf("places: only sponsors may fetch updated location infos")
|
||||
log.Errorf(config.MsgSponsorCommand)
|
||||
log.Errorf(config.MsgFundingInfo)
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
|
@ -51,37 +51,63 @@ func (w *Places) Start() (updated []string, err error) {
|
|||
// Fetch cell IDs from index.
|
||||
cells, err := query.CellIDs()
|
||||
|
||||
// Error?
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
} else if len(cells) == 0 {
|
||||
log.Warnf("places: found no locations")
|
||||
log.Warnf("index: found no places to update")
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
log.Infof("places: updating %s", english.Plural(len(cells), "location", "locations"))
|
||||
// Drop and recreate places database table.
|
||||
if err = entity.RecreateTable(entity.Place{}); err != nil {
|
||||
return []string{}, fmt.Errorf("index: %s", err)
|
||||
}
|
||||
|
||||
// List of updated cells.
|
||||
updated = make([]string, 0, len(cells))
|
||||
|
||||
// Update known locations.
|
||||
for _, id := range cells {
|
||||
for i, cell := range cells {
|
||||
if i%10 == 0 {
|
||||
log.Infof("index: updated %s, %d remaining", english.Plural(i, "place", "places"), len(cells)-i)
|
||||
}
|
||||
|
||||
if w.Canceled() {
|
||||
return updated, nil
|
||||
} else if id == "" || id == entity.UnknownID {
|
||||
} else if cell.ID == "" || cell.ID == entity.UnknownID {
|
||||
// Skip unknown places.
|
||||
continue
|
||||
}
|
||||
|
||||
c := entity.Cell{ID: id}
|
||||
// Create cell from location and place ID.
|
||||
c := entity.Cell{ID: cell.ID, PlaceID: cell.PlaceID}
|
||||
|
||||
// Fetch updated information from backend API.
|
||||
// Fetch updated cell data from backend API.
|
||||
if err = c.Refresh(entity.GeoApi); err != nil {
|
||||
log.Errorf("places: %s", err)
|
||||
log.Warnf("index: %s", err)
|
||||
} else {
|
||||
updated = append(updated, id)
|
||||
// Append if successful.
|
||||
updated = append(updated, cell.ID)
|
||||
}
|
||||
|
||||
// Short break.
|
||||
time.Sleep(25 * time.Millisecond)
|
||||
time.Sleep(40 * time.Millisecond)
|
||||
}
|
||||
|
||||
// Find and fix bad place ids.
|
||||
log.Debug("places: updating references")
|
||||
if fixed, err := query.UpdatePlaceIDs(); err != nil {
|
||||
log.Errorf("places: %s (updated references)", err)
|
||||
} else if fixed > 0 {
|
||||
log.Infof("places: updated %d references", fixed)
|
||||
}
|
||||
|
||||
// Update photo counts in places.
|
||||
if err := entity.UpdatePlacesCounts(); err != nil {
|
||||
log.Errorf("index: %s (update counts)", err)
|
||||
} else {
|
||||
log.Infof("index: updated counts")
|
||||
}
|
||||
|
||||
return updated, err
|
||||
|
|
|
@ -72,8 +72,51 @@ var MomentLabels = map[string]string{
|
|||
"hamster": "Pets",
|
||||
}
|
||||
|
||||
// MomentLabelsFilter returns the smart filter string for a moment based on a matching label.
|
||||
func MomentLabelsFilter(label string) string {
|
||||
// TODO: Needs refactoring
|
||||
label = strings.SplitN(label, txt.Or, 2)[0]
|
||||
|
||||
title := MomentLabels[label]
|
||||
|
||||
if title == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
var l []string
|
||||
|
||||
for i := range MomentLabels {
|
||||
if MomentLabels[i] == title {
|
||||
l = append(l, i)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(txt.UniqueWords(l), txt.Or)
|
||||
}
|
||||
|
||||
// Slug returns an identifier string for a moment.
|
||||
func (m Moment) Slug() string {
|
||||
func (m Moment) Slug() (s string) {
|
||||
state := txt.NormalizeState(m.State, m.Country)
|
||||
|
||||
if state == "" {
|
||||
return m.TitleSlug()
|
||||
}
|
||||
|
||||
country := maps.CountryName(m.Country)
|
||||
|
||||
if m.Year > 1900 && m.Month == 0 {
|
||||
s = fmt.Sprintf("%s-%s-%04d", country, state, m.Year)
|
||||
} else if m.Year > 1900 && m.Month > 0 && m.Month <= 12 {
|
||||
s = fmt.Sprintf("%s-%s-%04d-%02d", country, state, m.Year, m.Month)
|
||||
} else {
|
||||
s = fmt.Sprintf("%s-%s", country, state)
|
||||
}
|
||||
|
||||
return slug.Make(s)
|
||||
}
|
||||
|
||||
// TitleSlug returns an identifier string based on the title.
|
||||
func (m Moment) TitleSlug() string {
|
||||
return slug.Make(m.Title())
|
||||
}
|
||||
|
||||
|
@ -83,7 +126,7 @@ func (m Moment) Title() string {
|
|||
|
||||
if m.Year == 0 && m.Month == 0 {
|
||||
if m.Label != "" {
|
||||
return MomentLabels[m.Label]
|
||||
return MomentLabels[strings.SplitN(m.Label, txt.Or, 2)[0]]
|
||||
}
|
||||
|
||||
country := maps.CountryName(m.Country)
|
||||
|
@ -93,7 +136,7 @@ func (m Moment) Title() string {
|
|||
}
|
||||
|
||||
if state == "" {
|
||||
return m.Country
|
||||
return country
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s / %s", state, country)
|
||||
|
@ -182,6 +225,8 @@ func MomentsLabels(threshold int) (results Moments, err error) {
|
|||
cats = append(cats, cat)
|
||||
}
|
||||
|
||||
m := Moments{}
|
||||
|
||||
db := UnscopedDb().Table("photos").
|
||||
Select("l.label_slug AS label, COUNT(*) AS photo_count").
|
||||
Joins("JOIN photos_labels pl ON pl.photo_id = photos.id AND pl.uncertainty < 100").
|
||||
|
@ -190,8 +235,23 @@ func MomentsLabels(threshold int) (results Moments, err error) {
|
|||
Group("l.label_slug").
|
||||
Having("photo_count >= ?", threshold)
|
||||
|
||||
if err := db.Scan(&results).Error; err != nil {
|
||||
return results, err
|
||||
if err := db.Scan(&m).Error; err != nil {
|
||||
return m, err
|
||||
}
|
||||
|
||||
done := make(map[string]bool)
|
||||
|
||||
for i := 0; i < len(m); i++ {
|
||||
f := MomentLabelsFilter(m[i].Label)
|
||||
|
||||
if _, ok := done[f]; ok {
|
||||
continue
|
||||
} else {
|
||||
done[f] = true
|
||||
}
|
||||
|
||||
m[i].Label = f
|
||||
results = append(results, m[i])
|
||||
}
|
||||
|
||||
return results, nil
|
||||
|
|
|
@ -20,14 +20,19 @@ func TestMomentsTime(t *testing.T) {
|
|||
t.Logf("MomentsTime %+v", results)
|
||||
|
||||
for _, moment := range results {
|
||||
t.Logf("Title: %s", moment.Title())
|
||||
t.Logf("Slug: %s", moment.Slug())
|
||||
t.Logf("Title Slug: %s", moment.TitleSlug())
|
||||
|
||||
assert.Len(t, moment.Country, 0)
|
||||
assert.GreaterOrEqual(t, moment.Year, 1990)
|
||||
assert.LessOrEqual(t, moment.Year, 2800)
|
||||
assert.GreaterOrEqual(t, moment.Month, 1)
|
||||
assert.LessOrEqual(t, moment.Month, 12)
|
||||
assert.GreaterOrEqual(t, moment.PhotoCount, 1)
|
||||
t.Logf("Title: %s", moment.Title())
|
||||
t.Logf("Slug: %s", moment.Slug())
|
||||
assert.Regexp(t, "[a-zA-Z]+ [0-9]+", moment.Title())
|
||||
assert.Regexp(t, "[a-z]+\\-[0-9]+", moment.Slug())
|
||||
assert.Regexp(t, "[a-z]+\\-[0-9]+", moment.TitleSlug())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -47,13 +52,18 @@ func TestMomentsCountries(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, moment := range results {
|
||||
t.Logf("Title: %s", moment.Title())
|
||||
t.Logf("Slug: %s", moment.Slug())
|
||||
t.Logf("Title Slug: %s", moment.TitleSlug())
|
||||
|
||||
assert.Len(t, moment.Country, 2)
|
||||
assert.GreaterOrEqual(t, moment.Year, 1990)
|
||||
assert.LessOrEqual(t, moment.Year, 2800)
|
||||
assert.Equal(t, moment.Month, 0)
|
||||
assert.GreaterOrEqual(t, moment.PhotoCount, 1)
|
||||
t.Logf("Title: %s", moment.Title())
|
||||
t.Logf("Slug: %s", moment.Slug())
|
||||
assert.Regexp(t, "[ \\&a-zA-Z0-9]+", moment.Title())
|
||||
assert.Regexp(t, "[\\-a-z0-9]+", moment.Slug())
|
||||
assert.Regexp(t, "[\\-a-z0-9]+", moment.TitleSlug())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -73,13 +83,18 @@ func TestMomentsStates(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, moment := range results {
|
||||
t.Logf("Title: %s", moment.Title())
|
||||
t.Logf("Slug: %s", moment.Slug())
|
||||
t.Logf("Title Slug: %s", moment.TitleSlug())
|
||||
|
||||
assert.Len(t, moment.Country, 2)
|
||||
assert.NotEmpty(t, moment.State)
|
||||
assert.Equal(t, moment.Year, 0)
|
||||
assert.Equal(t, moment.Month, 0)
|
||||
assert.GreaterOrEqual(t, moment.PhotoCount, 1)
|
||||
t.Logf("Title: %s", moment.Title())
|
||||
t.Logf("Slug: %s", moment.Slug())
|
||||
assert.Regexp(t, "[ \\&a-zA-Z0-9]+", moment.Title())
|
||||
assert.Regexp(t, "[\\-a-z0-9]+", moment.Slug())
|
||||
assert.Regexp(t, "[\\-a-z0-9]+", moment.TitleSlug())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -99,14 +114,19 @@ func TestMomentsCategories(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, moment := range results {
|
||||
t.Logf("Title: %s", moment.Title())
|
||||
t.Logf("Slug: %s", moment.Slug())
|
||||
t.Logf("Title Slug: %s", moment.TitleSlug())
|
||||
|
||||
assert.NotEmpty(t, moment.Label)
|
||||
assert.Empty(t, moment.Country)
|
||||
assert.Empty(t, moment.State)
|
||||
assert.Equal(t, moment.Year, 0)
|
||||
assert.Equal(t, moment.Month, 0)
|
||||
assert.GreaterOrEqual(t, moment.PhotoCount, 1)
|
||||
t.Logf("Title: %s", moment.Title())
|
||||
t.Logf("Slug: %s", moment.Slug())
|
||||
assert.Regexp(t, "[ \\&a-zA-Z0-9]+", moment.Title())
|
||||
assert.Regexp(t, "[\\-a-z0-9]+", moment.Slug())
|
||||
assert.Regexp(t, "[\\-a-z0-9]+", moment.TitleSlug())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -122,7 +142,7 @@ func TestMoment_Title(t *testing.T) {
|
|||
PhotoCount: 0,
|
||||
}
|
||||
|
||||
assert.Equal(t, "de", moment.Title())
|
||||
assert.Equal(t, "Germany", moment.Title())
|
||||
})
|
||||
t.Run("country name", func(t *testing.T) {
|
||||
moment := Moment{
|
||||
|
|
|
@ -1,39 +1,49 @@
|
|||
package query
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
)
|
||||
|
||||
// CellIDs returns all known S2 cell ids as string slice.
|
||||
func CellIDs() (ids []string, err error) {
|
||||
type Cell struct {
|
||||
ID string
|
||||
PlaceID string
|
||||
}
|
||||
|
||||
type Cells []Cell
|
||||
|
||||
// CellIDs returns all known S2 cell ids as Cell slice.
|
||||
func CellIDs() (result Cells, err error) {
|
||||
tableName := entity.Cell{}.TableName()
|
||||
|
||||
var count int64
|
||||
|
||||
if err = UnscopedDb().Table(tableName).Where("id <> 'zz'").Count(&count).Error; err != nil {
|
||||
return []string{}, err
|
||||
return result, err
|
||||
}
|
||||
|
||||
ids = make([]string, 0, count)
|
||||
result = make(Cells, 0, count)
|
||||
|
||||
err = UnscopedDb().Table(tableName).Select("id").Where("id <> 'zz'").Pluck("id", &ids).Error
|
||||
err = UnscopedDb().Table(tableName).Select("id, place_id").Where("id <> 'zz'").Order("id").Scan(&result).Error
|
||||
|
||||
return ids, err
|
||||
return result, err
|
||||
}
|
||||
|
||||
// PlaceIDs returns all known S2 place ids as string slice.
|
||||
func PlaceIDs() (ids []string, err error) {
|
||||
tableName := entity.Place{}.TableName()
|
||||
// UpdatePlaceIDs finds and replaces invalid place references.
|
||||
func UpdatePlaceIDs() (fixed int64, err error) {
|
||||
photosTable := entity.Photo{}.TableName()
|
||||
placesTable := entity.Place{}.TableName()
|
||||
|
||||
var count int64
|
||||
res := Db().Table(photosTable).Where("place_id NOT IN (SELECT place_id FROM ?)", gorm.Expr(placesTable)).
|
||||
UpdateColumn("place_id", "zz")
|
||||
|
||||
if err = UnscopedDb().Table(tableName).Where("id <> 'zz'").Count(&count).Error; err != nil {
|
||||
return []string{}, err
|
||||
if res.Error != nil {
|
||||
return res.RowsAffected, res.Error
|
||||
}
|
||||
|
||||
ids = make([]string, 0, count)
|
||||
res = Db().Table(photosTable).Where("cell_id IS NOT NULL AND cell_id <> 'zz'").
|
||||
UpdateColumn("place_id", gorm.Expr("(SELECT id FROM cells WHERE id = cell_id)"))
|
||||
|
||||
err = UnscopedDb().Table(tableName).Select("id").Where("id <> 'zz'").Pluck("id", &ids).Error
|
||||
|
||||
return ids, err
|
||||
return res.RowsAffected, res.Error
|
||||
}
|
||||
|
|
|
@ -16,14 +16,16 @@ func TestCellIDs(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestPlaceIDs(t *testing.T) {
|
||||
func TestUpdatePlaceIDs(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
result, err := PlaceIDs()
|
||||
fixed, err := UpdatePlaceIDs()
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("place count: %v", len(result))
|
||||
if fixed < 0 {
|
||||
t.Fatal("fixed must be a positive integer")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ func Albums(f form.AlbumSearch) (results AlbumResults, err error) {
|
|||
|
||||
// Base query.
|
||||
s := UnscopedDb().Table("albums").
|
||||
Select("albums.*, cp.photo_count, cl.link_count").
|
||||
Select("albums.*, cp.photo_count, cl.link_count, CASE WHEN albums.album_year = 0 THEN 0 ELSE 1 END AS has_year").
|
||||
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)").
|
||||
|
@ -31,10 +31,30 @@ func Albums(f form.AlbumSearch) (results AlbumResults, err error) {
|
|||
|
||||
// Set sort order.
|
||||
switch f.Order {
|
||||
case "slug":
|
||||
s = s.Order("albums.album_favorite DESC, album_slug ASC")
|
||||
case entity.SortOrderCount:
|
||||
s = s.Order("photo_count DESC, albums.album_title, albums.album_uid DESC")
|
||||
case entity.SortOrderRelevance:
|
||||
s = s.Order("albums.album_favorite DESC, albums.updated_at DESC, albums.album_uid DESC")
|
||||
case entity.SortOrderNewest:
|
||||
s = s.Order("albums.album_favorite DESC, albums.album_year DESC, albums.album_month DESC, albums.album_day DESC, albums.album_title, albums.album_uid DESC")
|
||||
case entity.SortOrderOldest:
|
||||
s = s.Order("albums.album_favorite DESC, albums.album_year ASC, albums.album_month ASC, albums.album_day ASC, albums.album_title, albums.album_uid ASC")
|
||||
case entity.SortOrderAdded:
|
||||
s = s.Order("albums.album_uid DESC")
|
||||
case entity.SortOrderMoment:
|
||||
s = s.Order("albums.album_favorite DESC, has_year, albums.album_year DESC, albums.album_month DESC, albums.album_title ASC, albums.album_uid DESC")
|
||||
case entity.SortOrderPlace:
|
||||
s = s.Order("albums.album_favorite DESC, albums.album_country, albums.album_title, albums.album_year DESC, albums.album_month ASC, albums.album_day ASC, albums.album_uid DESC")
|
||||
case entity.SortOrderName:
|
||||
s = s.Order("albums.album_title ASC, albums.album_uid DESC")
|
||||
case entity.SortOrderPath:
|
||||
s = s.Order("albums.album_favorite DESC, albums.album_path DESC, albums.album_uid DESC")
|
||||
case entity.SortOrderCategory:
|
||||
s = s.Order("albums.album_category, albums.album_title, albums.album_uid DESC")
|
||||
case entity.SortOrderSlug:
|
||||
s = s.Order("albums.album_favorite DESC, albums.album_slug ASC, albums.album_uid DESC")
|
||||
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")
|
||||
s = s.Order("albums.album_favorite DESC, has_year, albums.album_title ASC, albums.album_uid DESC")
|
||||
}
|
||||
|
||||
if f.ID != "" {
|
||||
|
|
|
@ -53,7 +53,7 @@ func TestAlbums(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "April 1990", result[0].AlbumTitle)
|
||||
assert.Equal(t, "Import Album", result[0].AlbumTitle)
|
||||
})
|
||||
|
||||
t.Run("favorites true", func(t *testing.T) {
|
||||
|
|
|
@ -40,6 +40,9 @@ func TestUcFirst(t *testing.T) {
|
|||
t.Run("cat", func(t *testing.T) {
|
||||
assert.Equal(t, "Cat", UcFirst("Cat"))
|
||||
})
|
||||
t.Run("KwaZulu natal", func(t *testing.T) {
|
||||
assert.Equal(t, "KwaZulu Natal", Title("KwaZulu natal"))
|
||||
})
|
||||
t.Run("empty string", func(t *testing.T) {
|
||||
assert.Equal(t, "", UcFirst(""))
|
||||
})
|
||||
|
|
Loading…
Add table
Reference in a new issue