Search: Refactor photo search, fix test data and unit tests #1994
This commit is contained in:
parent
b8ea17d595
commit
686a8ab9b4
|
@ -37,7 +37,6 @@ services:
|
||||||
PHOTOPRISM_DATABASE_USER: "root"
|
PHOTOPRISM_DATABASE_USER: "root"
|
||||||
PHOTOPRISM_DATABASE_PASSWORD: "photoprism"
|
PHOTOPRISM_DATABASE_PASSWORD: "photoprism"
|
||||||
PHOTOPRISM_TEST_DRIVER: "sqlite"
|
PHOTOPRISM_TEST_DRIVER: "sqlite"
|
||||||
PHOTOPRISM_TEST_DSN: ".test.db"
|
|
||||||
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # the initial admin password (min 4 characters)
|
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # the initial admin password (min 4 characters)
|
||||||
PHOTOPRISM_ASSETS_PATH: "/go/src/github.com/photoprism/photoprism/assets"
|
PHOTOPRISM_ASSETS_PATH: "/go/src/github.com/photoprism/photoprism/assets"
|
||||||
PHOTOPRISM_STORAGE_PATH: "/go/src/github.com/photoprism/photoprism/storage"
|
PHOTOPRISM_STORAGE_PATH: "/go/src/github.com/photoprism/photoprism/storage"
|
||||||
|
|
|
@ -39,9 +39,9 @@ services:
|
||||||
PHOTOPRISM_HTTP_COMPRESSION: "gzip" # improves transfer speed and bandwidth utilization (none or gzip)
|
PHOTOPRISM_HTTP_COMPRESSION: "gzip" # improves transfer speed and bandwidth utilization (none or gzip)
|
||||||
PHOTOPRISM_DATABASE_DRIVER: "mysql"
|
PHOTOPRISM_DATABASE_DRIVER: "mysql"
|
||||||
PHOTOPRISM_DATABASE_SERVER: "mariadb:4001"
|
PHOTOPRISM_DATABASE_SERVER: "mariadb:4001"
|
||||||
PHOTOPRISM_DATABASE_NAME: "latest"
|
PHOTOPRISM_DATABASE_NAME: "photoprism_latest"
|
||||||
PHOTOPRISM_DATABASE_USER: "root"
|
PHOTOPRISM_DATABASE_USER: "photoprism_latest"
|
||||||
PHOTOPRISM_DATABASE_PASSWORD: "photoprism"
|
PHOTOPRISM_DATABASE_PASSWORD: "photoprism_latest"
|
||||||
PHOTOPRISM_DISABLE_CHOWN: "false" # disables storage permission updates on startup
|
PHOTOPRISM_DISABLE_CHOWN: "false" # disables storage permission updates on startup
|
||||||
PHOTOPRISM_DISABLE_BACKUPS: "false" # don't backup photo and album metadata to YAML files
|
PHOTOPRISM_DISABLE_BACKUPS: "false" # don't backup photo and album metadata to YAML files
|
||||||
PHOTOPRISM_DISABLE_WEBDAV: "false" # disables built-in WebDAV server
|
PHOTOPRISM_DISABLE_WEBDAV: "false" # disables built-in WebDAV server
|
||||||
|
@ -62,8 +62,8 @@ services:
|
||||||
TF_CPP_MIN_LOG_LEVEL: 0 # show TensorFlow log messages for development
|
TF_CPP_MIN_LOG_LEVEL: 0 # show TensorFlow log messages for development
|
||||||
working_dir: "/photoprism"
|
working_dir: "/photoprism"
|
||||||
volumes:
|
volumes:
|
||||||
- "./storage/latest:/photoprism/storage"
|
- "/photoprism/storage"
|
||||||
- "./storage/originals:/photoprism/originals"
|
- "/photoprism/originals"
|
||||||
|
|
||||||
## Join shared "photoprism-develop" network
|
## Join shared "photoprism-develop" network
|
||||||
networks:
|
networks:
|
||||||
|
|
|
@ -2,6 +2,23 @@ version: '3.5'
|
||||||
|
|
||||||
## MariaDB Server Versions for Development & Testing
|
## MariaDB Server Versions for Development & Testing
|
||||||
services:
|
services:
|
||||||
|
## MariaDB 10.8 Database Server
|
||||||
|
## Docs: https://mariadb.com/kb/en/release-notes-mariadb-108-series/
|
||||||
|
mariadb-10-8:
|
||||||
|
image: mariadb:10.8-rc
|
||||||
|
command: mysqld --port=4001 --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
|
||||||
|
expose:
|
||||||
|
- "4001"
|
||||||
|
ports:
|
||||||
|
- "4002:4001" # database port (host:container)
|
||||||
|
volumes:
|
||||||
|
- "./scripts/sql/mariadb-init.sql:/docker-entrypoint-initdb.d/init.sql"
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: photoprism
|
||||||
|
MYSQL_USER: photoprism
|
||||||
|
MYSQL_PASSWORD: photoprism
|
||||||
|
MYSQL_DATABASE: photoprism
|
||||||
|
|
||||||
## MariaDB 10.7 Database Server
|
## MariaDB 10.7 Database Server
|
||||||
mariadb-10-7:
|
mariadb-10-7:
|
||||||
image: mariadb:10.7
|
image: mariadb:10.7
|
||||||
|
|
|
@ -48,7 +48,6 @@ services:
|
||||||
PHOTOPRISM_DATABASE_USER: "photoprism"
|
PHOTOPRISM_DATABASE_USER: "photoprism"
|
||||||
PHOTOPRISM_DATABASE_PASSWORD: "photoprism"
|
PHOTOPRISM_DATABASE_PASSWORD: "photoprism"
|
||||||
PHOTOPRISM_TEST_DRIVER: "sqlite"
|
PHOTOPRISM_TEST_DRIVER: "sqlite"
|
||||||
PHOTOPRISM_TEST_DSN: ".test.db"
|
|
||||||
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # the initial admin password (min 4 characters)
|
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # the initial admin password (min 4 characters)
|
||||||
PHOTOPRISM_ASSETS_PATH: "/go/src/github.com/photoprism/photoprism/assets"
|
PHOTOPRISM_ASSETS_PATH: "/go/src/github.com/photoprism/photoprism/assets"
|
||||||
PHOTOPRISM_STORAGE_PATH: "/go/src/github.com/photoprism/photoprism/storage"
|
PHOTOPRISM_STORAGE_PATH: "/go/src/github.com/photoprism/photoprism/storage"
|
||||||
|
|
|
@ -79,7 +79,6 @@ services:
|
||||||
PHOTOPRISM_DATABASE_USER: "root"
|
PHOTOPRISM_DATABASE_USER: "root"
|
||||||
PHOTOPRISM_DATABASE_PASSWORD: "photoprism"
|
PHOTOPRISM_DATABASE_PASSWORD: "photoprism"
|
||||||
PHOTOPRISM_TEST_DRIVER: "sqlite"
|
PHOTOPRISM_TEST_DRIVER: "sqlite"
|
||||||
PHOTOPRISM_TEST_DSN: ".test.db"
|
|
||||||
# PHOTOPRISM_TEST_DSN_MYSQL8: "root:photoprism@tcp(mysql:4001)/photoprism?charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true"
|
# PHOTOPRISM_TEST_DSN_MYSQL8: "root:photoprism@tcp(mysql:4001)/photoprism?charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true"
|
||||||
PHOTOPRISM_ASSETS_PATH: "/go/src/github.com/photoprism/photoprism/assets"
|
PHOTOPRISM_ASSETS_PATH: "/go/src/github.com/photoprism/photoprism/assets"
|
||||||
PHOTOPRISM_STORAGE_PATH: "/go/src/github.com/photoprism/photoprism/storage"
|
PHOTOPRISM_STORAGE_PATH: "/go/src/github.com/photoprism/photoprism/storage"
|
||||||
|
|
|
@ -12,10 +12,6 @@ func TestMain(m *testing.M) {
|
||||||
log = logrus.StandardLogger()
|
log = logrus.StandardLogger()
|
||||||
log.SetLevel(logrus.TraceLevel)
|
log.SetLevel(logrus.TraceLevel)
|
||||||
|
|
||||||
if err := os.Remove(".test.db"); err == nil {
|
|
||||||
log.Debugln("removed .test.db")
|
|
||||||
}
|
|
||||||
|
|
||||||
c := config.TestConfig()
|
c := config.TestConfig()
|
||||||
|
|
||||||
code := m.Run()
|
code := m.Run()
|
||||||
|
|
|
@ -342,12 +342,12 @@ var GlobalFlags = []cli.Flag{
|
||||||
EnvVar: "PHOTOPRISM_DATABASE_DRIVER",
|
EnvVar: "PHOTOPRISM_DATABASE_DRIVER",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "database-dsn",
|
Name: "database-dsn, dsn",
|
||||||
Usage: "database connection `DSN` (sqlite filename, optional for mysql)",
|
Usage: "database connection `DSN` (sqlite filename, optional for mysql)",
|
||||||
EnvVar: "PHOTOPRISM_DATABASE_DSN",
|
EnvVar: "PHOTOPRISM_DATABASE_DSN",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "database-server",
|
Name: "database-server, db",
|
||||||
Usage: "database `HOST` incl. port e.g. \"mariadb:3306\" (or socket path)",
|
Usage: "database `HOST` incl. port e.g. \"mariadb:3306\" (or socket path)",
|
||||||
EnvVar: "PHOTOPRISM_DATABASE_SERVER",
|
EnvVar: "PHOTOPRISM_DATABASE_SERVER",
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,12 +15,14 @@ import (
|
||||||
"github.com/photoprism/photoprism/pkg/fs"
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Database drivers (sql dialects).
|
// SQL Databases.
|
||||||
const (
|
const (
|
||||||
MySQL = "mysql"
|
MySQL = "mysql"
|
||||||
MariaDB = "mariadb"
|
MariaDB = "mariadb"
|
||||||
SQLite3 = "sqlite3"
|
|
||||||
Postgres = "postgres" // TODO: Requires GORM 2.0 for generic column data types
|
Postgres = "postgres" // TODO: Requires GORM 2.0 for generic column data types
|
||||||
|
SQLite3 = "sqlite3"
|
||||||
|
SQLiteTestDB = ".test.db"
|
||||||
|
SQLiteMemoryDSN = ":memory:"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Options provides a struct in which application configuration is stored.
|
// Options provides a struct in which application configuration is stored.
|
||||||
|
|
|
@ -9,13 +9,16 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
_ "github.com/jinzhu/gorm/dialects/mysql"
|
||||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/thumb"
|
"github.com/photoprism/photoprism/internal/thumb"
|
||||||
"github.com/photoprism/photoprism/pkg/capture"
|
"github.com/photoprism/photoprism/pkg/capture"
|
||||||
"github.com/photoprism/photoprism/pkg/fs"
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
"github.com/photoprism/photoprism/pkg/rnd"
|
"github.com/photoprism/photoprism/pkg/rnd"
|
||||||
"github.com/urfave/cli"
|
"github.com/photoprism/photoprism/pkg/sanitize"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Download URL and ZIP hash for test files.
|
// Download URL and ZIP hash for test files.
|
||||||
|
@ -40,18 +43,31 @@ func NewTestOptions() *Options {
|
||||||
storagePath := fs.Abs("../../storage")
|
storagePath := fs.Abs("../../storage")
|
||||||
testDataPath := filepath.Join(storagePath, "testdata")
|
testDataPath := filepath.Join(storagePath, "testdata")
|
||||||
|
|
||||||
dbDriver := os.Getenv("PHOTOPRISM_TEST_DRIVER")
|
driver := os.Getenv("PHOTOPRISM_TEST_DRIVER")
|
||||||
dbDsn := os.Getenv("PHOTOPRISM_TEST_DSN")
|
dsn := os.Getenv("PHOTOPRISM_TEST_DSN")
|
||||||
|
|
||||||
// Config example for MySQL / MariaDB:
|
// Config example for MySQL / MariaDB:
|
||||||
// dbDriver = MySQL,
|
// driver = MySQL,
|
||||||
// dbDsn = "photoprism:photoprism@tcp(mariadb:4001)/photoprism?parseTime=true",
|
// dsn = "photoprism:photoprism@tcp(mariadb:4001)/photoprism?parseTime=true",
|
||||||
|
|
||||||
if dbDriver == "test" || dbDriver == "sqlite" || dbDriver == "" || dbDsn == "" {
|
// Set default test database driver.
|
||||||
dbDriver = SQLite3
|
if driver == "test" || driver == "sqlite" || driver == "" || dsn == "" {
|
||||||
dbDsn = ".test.db"
|
driver = SQLite3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set default database DSN.
|
||||||
|
if driver == SQLite3 {
|
||||||
|
if dsn == "" {
|
||||||
|
dsn = SQLiteMemoryDSN
|
||||||
|
} else if dsn != SQLiteTestDB {
|
||||||
|
// Continue.
|
||||||
|
|
||||||
|
} else if err := os.Remove(dsn); err == nil {
|
||||||
|
log.Debugf("sqlite: test file %s removed", sanitize.Log(dsn))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test config options.
|
||||||
c := &Options{
|
c := &Options{
|
||||||
Name: "PhotoPrism",
|
Name: "PhotoPrism",
|
||||||
Version: "0.0.0",
|
Version: "0.0.0",
|
||||||
|
@ -74,8 +90,8 @@ func NewTestOptions() *Options {
|
||||||
TempPath: testDataPath + "/temp",
|
TempPath: testDataPath + "/temp",
|
||||||
ConfigPath: testDataPath + "/config",
|
ConfigPath: testDataPath + "/config",
|
||||||
SidecarPath: testDataPath + "/sidecar",
|
SidecarPath: testDataPath + "/sidecar",
|
||||||
DatabaseDriver: dbDriver,
|
DatabaseDriver: driver,
|
||||||
DatabaseDsn: dbDsn,
|
DatabaseDsn: dsn,
|
||||||
AdminPassword: "photoprism",
|
AdminPassword: "photoprism",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -618,31 +618,6 @@ var AlbumFixtures = AlbumMap{
|
||||||
UpdatedAt: time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC),
|
UpdatedAt: time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC),
|
||||||
DeletedAt: nil,
|
DeletedAt: nil,
|
||||||
},
|
},
|
||||||
"red|green": {
|
|
||||||
ID: 1000023,
|
|
||||||
AlbumUID: "at1lxuqipotaab32",
|
|
||||||
AlbumSlug: "red|green",
|
|
||||||
AlbumPath: "",
|
|
||||||
AlbumType: AlbumDefault,
|
|
||||||
AlbumTitle: "Red|Green",
|
|
||||||
AlbumLocation: "",
|
|
||||||
AlbumCategory: "",
|
|
||||||
AlbumCaption: "",
|
|
||||||
AlbumDescription: "",
|
|
||||||
AlbumNotes: "",
|
|
||||||
AlbumFilter: "",
|
|
||||||
AlbumOrder: "name",
|
|
||||||
AlbumTemplate: "",
|
|
||||||
AlbumCountry: "zz",
|
|
||||||
AlbumYear: 0,
|
|
||||||
AlbumMonth: 0,
|
|
||||||
AlbumDay: 0,
|
|
||||||
AlbumFavorite: false,
|
|
||||||
AlbumPrivate: false,
|
|
||||||
CreatedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
|
||||||
UpdatedAt: time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC),
|
|
||||||
DeletedAt: nil,
|
|
||||||
},
|
|
||||||
"blue|": {
|
"blue|": {
|
||||||
ID: 1000024,
|
ID: 1000024,
|
||||||
AlbumUID: "at1lxuqipotaab33",
|
AlbumUID: "at1lxuqipotaab33",
|
||||||
|
@ -743,6 +718,31 @@ var AlbumFixtures = AlbumMap{
|
||||||
UpdatedAt: time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC),
|
UpdatedAt: time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC),
|
||||||
DeletedAt: nil,
|
DeletedAt: nil,
|
||||||
},
|
},
|
||||||
|
"red|green": {
|
||||||
|
ID: 1000028,
|
||||||
|
AlbumUID: "at1lxuqipotaab32",
|
||||||
|
AlbumSlug: "red|green",
|
||||||
|
AlbumPath: "",
|
||||||
|
AlbumType: AlbumDefault,
|
||||||
|
AlbumTitle: "Red|Green",
|
||||||
|
AlbumLocation: "",
|
||||||
|
AlbumCategory: "",
|
||||||
|
AlbumCaption: "",
|
||||||
|
AlbumDescription: "",
|
||||||
|
AlbumNotes: "",
|
||||||
|
AlbumFilter: "",
|
||||||
|
AlbumOrder: "name",
|
||||||
|
AlbumTemplate: "",
|
||||||
|
AlbumCountry: "zz",
|
||||||
|
AlbumYear: 0,
|
||||||
|
AlbumMonth: 0,
|
||||||
|
AlbumDay: 0,
|
||||||
|
AlbumFavorite: false,
|
||||||
|
AlbumPrivate: false,
|
||||||
|
CreatedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
UpdatedAt: time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
DeletedAt: nil,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateAlbumFixtures inserts known entities into the database for testing.
|
// CreateAlbumFixtures inserts known entities into the database for testing.
|
||||||
|
|
|
@ -64,9 +64,11 @@ const (
|
||||||
// Sort Orders
|
// Sort Orders
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
SortOrderDefault = ""
|
||||||
SortOrderRelevance = "relevance"
|
SortOrderRelevance = "relevance"
|
||||||
SortOrderCount = "count"
|
SortOrderCount = "count"
|
||||||
SortOrderAdded = "added"
|
SortOrderAdded = "added"
|
||||||
|
SortOrderImported = "imported"
|
||||||
SortOrderEdited = "edited"
|
SortOrderEdited = "edited"
|
||||||
SortOrderNewest = "newest"
|
SortOrderNewest = "newest"
|
||||||
SortOrderOldest = "oldest"
|
SortOrderOldest = "oldest"
|
||||||
|
|
|
@ -10,10 +10,12 @@ import (
|
||||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Database drivers (sql dialects).
|
// SQL Databases.
|
||||||
const (
|
const (
|
||||||
MySQL = "mysql"
|
MySQL = "mysql"
|
||||||
SQLite3 = "sqlite3"
|
SQLite3 = "sqlite3"
|
||||||
|
SQLiteTestDB = ".test.db"
|
||||||
|
SQLiteMemoryDSN = ":memory:"
|
||||||
)
|
)
|
||||||
|
|
||||||
var dbProvider DbProvider
|
var dbProvider DbProvider
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package entity
|
package entity
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/pkg/sanitize"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MigrateDb creates database tables and inserts default fixtures as needed.
|
// MigrateDb creates database tables and inserts default fixtures as needed.
|
||||||
|
@ -26,18 +29,31 @@ func InitTestDb(driver, dsn string) *Gorm {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set default test database driver.
|
||||||
if driver == "test" || driver == "sqlite" || driver == "" || dsn == "" {
|
if driver == "test" || driver == "sqlite" || driver == "" || dsn == "" {
|
||||||
driver = "sqlite3"
|
driver = SQLite3
|
||||||
dsn = ".test.db"
|
}
|
||||||
|
|
||||||
|
// Set default database DSN.
|
||||||
|
if driver == SQLite3 {
|
||||||
|
if dsn == "" {
|
||||||
|
dsn = SQLiteMemoryDSN
|
||||||
|
} else if dsn != SQLiteTestDB {
|
||||||
|
// Continue.
|
||||||
|
} else if err := os.Remove(dsn); err == nil {
|
||||||
|
log.Debugf("sqlite: test file %s removed", sanitize.Log(dsn))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("initializing %s test db in %s", driver, dsn)
|
log.Infof("initializing %s test db in %s", driver, dsn)
|
||||||
|
|
||||||
|
// Create ORM instance.
|
||||||
db := &Gorm{
|
db := &Gorm{
|
||||||
Driver: driver,
|
Driver: driver,
|
||||||
Dsn: dsn,
|
Dsn: dsn,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Insert test fixtures.
|
||||||
SetDbProvider(db)
|
SetDbProvider(db)
|
||||||
ResetTestFixtures()
|
ResetTestFixtures()
|
||||||
|
|
||||||
|
|
|
@ -4,20 +4,18 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
log = logrus.StandardLogger()
|
log = logrus.StandardLogger()
|
||||||
log.SetLevel(logrus.TraceLevel)
|
log.SetLevel(logrus.TraceLevel)
|
||||||
|
|
||||||
if err := os.Remove(".test.db"); err == nil {
|
db := InitTestDb(
|
||||||
log.Debugln("removed .test.db")
|
os.Getenv("PHOTOPRISM_TEST_DRIVER"),
|
||||||
}
|
os.Getenv("PHOTOPRISM_TEST_DSN"))
|
||||||
|
|
||||||
db := InitTestDb(os.Getenv("PHOTOPRISM_TEST_DRIVER"), os.Getenv("PHOTOPRISM_TEST_DSN"))
|
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
code := m.Run()
|
code := m.Run()
|
||||||
|
|
|
@ -4,19 +4,19 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/entity"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
log = logrus.StandardLogger()
|
log = logrus.StandardLogger()
|
||||||
log.SetLevel(logrus.TraceLevel)
|
log.SetLevel(logrus.TraceLevel)
|
||||||
|
|
||||||
if err := os.Remove(".test.db"); err == nil {
|
db := entity.InitTestDb(
|
||||||
log.Debugln("removed .test.db")
|
os.Getenv("PHOTOPRISM_TEST_DRIVER"),
|
||||||
}
|
os.Getenv("PHOTOPRISM_TEST_DSN"))
|
||||||
|
|
||||||
db := entity.InitTestDb(os.Getenv("PHOTOPRISM_TEST_DRIVER"), os.Getenv("PHOTOPRISM_TEST_DSN"))
|
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
code := m.Run()
|
code := m.Run()
|
||||||
|
|
|
@ -27,55 +27,87 @@ func Photos(f form.SearchPhotos) (results PhotoResults, count int, err error) {
|
||||||
s := UnscopedDb()
|
s := UnscopedDb()
|
||||||
// s = s.LogMode(true)
|
// s = s.LogMode(true)
|
||||||
|
|
||||||
// Base query.
|
// Select columns.
|
||||||
s = s.Table("photos").
|
cols := []string{
|
||||||
Select(`photos.*, photos.id AS composite_id,
|
"photos.*",
|
||||||
files.id AS file_id, files.file_uid, files.instance_id, files.file_primary, files.file_sidecar,
|
"files.file_uid",
|
||||||
files.file_portrait,files.file_video, files.file_missing, files.file_name, files.file_root, files.file_hash,
|
"files.id AS file_id",
|
||||||
files.file_codec, files.file_type, files.file_mime, files.file_width, files.file_height,
|
"files.photo_id AS composite_id",
|
||||||
files.file_aspect_ratio, files.file_orientation, files.file_main_color, files.file_colors, files.file_luminance,
|
"files.instance_id",
|
||||||
files.file_chroma, files.file_projection, files.file_diff, files.file_duration, files.file_size,
|
"files.file_primary",
|
||||||
cameras.camera_make, cameras.camera_model,
|
"files.file_sidecar",
|
||||||
lenses.lens_make, lenses.lens_model,
|
"files.file_portrait",
|
||||||
places.place_label, places.place_city, places.place_state, places.place_country`).
|
"files.file_video",
|
||||||
Joins("JOIN files ON photos.id = files.photo_id AND files.file_missing = 0 AND files.deleted_at IS NULL").
|
"files.file_missing",
|
||||||
|
"files.file_name",
|
||||||
|
"files.file_root",
|
||||||
|
"files.file_hash",
|
||||||
|
"files.file_codec",
|
||||||
|
"files.file_type",
|
||||||
|
"files.file_mime",
|
||||||
|
"files.file_width",
|
||||||
|
"files.file_height",
|
||||||
|
"files.file_aspect_ratio",
|
||||||
|
"files.file_orientation",
|
||||||
|
"files.file_main_color",
|
||||||
|
"files.file_colors",
|
||||||
|
"files.file_luminance",
|
||||||
|
"files.file_chroma",
|
||||||
|
"files.file_projection",
|
||||||
|
"files.file_diff",
|
||||||
|
"files.file_duration",
|
||||||
|
"files.file_size",
|
||||||
|
"cameras.camera_make",
|
||||||
|
"cameras.camera_model",
|
||||||
|
"lenses.lens_make",
|
||||||
|
"lenses.lens_model",
|
||||||
|
"places.place_label",
|
||||||
|
"places.place_city",
|
||||||
|
"places.place_state",
|
||||||
|
"places.place_country",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database tables.
|
||||||
|
s = s.Table("files").Select(strings.Join(cols, ", ")).
|
||||||
|
Joins("JOIN photos ON photos.id = files.photo_id").
|
||||||
Joins("LEFT JOIN cameras ON photos.camera_id = cameras.id").
|
Joins("LEFT JOIN cameras ON photos.camera_id = cameras.id").
|
||||||
Joins("LEFT JOIN lenses ON photos.lens_id = lenses.id").
|
Joins("LEFT JOIN lenses ON photos.lens_id = lenses.id").
|
||||||
Joins("LEFT JOIN places ON photos.place_id = places.id")
|
Joins("LEFT JOIN places ON photos.place_id = places.id").
|
||||||
|
Where("files.deleted_at IS NULL AND files.file_missing = 0")
|
||||||
|
|
||||||
// Limit result count.
|
// Offset and count.
|
||||||
if f.Count > 0 && f.Count <= MaxResults {
|
if f.Count > 0 && f.Count <= MaxResults {
|
||||||
s = s.Limit(f.Count).Offset(f.Offset)
|
s = s.Limit(f.Count).Offset(f.Offset)
|
||||||
} else {
|
} else {
|
||||||
s = s.Limit(MaxResults).Offset(f.Offset)
|
s = s.Limit(MaxResults).Offset(f.Offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set sort order.
|
// Sort order.
|
||||||
switch f.Order {
|
switch f.Order {
|
||||||
case entity.SortOrderEdited:
|
case entity.SortOrderEdited:
|
||||||
s = s.Where("edited_at IS NOT NULL").Order("edited_at DESC, photos.photo_uid, files.file_primary DESC")
|
s = s.Where("photos.edited_at IS NOT NULL").Order("photos.edited_at DESC, files.photo_id DESC, files.file_primary DESC, files.id")
|
||||||
case entity.SortOrderRelevance:
|
case entity.SortOrderRelevance:
|
||||||
if f.Label != "" {
|
if f.Label != "" {
|
||||||
s = s.Order("photo_quality DESC, photos_labels.uncertainty ASC, taken_at DESC, files.file_primary DESC")
|
s = s.Order("photos.photo_quality DESC, photos_labels.uncertainty ASC, photos.taken_at DESC, files.photo_id DESC, files.file_primary DESC, files.id")
|
||||||
} else {
|
} else {
|
||||||
s = s.Order("photo_quality DESC, taken_at DESC, files.file_primary DESC")
|
s = s.Order("photos.photo_quality DESC, photos.taken_at DESC, files.photo_id DESC, files.file_primary DESC, files.id")
|
||||||
}
|
}
|
||||||
case entity.SortOrderNewest:
|
case entity.SortOrderNewest:
|
||||||
s = s.Order("taken_at DESC, photos.photo_uid, files.file_primary DESC")
|
s = s.Order("photos.taken_at DESC, files.photo_id DESC, files.file_primary DESC, files.id")
|
||||||
case entity.SortOrderOldest:
|
case entity.SortOrderOldest:
|
||||||
s = s.Order("taken_at, photos.photo_uid, files.file_primary DESC")
|
s = s.Order("photos.taken_at, files.photo_id DESC, files.file_primary DESC, files.id")
|
||||||
case entity.SortOrderAdded:
|
|
||||||
s = s.Order("photos.id DESC, files.file_primary DESC")
|
|
||||||
case entity.SortOrderSimilar:
|
case entity.SortOrderSimilar:
|
||||||
s = s.Where("files.file_diff > 0")
|
s = s.Where("files.file_diff > 0")
|
||||||
s = s.Order("photos.photo_color, photos.cell_id, files.file_diff, taken_at DESC, files.file_primary DESC")
|
s = s.Order("photos.photo_color, photos.cell_id, files.file_diff, photos.taken_at DESC, files.photo_id DESC, files.file_primary DESC, files.id")
|
||||||
case entity.SortOrderName:
|
case entity.SortOrderName:
|
||||||
s = s.Order("photos.photo_path, photos.photo_name, files.file_primary DESC")
|
s = s.Order("photos.photo_path, photos.photo_name, files.photo_id DESC, files.file_primary DESC, files.id")
|
||||||
|
case entity.SortOrderDefault, entity.SortOrderImported, entity.SortOrderAdded:
|
||||||
|
s = s.Order("files.photo_id DESC, files.file_primary DESC, files.id")
|
||||||
default:
|
default:
|
||||||
s = s.Order("taken_at DESC, photos.photo_uid, files.file_primary DESC")
|
return PhotoResults{}, 0, fmt.Errorf("invalid sort order")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include hidden files?
|
// Show hidden files?
|
||||||
if !f.Hidden {
|
if !f.Hidden {
|
||||||
s = s.Where("files.file_type = 'jpg' OR files.file_video = 1")
|
s = s.Where("files.file_type = 'jpg' OR files.file_video = 1")
|
||||||
|
|
||||||
|
@ -86,7 +118,7 @@ func Photos(f form.SearchPhotos) (results PhotoResults, count int, err error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return primary files only.
|
// Primary files only?
|
||||||
if f.Primary {
|
if f.Primary {
|
||||||
s = s.Where("files.file_primary = 1")
|
s = s.Where("files.file_primary = 1")
|
||||||
}
|
}
|
||||||
|
@ -96,7 +128,7 @@ func Photos(f form.SearchPhotos) (results PhotoResults, count int, err error) {
|
||||||
|
|
||||||
// Take shortcut?
|
// Take shortcut?
|
||||||
if f.Album == "" && f.Query == "" {
|
if f.Album == "" && f.Query == "" {
|
||||||
s = s.Order("files.file_primary DESC")
|
s = s.Order("files.photo_id DESC, files.file_primary DESC, files.id")
|
||||||
|
|
||||||
if result := s.Scan(&results); result.Error != nil {
|
if result := s.Scan(&results); result.Error != nil {
|
||||||
return results, 0, result.Error
|
return results, 0, result.Error
|
||||||
|
@ -105,7 +137,7 @@ func Photos(f form.SearchPhotos) (results PhotoResults, count int, err error) {
|
||||||
log.Debugf("photos: found %s for %s [%s]", english.Plural(len(results), "result", "results"), f.SerializeAll(), time.Since(start))
|
log.Debugf("photos: found %s for %s [%s]", english.Plural(len(results), "result", "results"), f.SerializeAll(), time.Since(start))
|
||||||
|
|
||||||
if f.Merged {
|
if f.Merged {
|
||||||
return results.Merged()
|
return results.Merge()
|
||||||
}
|
}
|
||||||
|
|
||||||
return results, len(results), nil
|
return results, len(results), nil
|
||||||
|
@ -134,7 +166,7 @@ func Photos(f form.SearchPhotos) (results PhotoResults, count int, err error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s = s.Joins("JOIN photos_labels ON photos_labels.photo_id = photos.id AND photos_labels.uncertainty < 100 AND photos_labels.label_id IN (?)", labelIds).
|
s = s.Joins("JOIN photos_labels ON photos_labels.photo_id = files.photo_id AND photos_labels.uncertainty < 100 AND photos_labels.label_id IN (?)", labelIds).
|
||||||
Group("photos.id, files.id")
|
Group("photos.id, files.id")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -191,14 +223,14 @@ func Photos(f form.SearchPhotos) (results PhotoResults, count int, err error) {
|
||||||
s = s.Where("photos.cell_id <> 'zz'")
|
s = s.Where("photos.cell_id <> 'zz'")
|
||||||
|
|
||||||
for _, where := range LikeAnyKeyword("k.keyword", f.Query) {
|
for _, where := range LikeAnyKeyword("k.keyword", f.Query) {
|
||||||
s = s.Where("photos.id IN (SELECT pk.photo_id FROM keywords k JOIN photos_keywords pk ON k.id = pk.keyword_id WHERE (?))", gorm.Expr(where))
|
s = s.Where("files.photo_id IN (SELECT pk.photo_id FROM keywords k JOIN photos_keywords pk ON k.id = pk.keyword_id WHERE (?))", gorm.Expr(where))
|
||||||
}
|
}
|
||||||
} else if f.Query != "" {
|
} else if f.Query != "" {
|
||||||
if err := Db().Where(AnySlug("custom_slug", f.Query, " ")).Find(&labels).Error; len(labels) == 0 || err != nil {
|
if err := Db().Where(AnySlug("custom_slug", f.Query, " ")).Find(&labels).Error; len(labels) == 0 || err != nil {
|
||||||
log.Debugf("search: label %s not found, using fuzzy search", txt.LogParamLower(f.Query))
|
log.Debugf("search: label %s not found, using fuzzy search", txt.LogParamLower(f.Query))
|
||||||
|
|
||||||
for _, where := range LikeAnyKeyword("k.keyword", f.Query) {
|
for _, where := range LikeAnyKeyword("k.keyword", f.Query) {
|
||||||
s = s.Where("photos.id IN (SELECT pk.photo_id FROM keywords k JOIN photos_keywords pk ON k.id = pk.keyword_id WHERE (?))", gorm.Expr(where))
|
s = s.Where("files.photo_id IN (SELECT pk.photo_id FROM keywords k JOIN photos_keywords pk ON k.id = pk.keyword_id WHERE (?))", gorm.Expr(where))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for _, l := range labels {
|
for _, l := range labels {
|
||||||
|
@ -215,11 +247,11 @@ func Photos(f form.SearchPhotos) (results PhotoResults, count int, err error) {
|
||||||
|
|
||||||
if wheres := LikeAnyKeyword("k.keyword", f.Query); len(wheres) > 0 {
|
if wheres := LikeAnyKeyword("k.keyword", f.Query); len(wheres) > 0 {
|
||||||
for _, where := range wheres {
|
for _, where := range wheres {
|
||||||
s = s.Where("photos.id IN (SELECT pk.photo_id FROM keywords k JOIN photos_keywords pk ON k.id = pk.keyword_id WHERE (?)) OR "+
|
s = s.Where("files.photo_id IN (SELECT pk.photo_id FROM keywords k JOIN photos_keywords pk ON k.id = pk.keyword_id WHERE (?)) OR "+
|
||||||
"photos.id IN (SELECT pl.photo_id FROM photos_labels pl WHERE pl.uncertainty < 100 AND pl.label_id IN (?))", gorm.Expr(where), labelIds)
|
"files.photo_id IN (SELECT pl.photo_id FROM photos_labels pl WHERE pl.uncertainty < 100 AND pl.label_id IN (?))", gorm.Expr(where), labelIds)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
s = s.Where("photos.id IN (SELECT pl.photo_id FROM photos_labels pl WHERE pl.uncertainty < 100 AND pl.label_id IN (?))", labelIds)
|
s = s.Where("files.photo_id IN (SELECT pl.photo_id FROM photos_labels pl WHERE pl.uncertainty < 100 AND pl.label_id IN (?))", labelIds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -227,7 +259,7 @@ func Photos(f form.SearchPhotos) (results PhotoResults, count int, err error) {
|
||||||
// Search for one or more keywords?
|
// Search for one or more keywords?
|
||||||
if txt.NotEmpty(f.Keywords) {
|
if txt.NotEmpty(f.Keywords) {
|
||||||
for _, where := range LikeAnyWord("k.keyword", f.Keywords) {
|
for _, where := range LikeAnyWord("k.keyword", f.Keywords) {
|
||||||
s = s.Where("photos.id IN (SELECT pk.photo_id FROM keywords k JOIN photos_keywords pk ON k.id = pk.keyword_id WHERE (?))", gorm.Expr(where))
|
s = s.Where("files.photo_id IN (SELECT pk.photo_id FROM keywords k JOIN photos_keywords pk ON k.id = pk.keyword_id WHERE (?))", gorm.Expr(where))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,17 +278,17 @@ func Photos(f form.SearchPhotos) (results PhotoResults, count int, err error) {
|
||||||
// Filter for specific face clusters? Example: PLJ7A3G4MBGZJRMVDIUCBLC46IAP4N7O
|
// Filter for specific face clusters? Example: PLJ7A3G4MBGZJRMVDIUCBLC46IAP4N7O
|
||||||
if len(f.Face) >= 32 {
|
if len(f.Face) >= 32 {
|
||||||
for _, f := range strings.Split(strings.ToUpper(f.Face), txt.And) {
|
for _, f := range strings.Split(strings.ToUpper(f.Face), txt.And) {
|
||||||
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 WHERE face_id IN (?))",
|
s = s.Where(fmt.Sprintf("files.photo_id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 WHERE face_id IN (?))",
|
||||||
entity.Marker{}.TableName()), strings.Split(f, txt.Or))
|
entity.Marker{}.TableName()), strings.Split(f, txt.Or))
|
||||||
}
|
}
|
||||||
} else if txt.New(f.Face) {
|
} else if txt.New(f.Face) {
|
||||||
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 AND m.marker_type = ? WHERE subj_uid IS NULL OR subj_uid = '')",
|
s = s.Where(fmt.Sprintf("files.photo_id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 AND m.marker_type = ? WHERE subj_uid IS NULL OR subj_uid = '')",
|
||||||
entity.Marker{}.TableName()), entity.MarkerFace)
|
entity.Marker{}.TableName()), entity.MarkerFace)
|
||||||
} else if txt.No(f.Face) {
|
} else if txt.No(f.Face) {
|
||||||
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 AND m.marker_type = ? WHERE face_id IS NULL OR face_id = '')",
|
s = s.Where(fmt.Sprintf("files.photo_id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 AND m.marker_type = ? WHERE face_id IS NULL OR face_id = '')",
|
||||||
entity.Marker{}.TableName()), entity.MarkerFace)
|
entity.Marker{}.TableName()), entity.MarkerFace)
|
||||||
} else if txt.Yes(f.Face) {
|
} else if txt.Yes(f.Face) {
|
||||||
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 AND m.marker_type = ? WHERE face_id IS NOT NULL AND face_id <> '')",
|
s = s.Where(fmt.Sprintf("files.photo_id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 AND m.marker_type = ? WHERE face_id IS NOT NULL AND face_id <> '')",
|
||||||
entity.Marker{}.TableName()), entity.MarkerFace)
|
entity.Marker{}.TableName()), entity.MarkerFace)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,16 +296,16 @@ func Photos(f form.SearchPhotos) (results PhotoResults, count int, err error) {
|
||||||
if txt.NotEmpty(f.Subject) {
|
if txt.NotEmpty(f.Subject) {
|
||||||
for _, subj := range strings.Split(strings.ToLower(f.Subject), txt.And) {
|
for _, subj := range strings.Split(strings.ToLower(f.Subject), txt.And) {
|
||||||
if subjects := strings.Split(subj, txt.Or); rnd.ContainsUIDs(subjects, 'j') {
|
if subjects := strings.Split(subj, txt.Or); rnd.ContainsUIDs(subjects, 'j') {
|
||||||
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 WHERE subj_uid IN (?))",
|
s = s.Where(fmt.Sprintf("files.photo_id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 WHERE subj_uid IN (?))",
|
||||||
entity.Marker{}.TableName()), subjects)
|
entity.Marker{}.TableName()), subjects)
|
||||||
} else {
|
} else {
|
||||||
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 JOIN %s s ON s.subj_uid = m.subj_uid WHERE (?))",
|
s = s.Where(fmt.Sprintf("files.photo_id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 JOIN %s s ON s.subj_uid = m.subj_uid WHERE (?))",
|
||||||
entity.Marker{}.TableName(), entity.Subject{}.TableName()), gorm.Expr(AnySlug("s.subj_slug", subj, txt.Or)))
|
entity.Marker{}.TableName(), entity.Subject{}.TableName()), gorm.Expr(AnySlug("s.subj_slug", subj, txt.Or)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if txt.NotEmpty(f.Subjects) {
|
} else if txt.NotEmpty(f.Subjects) {
|
||||||
for _, where := range LikeAllNames(Cols{"subj_name", "subj_alias"}, f.Subjects) {
|
for _, where := range LikeAllNames(Cols{"subj_name", "subj_alias"}, f.Subjects) {
|
||||||
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 JOIN %s s ON s.subj_uid = m.subj_uid WHERE (?))",
|
s = s.Where(fmt.Sprintf("files.photo_id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 JOIN %s s ON s.subj_uid = m.subj_uid WHERE (?))",
|
||||||
entity.Marker{}.TableName(), entity.Subject{}.TableName()), gorm.Expr(where))
|
entity.Marker{}.TableName(), entity.Subject{}.TableName()), gorm.Expr(where))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -491,25 +523,25 @@ func Photos(f form.SearchPhotos) (results PhotoResults, count int, err error) {
|
||||||
|
|
||||||
// Find stacks only?
|
// Find stacks only?
|
||||||
if f.Stack {
|
if f.Stack {
|
||||||
s = s.Where("photos.id IN (SELECT a.photo_id FROM files a JOIN files b ON a.id != b.id AND a.photo_id = b.photo_id AND a.file_type = b.file_type WHERE a.file_type='jpg')")
|
s = s.Where("files.photo_id IN (SELECT a.photo_id FROM files a JOIN files b ON a.id != b.id AND a.photo_id = b.photo_id AND a.file_type = b.file_type WHERE a.file_type='jpg')")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by album?
|
// Filter by album?
|
||||||
if rnd.IsPPID(f.Album, 'a') {
|
if rnd.IsPPID(f.Album, 'a') {
|
||||||
if f.Filter != "" {
|
if f.Filter != "" {
|
||||||
s = s.Where("photos.photo_uid NOT IN (SELECT photo_uid FROM photos_albums pa WHERE pa.hidden = 1 AND pa.album_uid = ?)", f.Album)
|
s = s.Where("files.photo_uid NOT IN (SELECT photo_uid FROM photos_albums pa WHERE pa.hidden = 1 AND pa.album_uid = ?)", f.Album)
|
||||||
} else {
|
} else {
|
||||||
s = s.Joins("JOIN photos_albums ON photos_albums.photo_uid = photos.photo_uid").
|
s = s.Joins("JOIN photos_albums ON photos_albums.photo_uid = files.photo_uid").
|
||||||
Where("photos_albums.hidden = 0 AND photos_albums.album_uid = ?", f.Album)
|
Where("photos_albums.hidden = 0 AND photos_albums.album_uid = ?", f.Album)
|
||||||
}
|
}
|
||||||
} else if f.Unsorted && f.Filter == "" {
|
} else if f.Unsorted && f.Filter == "" {
|
||||||
s = s.Where("photos.photo_uid NOT IN (SELECT photo_uid FROM photos_albums pa WHERE pa.hidden = 0)")
|
s = s.Where("files.photo_uid NOT IN (SELECT photo_uid FROM photos_albums pa WHERE pa.hidden = 0)")
|
||||||
} else if txt.NotEmpty(f.Album) {
|
} else if txt.NotEmpty(f.Album) {
|
||||||
v := strings.Trim(f.Album, "*%") + "%"
|
v := strings.Trim(f.Album, "*%") + "%"
|
||||||
s = s.Where("photos.photo_uid IN (SELECT pa.photo_uid FROM photos_albums pa JOIN albums a ON a.album_uid = pa.album_uid AND pa.hidden = 0 WHERE (a.album_title LIKE ? OR a.album_slug LIKE ?))", v, v)
|
s = s.Where("files.photo_uid IN (SELECT pa.photo_uid FROM photos_albums pa JOIN albums a ON a.album_uid = pa.album_uid AND pa.hidden = 0 WHERE (a.album_title LIKE ? OR a.album_slug LIKE ?))", v, v)
|
||||||
} else if txt.NotEmpty(f.Albums) {
|
} else if txt.NotEmpty(f.Albums) {
|
||||||
for _, where := range LikeAnyWord("a.album_title", f.Albums) {
|
for _, where := range LikeAnyWord("a.album_title", f.Albums) {
|
||||||
s = s.Where("photos.photo_uid IN (SELECT pa.photo_uid FROM photos_albums pa JOIN albums a ON a.album_uid = pa.album_uid AND pa.hidden = 0 WHERE (?))", gorm.Expr(where))
|
s = s.Where("files.photo_uid IN (SELECT pa.photo_uid FROM photos_albums pa JOIN albums a ON a.album_uid = pa.album_uid AND pa.hidden = 0 WHERE (?))", gorm.Expr(where))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -520,7 +552,7 @@ func Photos(f form.SearchPhotos) (results PhotoResults, count int, err error) {
|
||||||
log.Debugf("photos: found %s for %s [%s]", english.Plural(len(results), "result", "results"), f.SerializeAll(), time.Since(start))
|
log.Debugf("photos: found %s for %s [%s]", english.Plural(len(results), "result", "results"), f.SerializeAll(), time.Since(start))
|
||||||
|
|
||||||
if f.Merged {
|
if f.Merged {
|
||||||
return results.Merged()
|
return results.Merge()
|
||||||
}
|
}
|
||||||
|
|
||||||
return results, len(results), nil
|
return results, len(results), nil
|
||||||
|
|
|
@ -63,7 +63,6 @@ func TestPhotosFilterAlbum(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Needs review, variable number of results.
|
|
||||||
assert.GreaterOrEqual(t, len(photos), 0)
|
assert.GreaterOrEqual(t, len(photos), 0)
|
||||||
})
|
})
|
||||||
t.Run("album middle &", func(t *testing.T) {
|
t.Run("album middle &", func(t *testing.T) {
|
||||||
|
@ -181,7 +180,6 @@ func TestPhotosFilterAlbum(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
// TODO: Needs review, variable number of results.
|
|
||||||
|
|
||||||
assert.GreaterOrEqual(t, len(photos), 0)
|
assert.GreaterOrEqual(t, len(photos), 0)
|
||||||
})
|
})
|
||||||
|
@ -196,7 +194,6 @@ func TestPhotosFilterAlbum(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
// TODO: Needs review, variable number of results.
|
|
||||||
|
|
||||||
assert.GreaterOrEqual(t, len(photos), 0)
|
assert.GreaterOrEqual(t, len(photos), 0)
|
||||||
})
|
})
|
||||||
|
@ -306,7 +303,6 @@ func TestPhotosQueryAlbum(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Needs review, variable number of results.
|
|
||||||
assert.GreaterOrEqual(t, len(photos), 0)
|
assert.GreaterOrEqual(t, len(photos), 0)
|
||||||
})
|
})
|
||||||
t.Run("album middle &", func(t *testing.T) {
|
t.Run("album middle &", func(t *testing.T) {
|
||||||
|
@ -442,9 +438,8 @@ func TestPhotosQueryAlbum(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
// TODO: Needs review, variable number of results.
|
|
||||||
|
|
||||||
assert.GreaterOrEqual(t, len(photos), 0)
|
assert.Greater(t, 2, len(photos))
|
||||||
})
|
})
|
||||||
t.Run("album end |", func(t *testing.T) {
|
t.Run("album end |", func(t *testing.T) {
|
||||||
var f form.SearchPhotos
|
var f form.SearchPhotos
|
||||||
|
|
|
@ -3,8 +3,9 @@ package search
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/form"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/form"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPhotosFilterAlbums(t *testing.T) {
|
func TestPhotosFilterAlbums(t *testing.T) {
|
||||||
|
@ -182,22 +183,20 @@ func TestPhotosFilterAlbums(t *testing.T) {
|
||||||
var f form.SearchPhotos
|
var f form.SearchPhotos
|
||||||
|
|
||||||
f.Albums = "Red|Green"
|
f.Albums = "Red|Green"
|
||||||
f.Merged = false
|
f.Merged = true
|
||||||
|
|
||||||
|
photos, count, err := Photos(f)
|
||||||
|
|
||||||
UnscopedDb().LogMode(true)
|
|
||||||
photos, _, err := Photos(f)
|
|
||||||
UnscopedDb().LogMode(false)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Needs review, variable number of results.
|
if len(photos) != 1 {
|
||||||
if len(photos) > 0 {
|
t.Logf("excactly one result expected, but %d photos with %d files found", len(photos), count)
|
||||||
// UID: pt9jtdre2lvl0yh0
|
t.Logf("query results: %#v", photos)
|
||||||
t.Logf("Search Result: %#v", photos)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, 0, len(photos))
|
assert.Equal(t, 1, len(photos))
|
||||||
})
|
})
|
||||||
t.Run("EndsWithPipe", func(t *testing.T) {
|
t.Run("EndsWithPipe", func(t *testing.T) {
|
||||||
var f form.SearchPhotos
|
var f form.SearchPhotos
|
||||||
|
@ -210,6 +209,7 @@ func TestPhotosFilterAlbums(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Greater(t, len(photos), 0)
|
assert.Greater(t, len(photos), 0)
|
||||||
})
|
})
|
||||||
t.Run("StartsWithNumber", func(t *testing.T) {
|
t.Run("StartsWithNumber", func(t *testing.T) {
|
||||||
|
@ -428,23 +428,20 @@ func TestPhotosQueryAlbums(t *testing.T) {
|
||||||
var f form.SearchPhotos
|
var f form.SearchPhotos
|
||||||
|
|
||||||
f.Query = "albums:\"Red|Green\""
|
f.Query = "albums:\"Red|Green\""
|
||||||
f.Merged = false
|
f.Merged = true
|
||||||
UnscopedDb().LogMode(true)
|
|
||||||
photos, _, err := Photos(f)
|
photos, count, err := Photos(f)
|
||||||
UnscopedDb().LogMode(false)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Needs review, variable number of results.
|
if len(photos) != 1 {
|
||||||
if len(photos) > 0 {
|
t.Logf("excactly one result expected, but %d photos with %d files found", len(photos), count)
|
||||||
// UID pt9jtdre2lvl0yh0
|
t.Logf("query results: %#v", photos)
|
||||||
for _, p := range photos {
|
|
||||||
t.Logf("%#v", p)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, 0, len(photos))
|
assert.Equal(t, 1, len(photos))
|
||||||
})
|
})
|
||||||
t.Run("EndsWithPipe", func(t *testing.T) {
|
t.Run("EndsWithPipe", func(t *testing.T) {
|
||||||
var f form.SearchPhotos
|
var f form.SearchPhotos
|
||||||
|
|
|
@ -116,7 +116,8 @@ func TestPhotosQueryFavorite(t *testing.T) {
|
||||||
t.Run("CenterSingleQuote", func(t *testing.T) {
|
t.Run("CenterSingleQuote", func(t *testing.T) {
|
||||||
var f form.SearchPhotos
|
var f form.SearchPhotos
|
||||||
|
|
||||||
f.Query = "favorite:\"Father's Day\""
|
// Note: If the string in favorite starts with f/F, the txt package will assume it means false,
|
||||||
|
f.Query = "favorite:\"Mother's Day\""
|
||||||
f.Merged = true
|
f.Merged = true
|
||||||
|
|
||||||
photos, _, err := Photos(f)
|
photos, _, err := Photos(f)
|
||||||
|
@ -124,7 +125,7 @@ func TestPhotosQueryFavorite(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
//TODO should not fail
|
|
||||||
assert.Equal(t, len(photos), len(photos0))
|
assert.Equal(t, len(photos), len(photos0))
|
||||||
})
|
})
|
||||||
t.Run("EndsWithSingleQuote", func(t *testing.T) {
|
t.Run("EndsWithSingleQuote", func(t *testing.T) {
|
||||||
|
|
|
@ -111,38 +111,38 @@ func (m PhotoResults) UIDs() []string {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m PhotoResults) Merged() (PhotoResults, int, error) {
|
// Merge consecutive file results that belong to the same photo.
|
||||||
count := len(m)
|
func (m PhotoResults) Merge() (photos PhotoResults, count int, err error) {
|
||||||
merged := make([]Photo, 0, count)
|
count = len(m)
|
||||||
|
photos = make(PhotoResults, 0, count)
|
||||||
|
|
||||||
var lastId uint
|
|
||||||
var i int
|
var i int
|
||||||
|
var photoId uint
|
||||||
|
|
||||||
for _, res := range m {
|
for _, photo := range m {
|
||||||
file := entity.File{}
|
file := entity.File{}
|
||||||
|
|
||||||
if err := deepcopier.Copy(&file).From(res); err != nil {
|
if err = deepcopier.Copy(&file).From(photo); err != nil {
|
||||||
return merged, count, err
|
return photos, count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
file.ID = res.FileID
|
file.ID = photo.FileID
|
||||||
res.CompositeID = fmt.Sprintf("%d-%d", res.ID, res.FileID)
|
|
||||||
|
|
||||||
if lastId == res.ID && i > 0 {
|
if photoId == photo.ID && i > 0 {
|
||||||
merged[i-1].Files = append(merged[i-1].Files, file)
|
photos[i-1].Files = append(photos[i-1].Files, file)
|
||||||
merged[i-1].Merged = true
|
photos[i-1].Merged = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
res.Files = append(res.Files, file)
|
|
||||||
|
|
||||||
merged = append(merged, res)
|
|
||||||
|
|
||||||
lastId = res.ID
|
|
||||||
i++
|
i++
|
||||||
|
photoId = photo.ID
|
||||||
|
photo.CompositeID = fmt.Sprintf("%d-%d", photoId, file.ID)
|
||||||
|
photo.Files = append(photo.Files, file)
|
||||||
|
|
||||||
|
photos = append(photos, photo)
|
||||||
}
|
}
|
||||||
|
|
||||||
return merged, count, nil
|
return photos, count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShareBase returns a meaningful file name for sharing.
|
// ShareBase returns a meaningful file name for sharing.
|
||||||
|
|
|
@ -128,7 +128,7 @@ func TestPhotosResults_Merged(t *testing.T) {
|
||||||
|
|
||||||
results := PhotoResults{result1, result2}
|
results := PhotoResults{result1, result2}
|
||||||
|
|
||||||
merged, count, err := results.Merged()
|
merged, count, err := results.Merge()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|
|
@ -12,11 +12,10 @@ func TestMain(m *testing.M) {
|
||||||
log = logrus.StandardLogger()
|
log = logrus.StandardLogger()
|
||||||
log.SetLevel(logrus.TraceLevel)
|
log.SetLevel(logrus.TraceLevel)
|
||||||
|
|
||||||
if err := os.Remove(".test.db"); err == nil {
|
db := entity.InitTestDb(
|
||||||
log.Debugln("removed .test.db")
|
os.Getenv("PHOTOPRISM_TEST_DRIVER"),
|
||||||
}
|
os.Getenv("PHOTOPRISM_TEST_DSN"))
|
||||||
|
|
||||||
db := entity.InitTestDb(os.Getenv("PHOTOPRISM_TEST_DRIVER"), os.Getenv("PHOTOPRISM_TEST_DSN"))
|
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
code := m.Run()
|
code := m.Run()
|
||||||
|
|
Loading…
Reference in a new issue