Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
Theresa Gresch 2020-04-29 10:30:57 +02:00
commit b8416f3d04
37 changed files with 305 additions and 266 deletions

View file

@ -15,14 +15,14 @@ assets-path: ~/.local/share/photoprism
resources-path: ~/.local/share/photoprism/resources resources-path: ~/.local/share/photoprism/resources
originals-path: ~/Pictures/Originals originals-path: ~/Pictures/Originals
import-path: ~/Pictures/Import import-path: ~/Pictures/Import
sql-host: localhost
sql-port: 4000
sql-password: photoprism
http-host: http-host:
http-mode: release http-mode: release
http-port: 2342 http-port: 2342
database-driver: internal tidb-host: localhost
database-dsn: root:photoprism@tcp(localhost:4000)/photoprism?parseTime=true tidb-port: 2343
tidb-password: photoprism
database-driver: tidb
database-dsn: root:photoprism@tcp(localhost:2343)/photoprism?parseTime=true
pid-filename: ~/.local/share/photoprism/photoprism.pid pid-filename: ~/.local/share/photoprism/photoprism.pid
log-filename: ~/.local/share/photoprism/photoprism.log log-filename: ~/.local/share/photoprism/photoprism.log
detach-server: false detach-server: false

View file

@ -12,23 +12,23 @@ services:
- "~/.cache/go-mod:/go/pkg/mod" - "~/.cache/go-mod:/go/pkg/mod"
environment: environment:
PHOTOPRISM_URL: "http://localhost:2342/" PHOTOPRISM_URL: "http://localhost:2342/"
PHOTOPRISM_UPLOAD_NSFW: "false"
PHOTOPRISM_DETECT_NSFW: "true"
PHOTOPRISM_PID_FILENAME: "photoprism.pid"
PHOTOPRISM_LOG_FILENAME: "photoprism.log"
PHOTOPRISM_DETACH_SERVER: "true"
PHOTOPRISM_HTTP_HOST: "0.0.0.0"
PHOTOPRISM_HTTP_PORT: 2342
PHOTOPRISM_TIDB_HOST: "0.0.0.0"
PHOTOPRISM_TIDB_PORT: 2343
PHOTOPRISM_TIDB_PASSWORD: "photoprism"
PHOTOPRISM_DATABASE_DRIVER: "tidb"
PHOTOPRISM_DATABASE_DSN: "root:photoprism@tcp(localhost:2343)/photoprism?parseTime=true"
PHOTOPRISM_TITLE: "PhotoPrism" PHOTOPRISM_TITLE: "PhotoPrism"
PHOTOPRISM_SUBTITLE: "Browse your life" PHOTOPRISM_SUBTITLE: "Browse your life"
PHOTOPRISM_DESCRIPTION: "Personal Photo Management tested by Travis CI." PHOTOPRISM_DESCRIPTION: "Personal Photo Management tested by Travis CI."
PHOTOPRISM_AUTHOR: "PhotoPrism.org" PHOTOPRISM_AUTHOR: "PhotoPrism.org"
PHOTOPRISM_TWITTER: "@browseyourlife" PHOTOPRISM_TWITTER: "@browseyourlife"
PHOTOPRISM_HTTP_HOST: "0.0.0.0"
PHOTOPRISM_HTTP_PORT: 2342
PHOTOPRISM_SQL_HOST: "0.0.0.0"
PHOTOPRISM_SQL_PORT: 4000
PHOTOPRISM_SQL_PASSWORD: "photoprism"
PHOTOPRISM_DATABASE_DSN: "root:photoprism@tcp(localhost:4000)/photoprism?parseTime=true"
PHOTOPRISM_DATABASE_DRIVER: "internal"
PHOTOPRISM_PID_FILENAME: "photoprism.pid"
PHOTOPRISM_LOG_FILENAME: "photoprism.log"
PHOTOPRISM_DETACH_SERVER: "true"
PHOTOPRISM_UPLOAD_NSFW: "false"
PHOTOPRISM_DETECT_NSFW: "true"
CODECOV_TOKEN: CODECOV_TOKEN:
CODECOV_ENV: CODECOV_ENV:
CODECOV_URL: CODECOV_URL:

View file

@ -8,16 +8,12 @@ services:
- photoprism-db - photoprism-db
ports: ports:
- "2342:2342" # Web Server (PhotoPrism) - "2342:2342" # Web Server (PhotoPrism)
- "4000:4000" # Database (MySQL compatible) - "2343:2343" # Database (built-in TiDB)
volumes: volumes:
- ".:/go/src/github.com/photoprism/photoprism" - ".:/go/src/github.com/photoprism/photoprism"
shm_size: "2gb" shm_size: "2gb"
environment: environment:
PHOTOPRISM_URL: "http://localhost:2342/" PHOTOPRISM_URL: "http://localhost:2342/"
PHOTOPRISM_TITLE: "PhotoPrism"
PHOTOPRISM_SUBTITLE: "Browse your life"
PHOTOPRISM_AUTHOR: "PhotoPrism.org"
PHOTOPRISM_TWITTER: "@browseyourlife"
PHOTOPRISM_DEBUG: "true" PHOTOPRISM_DEBUG: "true"
PHOTOPRISM_READONLY: "false" PHOTOPRISM_READONLY: "false"
PHOTOPRISM_PUBLIC: "false" PHOTOPRISM_PUBLIC: "false"
@ -25,6 +21,13 @@ services:
PHOTOPRISM_UPLOAD_NSFW: "false" PHOTOPRISM_UPLOAD_NSFW: "false"
PHOTOPRISM_DETECT_NSFW: "true" PHOTOPRISM_DETECT_NSFW: "true"
PHOTOPRISM_SERVER_MODE: "debug" PHOTOPRISM_SERVER_MODE: "debug"
PHOTOPRISM_HTTP_HOST: "0.0.0.0"
PHOTOPRISM_HTTP_PORT: 2342
PHOTOPRISM_TIDB_HOST: "0.0.0.0"
PHOTOPRISM_TIDB_PORT: 2343
PHOTOPRISM_TIDB_PASSWORD: "photoprism"
PHOTOPRISM_DATABASE_DRIVER: "tidb"
PHOTOPRISM_DATABASE_DSN: "root:photoprism@tcp(localhost:2343)/photoprism?parseTime=true"
PHOTOPRISM_ASSETS_PATH: "/go/src/github.com/photoprism/photoprism/assets" PHOTOPRISM_ASSETS_PATH: "/go/src/github.com/photoprism/photoprism/assets"
PHOTOPRISM_CACHE_PATH: "/go/src/github.com/photoprism/photoprism/assets/cache" PHOTOPRISM_CACHE_PATH: "/go/src/github.com/photoprism/photoprism/assets/cache"
PHOTOPRISM_RESOURCES_PATH: "/go/src/github.com/photoprism/photoprism/assets/resources" PHOTOPRISM_RESOURCES_PATH: "/go/src/github.com/photoprism/photoprism/assets/resources"
@ -32,13 +35,10 @@ services:
PHOTOPRISM_IMPORT_PATH: "/go/src/github.com/photoprism/photoprism/assets/photos/import" PHOTOPRISM_IMPORT_PATH: "/go/src/github.com/photoprism/photoprism/assets/photos/import"
PHOTOPRISM_TEMP_PATH: "/go/src/github.com/photoprism/photoprism/assets/photos/temp" PHOTOPRISM_TEMP_PATH: "/go/src/github.com/photoprism/photoprism/assets/photos/temp"
PHOTOPRISM_ORIGINALS_PATH: "/go/src/github.com/photoprism/photoprism/assets/photos/originals" PHOTOPRISM_ORIGINALS_PATH: "/go/src/github.com/photoprism/photoprism/assets/photos/originals"
PHOTOPRISM_DATABASE_DRIVER: "internal" PHOTOPRISM_TITLE: "PhotoPrism"
PHOTOPRISM_DATABASE_DSN: "root:photoprism@tcp(localhost:4000)/photoprism?parseTime=true" PHOTOPRISM_SUBTITLE: "Browse your life"
PHOTOPRISM_HTTP_HOST: "0.0.0.0" PHOTOPRISM_AUTHOR: "PhotoPrism.org"
PHOTOPRISM_HTTP_PORT: 2342 PHOTOPRISM_TWITTER: "@browseyourlife"
PHOTOPRISM_SQL_HOST: "0.0.0.0"
PHOTOPRISM_SQL_PORT: 4000
PHOTOPRISM_SQL_PASSWORD: "photoprism"
TF_CPP_MIN_LOG_LEVEL: 0 TF_CPP_MIN_LOG_LEVEL: 0
photoprism-db: photoprism-db:

View file

@ -119,8 +119,8 @@ RUN echo "alias go=richgo" > /root/.bash_aliases
# Set up project directory # Set up project directory
WORKDIR "/go/src/github.com/photoprism/photoprism" WORKDIR "/go/src/github.com/photoprism/photoprism"
# Expose HTTP port 2342 plus 4000 for TiDB and 9515 for chromedriver # Expose HTTP port 2342 plus 2343 for TiDB and 9515 for chromedriver
EXPOSE 2342 4000 9515 EXPOSE 2342 2343 9515
# Keep container running (services can be started manually using a terminal) # Keep container running (services can be started manually using a terminal)
CMD tail -f /dev/null CMD tail -f /dev/null

View file

@ -56,7 +56,7 @@ ENV PATH /photoprism/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin
ENV PHOTOPRISM_ORIGINALS_PATH /photoprism/originals ENV PHOTOPRISM_ORIGINALS_PATH /photoprism/originals
ENV PHOTOPRISM_IMPORT_PATH /photoprism/import ENV PHOTOPRISM_IMPORT_PATH /photoprism/import
ENV PHOTOPRISM_DATABASE_PATH /photoprism/database ENV PHOTOPRISM_TIDB_PATH /photoprism/database
ENV PHOTOPRISM_TEMP_PATH /photoprism/temp ENV PHOTOPRISM_TEMP_PATH /photoprism/temp
ENV PHOTOPRISM_CACHE_PATH /photoprism/cache ENV PHOTOPRISM_CACHE_PATH /photoprism/cache
ENV PHOTOPRISM_CONFIG_PATH /photoprism/config ENV PHOTOPRISM_CONFIG_PATH /photoprism/config
@ -85,7 +85,7 @@ RUN chmod -R 777 /photoprism
RUN photoprism -v RUN photoprism -v
# Expose http and database ports # Expose http and database ports
EXPOSE 2342 4000 EXPOSE 2342 2343
# Run server # Run server
CMD photoprism start CMD photoprism start

View file

@ -52,7 +52,7 @@ RUN apt-get update && apt-get upgrade && \
ENV LD_LIBRARY_PATH /root/.local/lib:/usr/local/lib:/usr/lib:/lib ENV LD_LIBRARY_PATH /root/.local/lib:/usr/local/lib:/usr/lib:/lib
ENV TF_CPP_MIN_LOG_LEVEL 0 ENV TF_CPP_MIN_LOG_LEVEL 0
RUN curl -L \ RUN curl -L \
"https://dl.photoprism.org/tensorflow/arm64/libtensorflow-arm64-1.14.0.tar.gz" | \ "https://dl.photoprism.org/tensorflow/arm64/libtensorflow-arm64-1.15.2.tar.gz" | \
tar -C "/usr" -xz tar -C "/usr" -xz
RUN ldconfig RUN ldconfig
@ -94,7 +94,7 @@ RUN wget "https://dl.photoprism.org/tensorflow/nasnet.zip?${BUILD_TAG}" -O /tmp/
# Set up project directory # Set up project directory
WORKDIR "/go/src/github.com/photoprism/photoprism" WORKDIR "/go/src/github.com/photoprism/photoprism"
COPY ../../photoprism-arm64 . COPY . .
# Build PhotoPrism # Build PhotoPrism
RUN make dep build-js install RUN make dep build-js install
@ -140,7 +140,7 @@ ENV PATH /photoprism/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin
ENV PHOTOPRISM_ORIGINALS_PATH /photoprism/originals ENV PHOTOPRISM_ORIGINALS_PATH /photoprism/originals
ENV PHOTOPRISM_IMPORT_PATH /photoprism/import ENV PHOTOPRISM_IMPORT_PATH /photoprism/import
ENV PHOTOPRISM_DATABASE_PATH /photoprism/database ENV PHOTOPRISM_TIDB_PATH /photoprism/database
ENV PHOTOPRISM_TEMP_PATH /photoprism/temp ENV PHOTOPRISM_TEMP_PATH /photoprism/temp
ENV PHOTOPRISM_CACHE_PATH /photoprism/cache ENV PHOTOPRISM_CACHE_PATH /photoprism/cache
ENV PHOTOPRISM_CONFIG_PATH /photoprism/config ENV PHOTOPRISM_CONFIG_PATH /photoprism/config
@ -169,7 +169,7 @@ RUN chmod -R 777 /photoprism
RUN photoprism -v RUN photoprism -v
# Expose http and database ports # Expose http and database ports
EXPOSE 2342 4000 EXPOSE 2342 2343
# Run server # Run server
CMD photoprism start CMD photoprism start

View file

@ -13,7 +13,7 @@ services:
- seccomp:unconfined - seccomp:unconfined
ports: ports:
- 2342:2342 # [local port]:[container port] - 2342:2342 # [local port]:[container port]
# - 4000:4000 # Internal database (MySQL compatible) # - 2343:2343 # Database (built-in TiDB)
healthcheck: # Optional healthcheck: # Optional
test: "photoprism status" test: "photoprism status"
interval: 60s interval: 60s
@ -35,13 +35,13 @@ services:
PHOTOPRISM_DISABLE_SETTINGS: "false" PHOTOPRISM_DISABLE_SETTINGS: "false"
PHOTOPRISM_HTTP_HOST: "0.0.0.0" PHOTOPRISM_HTTP_HOST: "0.0.0.0"
PHOTOPRISM_HTTP_PORT: 2342 PHOTOPRISM_HTTP_PORT: 2342
PHOTOPRISM_SQL_HOST: "0.0.0.0" PHOTOPRISM_TIDB_HOST: "0.0.0.0"
PHOTOPRISM_SQL_PORT: 4000 # Port for internal TiDB SQL server (driver "internal") PHOTOPRISM_TIDB_PORT: 2343 # Port for built-in TiDB SQL server (driver "tidb")
PHOTOPRISM_SQL_PASSWORD: "photoprism" # Plain text only (username "root") PHOTOPRISM_TIDB_PASSWORD: "photoprism" # Plain text only (username "root")
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # Plain text or bcrypt hash (escape "$" with "$$") PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # Plain text or bcrypt hash (escape "$" with "$$")
PHOTOPRISM_WEBDAV_PASSWORD: "photoprism" # Plain text only (username "photoprism") PHOTOPRISM_WEBDAV_PASSWORD: "photoprism" # Plain text only (username "photoprism")
PHOTOPRISM_DATABASE_DRIVER: "internal" # Change to "mysql" for external MySQL or MariaDB PHOTOPRISM_DATABASE_DRIVER: "tidb" # Change to "mysql" for external MySQL or MariaDB
PHOTOPRISM_DATABASE_DSN: "root:photoprism@tcp(localhost:4000)/photoprism?parseTime=true" PHOTOPRISM_DATABASE_DSN: "root:photoprism@tcp(localhost:2343)/photoprism?parseTime=true"
# PHOTOPRISM_THUMB_QUALITY: 95 # High-quality thumbnails (optional) # PHOTOPRISM_THUMB_QUALITY: 95 # High-quality thumbnails (optional)
# PHOTOPRISM_THUMB_SIZE: 3840 # PHOTOPRISM_THUMB_SIZE: 3840
# PHOTOPRISM_THUMB_LIMIT: 3840 # PHOTOPRISM_THUMB_LIMIT: 3840

View file

@ -12,7 +12,7 @@ services:
restart: unless-stopped restart: unless-stopped
ports: ports:
- 2342:2342 # [local port]:[container port] - 2342:2342 # [local port]:[container port]
# - 4000:4000 # Internal database (MySQL compatible) # - 2343:2343 # Database (built-in TiDB)
healthcheck: # Optional healthcheck: # Optional
test: "photoprism status" test: "photoprism status"
interval: 60s interval: 60s
@ -34,13 +34,13 @@ services:
PHOTOPRISM_DISABLE_SETTINGS: "false" PHOTOPRISM_DISABLE_SETTINGS: "false"
PHOTOPRISM_HTTP_HOST: "0.0.0.0" PHOTOPRISM_HTTP_HOST: "0.0.0.0"
PHOTOPRISM_HTTP_PORT: 2342 PHOTOPRISM_HTTP_PORT: 2342
PHOTOPRISM_SQL_HOST: "0.0.0.0" PHOTOPRISM_TIDB_HOST: "0.0.0.0"
PHOTOPRISM_SQL_PORT: 4000 # Port for internal TiDB SQL server (driver "internal") PHOTOPRISM_TIDB_PORT: 2343 # Port for built-in TiDB SQL server (driver "tidb")
PHOTOPRISM_SQL_PASSWORD: "photoprism" # Plain text only (username "root") PHOTOPRISM_TIDB_PASSWORD: "photoprism" # Plain text only (username "root")
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # Plain text or bcrypt hash (escape "$" with "$$") PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # Plain text or bcrypt hash (escape "$" with "$$")
PHOTOPRISM_WEBDAV_PASSWORD: "photoprism" # Plain text only (username "photoprism") PHOTOPRISM_WEBDAV_PASSWORD: "photoprism" # Plain text only (username "photoprism")
PHOTOPRISM_DATABASE_DRIVER: "internal" # Change to "mysql" for external MySQL or MariaDB PHOTOPRISM_DATABASE_DRIVER: "tidb" # Change to "mysql" for external MySQL or MariaDB
PHOTOPRISM_DATABASE_DSN: "root:photoprism@tcp(localhost:4000)/photoprism?parseTime=true" PHOTOPRISM_DATABASE_DSN: "root:photoprism@tcp(localhost:2343)/photoprism?parseTime=true"
# PHOTOPRISM_DATABASE_DRIVER: "mysql" # Using MariaDB or MySQL instead of the internal TiDB is optional # PHOTOPRISM_DATABASE_DRIVER: "mysql" # Using MariaDB or MySQL instead of the internal TiDB is optional
# PHOTOPRISM_DATABASE_DSN: "photoprism:photoprism@tcp(photoprism-db:3306)/photoprism?parseTime=true" # PHOTOPRISM_DATABASE_DSN: "photoprism:photoprism@tcp(photoprism-db:3306)/photoprism?parseTime=true"
# PHOTOPRISM_THUMB_QUALITY: 95 # High-quality thumbnails (optional, default JPEG quality is 90) # PHOTOPRISM_THUMB_QUALITY: 95 # High-quality thumbnails (optional, default JPEG quality is 90)

View file

@ -85,7 +85,7 @@
<div class="caption"> <div class="caption">
<button @click.exact="editPhoto(index)"> <button @click.exact="editPhoto(index)">
<v-icon size="14" title="Taken" v-if="photo.TakenSrc">date_range</v-icon> <v-icon size="14" title="Taken" v-if="photo.TakenSrc">date_range</v-icon>
<v-icon size="14" title="Imported" v-else>save</v-icon> <v-icon size="14" title="Modified" v-else>save</v-icon>
{{ photo.getDateString() }} {{ photo.getDateString() }}
</button> </button>
<br/> <br/>

View file

@ -102,7 +102,7 @@ class Photo extends RestModel {
refreshFileAttr() { refreshFileAttr() {
const file = this.mainFile(); const file = this.mainFile();
if (!file) { if (!file || !file.FileHash) {
return; return;
} }

View file

@ -29,7 +29,11 @@ class Thumb extends Model {
let result = []; let result = [];
photos.forEach((p) => { photos.forEach((p) => {
result.push(this.fromPhoto(p)); let thumb = this.fromPhoto(p);
if(thumb) {
result.push(thumb);
}
}); });
return result; return result;
@ -40,11 +44,15 @@ class Thumb extends Model {
return this.fromFile(photo, photo.Files.find(f => !!f.FilePrimary)); return this.fromFile(photo, photo.Files.find(f => !!f.FilePrimary));
} }
if(!photo || !photo.FileHash) {
return false;
}
const result = { const result = {
uuid: photo.PhotoUUID, uuid: photo.PhotoUUID,
title: photo.PhotoTitle, title: photo.PhotoTitle,
favorite: photo.PhotoFavorite, favorite: photo.PhotoFavorite,
download_url: "/api/v1/download/" + photo.FileHash, download_url: this.downloadUrl(photo),
original_w: photo.FileWidth, original_w: photo.FileWidth,
original_h: photo.FileHeight, original_h: photo.FileHeight,
}; };
@ -63,11 +71,15 @@ class Thumb extends Model {
} }
static fromFile(photo, file) { static fromFile(photo, file) {
if(!photo || !file || !file.FileHash) {
return false;
}
const result = { const result = {
uuid: photo.PhotoUUID, uuid: photo.PhotoUUID,
title: photo.PhotoTitle, title: photo.PhotoTitle,
favorite: photo.PhotoFavorite, favorite: photo.PhotoFavorite,
download_url: "/api/v1/download/" + file.FileHash, download_url: this.downloadUrl(file),
original_w: file.FileWidth, original_w: file.FileWidth,
original_h: file.FileHeight, original_h: file.FileHeight,
}; };
@ -92,8 +104,12 @@ class Thumb extends Model {
if (!p.Files) return; if (!p.Files) return;
p.Files.forEach((f) => { p.Files.forEach((f) => {
if (f.FileType === "jpg") { if (f && f.FileType === "jpg") {
result.push(this.fromFile(p, f)); let thumb = this.fromFile(p, f);
if(thumb) {
result.push(thumb);
}
} }
} }
); );
@ -132,6 +148,15 @@ class Thumb extends Model {
return "/api/v1/thumbnails/" + file.FileHash + "/" + type; return "/api/v1/thumbnails/" + file.FileHash + "/" + type;
} }
static downloadUrl(file) {
if (!file || !file.FileHash) {
return "";
}
return "/api/v1/download/" + file.FileHash;
}
} }
export default Thumb; export default Thumb;

View file

@ -57,18 +57,18 @@ func configAction(ctx *cli.Context) error {
fmt.Printf("static-path %s\n", conf.HttpStaticPath()) fmt.Printf("static-path %s\n", conf.HttpStaticPath())
fmt.Printf("static-build-path %s\n", conf.HttpStaticBuildPath()) fmt.Printf("static-build-path %s\n", conf.HttpStaticBuildPath())
fmt.Printf("database-path %s\n", conf.DatabasePath())
fmt.Printf("database-driver %s\n", conf.DatabaseDriver())
fmt.Printf("database-dsn %s\n", conf.DatabaseDsn())
fmt.Printf("sql-host %s\n", conf.SqlServerHost())
fmt.Printf("sql-port %d\n", conf.SqlServerPort())
fmt.Printf("sql-password %s\n", conf.SqlServerPassword())
fmt.Printf("http-host %s\n", conf.HttpServerHost()) fmt.Printf("http-host %s\n", conf.HttpServerHost())
fmt.Printf("http-port %d\n", conf.HttpServerPort()) fmt.Printf("http-port %d\n", conf.HttpServerPort())
fmt.Printf("http-mode %s\n", conf.HttpServerMode()) fmt.Printf("http-mode %s\n", conf.HttpServerMode())
fmt.Printf("tidb-host %s\n", conf.TidbServerHost())
fmt.Printf("tidb-port %d\n", conf.TidbServerPort())
fmt.Printf("tidb-password %s\n", conf.TidbServerPassword())
fmt.Printf("tidb-path %s\n", conf.TidbServerPath())
fmt.Printf("database-driver %s\n", conf.DatabaseDriver())
fmt.Printf("database-dsn %s\n", conf.DatabaseDsn())
fmt.Printf("sips-bin %s\n", conf.SipsBin()) fmt.Printf("sips-bin %s\n", conf.SipsBin())
fmt.Printf("darktable-bin %s\n", conf.DarktableBin()) fmt.Printf("darktable-bin %s\n", conf.DarktableBin())
fmt.Printf("exiftool-bin %s\n", conf.ExifToolBin()) fmt.Printf("exiftool-bin %s\n", conf.ExifToolBin())

View file

@ -51,11 +51,11 @@ func startAction(ctx *cli.Context) error {
if ctx.IsSet("config") { if ctx.IsSet("config") {
fmt.Printf("NAME VALUE\n") fmt.Printf("NAME VALUE\n")
fmt.Printf("detach-server %t\n", conf.DetachServer()) fmt.Printf("detach-server %t\n", conf.DetachServer())
fmt.Printf("database-path %s\n", conf.DatabasePath())
fmt.Printf("sql-host %s\n", conf.SqlServerHost()) fmt.Printf("tidb-host %s\n", conf.TidbServerHost())
fmt.Printf("sql-port %d\n", conf.SqlServerPort()) fmt.Printf("tidb-port %d\n", conf.TidbServerPort())
fmt.Printf("sql-password %s\n", conf.SqlServerPassword()) fmt.Printf("tidb-password %s\n", conf.TidbServerPassword())
fmt.Printf("tidb-path %s\n", conf.TidbServerPath())
fmt.Printf("http-host %s\n", conf.HttpServerHost()) fmt.Printf("http-host %s\n", conf.HttpServerHost())
fmt.Printf("http-port %d\n", conf.HttpServerPort()) fmt.Printf("http-port %d\n", conf.HttpServerPort())

View file

@ -36,7 +36,7 @@ func TestConfig_Version(t *testing.T) {
c := NewConfig(ctx) c := NewConfig(ctx)
version := c.Version() version := c.Version()
assert.Equal(t, "1.0.0", version) assert.Equal(t, "test", version)
} }
func TestConfig_TensorFlowVersion(t *testing.T) { func TestConfig_TensorFlowVersion(t *testing.T) {
@ -103,35 +103,35 @@ func TestConfig_DetachServer(t *testing.T) {
assert.Equal(t, false, detachServer) assert.Equal(t, false, detachServer)
} }
func TestConfig_SqlServerHost(t *testing.T) { func TestConfig_TidbServerHost(t *testing.T) {
ctx := CliTestContext() ctx := CliTestContext()
c := NewConfig(ctx) c := NewConfig(ctx)
host := c.SqlServerHost() host := c.TidbServerHost()
assert.Equal(t, "127.0.0.1", host) assert.Equal(t, "127.0.0.1", host)
} }
func TestConfig_SqlServerPort(t *testing.T) { func TestConfig_TidbServerPort(t *testing.T) {
ctx := CliTestContext() ctx := CliTestContext()
c := NewConfig(ctx) c := NewConfig(ctx)
port := c.SqlServerPort() port := c.TidbServerPort()
assert.Equal(t, uint(4000), port) assert.Equal(t, uint(2343), port)
} }
func TestConfig_SqlServerPath(t *testing.T) { func TestConfig_TidbServerPath(t *testing.T) {
ctx := CliTestContext() ctx := CliTestContext()
c := NewConfig(ctx) c := NewConfig(ctx)
path := c.DatabasePath() path := c.TidbServerPath()
assert.Equal(t, "/go/src/github.com/photoprism/photoprism/assets/resources/database", path) assert.Equal(t, "/go/src/github.com/photoprism/photoprism/assets/resources/database", path)
} }
func TestConfig_SqlServerPassword(t *testing.T) { func TestConfig_TidbServerPassword(t *testing.T) {
ctx := CliTestContext() ctx := CliTestContext()
c := NewConfig(ctx) c := NewConfig(ctx)
password := c.SqlServerPassword() password := c.TidbServerPassword()
assert.Equal(t, "", password) assert.Equal(t, "", password)
} }
@ -222,7 +222,7 @@ func TestConfig_DatabaseDriver(t *testing.T) {
c := NewConfig(ctx) c := NewConfig(ctx)
driver := c.DatabaseDriver() driver := c.DatabaseDriver()
assert.Equal(t, "internal", driver) assert.Equal(t, DriverTidb, driver)
} }
func TestConfig_DatabaseDsn(t *testing.T) { func TestConfig_DatabaseDsn(t *testing.T) {
@ -230,7 +230,7 @@ func TestConfig_DatabaseDsn(t *testing.T) {
c := NewConfig(ctx) c := NewConfig(ctx)
dsn := c.DatabaseDriver() dsn := c.DatabaseDriver()
assert.Equal(t, "internal", dsn) assert.Equal(t, DriverTidb, dsn)
} }
func TestConfig_CachePath(t *testing.T) { func TestConfig_CachePath(t *testing.T) {

View file

@ -18,17 +18,17 @@ import (
// DatabaseDriver returns the database driver name. // DatabaseDriver returns the database driver name.
func (c *Config) DatabaseDriver() string { func (c *Config) DatabaseDriver() string {
if c.params.DatabaseDriver == "" { if strings.ToLower(c.params.DatabaseDriver) == "mysql" {
return DbTiDB return DriverMysql
} }
return c.params.DatabaseDriver return DriverTidb
} }
// DatabaseDsn returns the database data source name (DSN). // DatabaseDsn returns the database data source name (DSN).
func (c *Config) DatabaseDsn() string { func (c *Config) DatabaseDsn() string {
if c.params.DatabaseDsn == "" { if c.params.DatabaseDsn == "" {
return "root:photoprism@tcp(localhost:4000)/photoprism?parseTime=true" return "root:photoprism@tcp(localhost:2343)/photoprism?parseTime=true"
} }
return c.params.DatabaseDsn return c.params.DatabaseDsn
@ -67,7 +67,6 @@ func (c *Config) MigrateDb() {
&entity.FileSync{}, &entity.FileSync{},
&entity.Photo{}, &entity.Photo{},
&entity.Description{}, &entity.Description{},
&entity.Event{},
&entity.Place{}, &entity.Place{},
&entity.Location{}, &entity.Location{},
&entity.Camera{}, &entity.Camera{},
@ -106,7 +105,6 @@ func (c *Config) DropTables() {
&entity.FileSync{}, &entity.FileSync{},
&entity.Photo{}, &entity.Photo{},
&entity.Description{}, &entity.Description{},
&entity.Event{},
&entity.Place{}, &entity.Place{},
&entity.Location{}, &entity.Location{},
&entity.Camera{}, &entity.Camera{},
@ -146,17 +144,17 @@ func (c *Config) connectToDatabase(ctx context.Context) error {
isTiDB := false isTiDB := false
initSuccess := false initSuccess := false
if dbDriver == DbTiDB { if dbDriver == DriverTidb {
isTiDB = true isTiDB = true
dbDriver = DbMySQL dbDriver = DriverMysql
} }
db, err := gorm.Open(dbDriver, dbDsn) db, err := gorm.Open(dbDriver, dbDsn)
if err != nil || db == nil { if err != nil || db == nil {
if isTiDB { if isTiDB {
log.Infof("starting database server at %s:%d\n", c.SqlServerHost(), c.SqlServerPort()) log.Infof("starting database server at %s:%d\n", c.TidbServerHost(), c.TidbServerPort())
go tidb.Start(ctx, c.DatabasePath(), c.SqlServerPort(), c.SqlServerHost(), c.Debug()) go tidb.Start(ctx, c.TidbServerPath(), c.TidbServerPort(), c.TidbServerHost(), c.Debug())
} }
for i := 1; i <= 12; i++ { for i := 1; i <= 12; i++ {
@ -169,7 +167,7 @@ func (c *Config) connectToDatabase(ctx context.Context) error {
} }
if isTiDB && !initSuccess { if isTiDB && !initSuccess {
err = tidb.InitDatabase(c.SqlServerPort(), c.SqlServerPassword()) err = tidb.InitDatabase(c.TidbServerPort(), c.TidbServerPassword())
if err != nil { if err != nil {
log.Debug(err) log.Debug(err)

View file

@ -61,8 +61,8 @@ func (c *Config) CreateDirectories() error {
return createError(c.ResourcesPath(), err) return createError(c.ResourcesPath(), err)
} }
if err := os.MkdirAll(c.DatabasePath(), os.ModePerm); err != nil { if err := os.MkdirAll(c.TidbServerPath(), os.ModePerm); err != nil {
return createError(c.DatabasePath(), err) return createError(c.TidbServerPath(), err)
} }
if err := os.MkdirAll(c.TensorFlowModelPath(), os.ModePerm); err != nil { if err := os.MkdirAll(c.TensorFlowModelPath(), os.ModePerm); err != nil {

View file

@ -149,23 +149,6 @@ var GlobalFlags = []cli.Flag{
Value: "~/.local/share/photoprism", Value: "~/.local/share/photoprism",
EnvVar: "PHOTOPRISM_ASSETS_PATH", EnvVar: "PHOTOPRISM_ASSETS_PATH",
}, },
cli.StringFlag{
Name: "database-path",
Usage: "built-in database server storage path",
EnvVar: "PHOTOPRISM_DATABASE_PATH",
},
cli.StringFlag{
Name: "database-driver",
Usage: "database `DRIVER` (internal or mysql)",
Value: "internal",
EnvVar: "PHOTOPRISM_DATABASE_DRIVER",
},
cli.StringFlag{
Name: "database-dsn",
Usage: "database data source name (`DSN`)",
Value: "root:@tcp(localhost:4000)/photoprism?parseTime=true",
EnvVar: "PHOTOPRISM_DATABASE_DSN",
},
cli.StringFlag{ cli.StringFlag{
Name: "sips-bin", Name: "sips-bin",
Usage: "sips cli binary `FILENAME`", Usage: "sips cli binary `FILENAME`",
@ -192,6 +175,7 @@ var GlobalFlags = []cli.Flag{
}, },
cli.IntFlag{ cli.IntFlag{
Name: "http-port", Name: "http-port",
Value: 2342,
Usage: "HTTP server port", Usage: "HTTP server port",
EnvVar: "PHOTOPRISM_HTTP_PORT", EnvVar: "PHOTOPRISM_HTTP_PORT",
}, },
@ -206,19 +190,37 @@ var GlobalFlags = []cli.Flag{
EnvVar: "PHOTOPRISM_HTTP_MODE", EnvVar: "PHOTOPRISM_HTTP_MODE",
}, },
cli.IntFlag{ cli.IntFlag{
Name: "sql-port", Name: "tidb-port",
Usage: "built-in SQL server port", Value: 2343,
EnvVar: "PHOTOPRISM_SQL_PORT", Usage: "built-in TiDB server port",
EnvVar: "PHOTOPRISM_TIDB_PORT",
}, },
cli.StringFlag{ cli.StringFlag{
Name: "sql-host", Name: "tidb-host",
Usage: "built-in SQL server host", Usage: "built-in TiDB server host",
EnvVar: "PHOTOPRISM_SQL_HOST", EnvVar: "PHOTOPRISM_TIDB_HOST",
}, },
cli.StringFlag{ cli.StringFlag{
Name: "sql-password", Name: "tidb-password",
Usage: "built-in SQL server password", Usage: "built-in TiDB server password",
EnvVar: "PHOTOPRISM_SQL_PASSWORD", EnvVar: "PHOTOPRISM_TIDB_PASSWORD",
},
cli.StringFlag{
Name: "tidb-path",
Usage: "built-in TiDB server storage path",
EnvVar: "PHOTOPRISM_TIDB_PATH",
},
cli.StringFlag{
Name: "database-driver",
Usage: "database `DRIVER` (tidb or mysql)",
Value: "tidb",
EnvVar: "PHOTOPRISM_DATABASE_DRIVER",
},
cli.StringFlag{
Name: "database-dsn",
Usage: "database data source name (`DSN`)",
Value: "root:@tcp(localhost:2343)/photoprism?parseTime=true",
EnvVar: "PHOTOPRISM_DATABASE_DSN",
}, },
cli.BoolFlag{ cli.BoolFlag{
Name: "detect-nsfw", Name: "detect-nsfw",

View file

@ -15,8 +15,8 @@ import (
// define database drivers const // define database drivers const
const ( const (
DbTiDB = "internal" DriverTidb = "tidb"
DbMySQL = "mysql" DriverMysql = "mysql"
) )
// Params provides a struct in which application configuration is stored. // Params provides a struct in which application configuration is stored.
@ -54,12 +54,12 @@ type Params struct {
ImportPath string `yaml:"import-path" flag:"import-path"` ImportPath string `yaml:"import-path" flag:"import-path"`
AssetsPath string `yaml:"assets-path" flag:"assets-path"` AssetsPath string `yaml:"assets-path" flag:"assets-path"`
ResourcesPath string `yaml:"resources-path" flag:"resources-path"` ResourcesPath string `yaml:"resources-path" flag:"resources-path"`
DatabasePath string `yaml:"database-path" flag:"database-path"`
DatabaseDriver string `yaml:"database-driver" flag:"database-driver"` DatabaseDriver string `yaml:"database-driver" flag:"database-driver"`
DatabaseDsn string `yaml:"database-dsn" flag:"database-dsn"` DatabaseDsn string `yaml:"database-dsn" flag:"database-dsn"`
SqlServerHost string `yaml:"sql-host" flag:"sql-host"` TidbServerHost string `yaml:"tidb-host" flag:"tidb-host"`
SqlServerPort uint `yaml:"sql-port" flag:"sql-port"` TidbServerPort uint `yaml:"tidb-port" flag:"tidb-port"`
SqlServerPassword string `yaml:"sql-password" flag:"sql-password"` TidbServerPassword string `yaml:"tidb-password" flag:"tidb-password"`
TidbServerPath string `yaml:"tidb-path" flag:"tidb-path"`
HttpServerHost string `yaml:"http-host" flag:"http-host"` HttpServerHost string `yaml:"http-host" flag:"http-host"`
HttpServerPort int `yaml:"http-port" flag:"http-port"` HttpServerPort int `yaml:"http-port" flag:"http-port"`
HttpServerMode string `yaml:"http-mode" flag:"http-mode"` HttpServerMode string `yaml:"http-mode" flag:"http-mode"`
@ -116,7 +116,7 @@ func (c *Params) expandFilenames() {
c.OriginalsPath = fs.Abs(c.OriginalsPath) c.OriginalsPath = fs.Abs(c.OriginalsPath)
c.ImportPath = fs.Abs(c.ImportPath) c.ImportPath = fs.Abs(c.ImportPath)
c.TempPath = fs.Abs(c.TempPath) c.TempPath = fs.Abs(c.TempPath)
c.DatabasePath = fs.Abs(c.DatabasePath) c.TidbServerPath = fs.Abs(c.TidbServerPath)
c.PIDFilename = fs.Abs(c.PIDFilename) c.PIDFilename = fs.Abs(c.PIDFilename)
c.LogFilename = fs.Abs(c.LogFilename) c.LogFilename = fs.Abs(c.LogFilename)
} }

View file

@ -36,7 +36,7 @@ func TestParams_SetValuesFromFile(t *testing.T) {
assert.Equal(t, "/srv/photoprism/photos/originals", c.OriginalsPath) assert.Equal(t, "/srv/photoprism/photos/originals", c.OriginalsPath)
assert.Equal(t, "/srv/photoprism/photos/import", c.ImportPath) assert.Equal(t, "/srv/photoprism/photos/import", c.ImportPath)
assert.Equal(t, "/srv/photoprism/temp", c.TempPath) assert.Equal(t, "/srv/photoprism/temp", c.TempPath)
assert.Equal(t, "internal", c.DatabaseDriver) assert.Equal(t, DriverTidb, c.DatabaseDriver)
assert.Equal(t, "root:photoprism@tcp(localhost:4000)/photoprism?parseTime=true", c.DatabaseDsn) assert.Equal(t, "root:photoprism@tcp(localhost:2343)/photoprism?parseTime=true", c.DatabaseDsn)
assert.Equal(t, 81, c.HttpServerPort) assert.Equal(t, 81, c.HttpServerPort)
} }

View file

@ -2,15 +2,6 @@ package config
import "github.com/photoprism/photoprism/pkg/fs" import "github.com/photoprism/photoprism/pkg/fs"
// DatabasePath returns the database storage path for TiDB.
func (c *Config) DatabasePath() string {
if c.params.DatabasePath == "" {
return c.ResourcesPath() + "/database"
}
return fs.Abs(c.params.DatabasePath)
}
// DetachServer returns true if server should detach from console (daemon mode). // DetachServer returns true if server should detach from console (daemon mode).
func (c *Config) DetachServer() bool { func (c *Config) DetachServer() bool {
return c.params.DetachServer return c.params.DetachServer
@ -72,25 +63,34 @@ func (c *Config) HttpStaticBuildPath() string {
return c.HttpStaticPath() + "/build" return c.HttpStaticPath() + "/build"
} }
// SqlServerHost returns the built-in SQL server host name or IP address (empty for all interfaces). // TidbServerHost returns the host for the built-in TiDB server. (empty for all interfaces).
func (c *Config) SqlServerHost() string { func (c *Config) TidbServerHost() string {
if c.params.SqlServerHost == "" { if c.params.TidbServerHost == "" {
return "127.0.0.1" return "127.0.0.1"
} }
return c.params.SqlServerHost return c.params.TidbServerHost
} }
// SqlServerPort returns the built-in SQL server port. // TidbServerPort returns the port for the built-in TiDB server.
func (c *Config) SqlServerPort() uint { func (c *Config) TidbServerPort() uint {
if c.params.SqlServerPort == 0 { if c.params.TidbServerPort == 0 {
return 4000 return 2343
} }
return c.params.SqlServerPort return c.params.TidbServerPort
} }
// SqlServerPassword returns the password for the built-in database server. // TidbServerPassword returns the password for the built-in TiDB server.
func (c *Config) SqlServerPassword() string { func (c *Config) TidbServerPassword() string {
return c.params.SqlServerPassword return c.params.TidbServerPassword
}
// TidbServerPath returns the database storage path for the built-in TiDB server.
func (c *Config) TidbServerPath() string {
if c.params.TidbServerPath == "" {
return c.ResourcesPath() + "/database"
}
return fs.Abs(c.params.TidbServerPath)
} }

View file

@ -144,7 +144,7 @@ func CliTestContext() *cli.Context {
globalSet.Bool("detect-nsfw", config.DetectNSFW, "doc") globalSet.Bool("detect-nsfw", config.DetectNSFW, "doc")
app := cli.NewApp() app := cli.NewApp()
app.Version = "1.0.0" app.Version = "test"
c := cli.NewContext(app, globalSet, nil) c := cli.NewContext(app, globalSet, nil)

View file

@ -5,14 +5,14 @@ cache-path: /srv/photoprism/cache
originals-path: /srv/photoprism/photos/originals originals-path: /srv/photoprism/photos/originals
import-path: /srv/photoprism/photos/import import-path: /srv/photoprism/photos/import
temp-path: /srv/photoprism/temp temp-path: /srv/photoprism/temp
sql-host: localhost
sql-port: 4000
sql-password: photoprism
http-host: http-host:
http-mode: release http-mode: release
http-port: 81 http-port: 81
http-password: http-password:
database-driver: internal tidb-host: localhost
database-dsn: root:photoprism@tcp(localhost:4000)/photoprism?parseTime=true tidb-port: 2343
tidb-password: photoprism
database-driver: tidb
database-dsn: root:photoprism@tcp(localhost:2343)/photoprism?parseTime=true
theme: lavendel theme: lavendel
language: english language: english

View file

@ -1,36 +0,0 @@
package entity
import (
"time"
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/pkg/rnd"
)
// Event defines temporal event that can be used to link photos together
type Event struct {
EventUUID string `gorm:"type:varbinary(36);unique_index;"`
EventSlug string `gorm:"type:varbinary(255);unique_index;"`
EventName string
EventType string
EventDescription string `gorm:"type:text;"`
EventNotes string `gorm:"type:text;"`
EventBegin time.Time `gorm:"type:datetime;"`
EventEnd time.Time `gorm:"type:datetime;"`
EventLat float32 `gorm:"type:FLOAT;"`
EventLng float32 `gorm:"type:FLOAT;"`
EventDist float32 `gorm:"type:FLOAT;"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `sql:"index"`
}
// TableName returns Event table identifier "events"
func (Event) TableName() string {
return "events"
}
// BeforeCreate computes a random UUID when a new event is created in database
func (e *Event) BeforeCreate(scope *gorm.Scope) error {
return scope.SetColumn("EventUUID", rnd.PPID('e'))
}

View file

@ -1,14 +0,0 @@
package entity
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestEvent_TableName(t *testing.T) {
event := &Event{EventSlug: "christmas-2000"}
tableName := event.TableName()
assert.Equal(t, "events", tableName)
}

View file

@ -45,21 +45,22 @@ func (m *Location) Find(db *gorm.DB, api string) error {
return err return err
} }
if place := FindPlaceByLabel(l.ID, l.LocLabel, db); place != nil { if place := FindPlaceByLabel(l.S2Token(), l.Label(), db); place != nil {
m.Place = place m.Place = place
} else { } else {
m.Place = &Place{ m.Place = &Place{
ID: l.ID, ID: l.S2Token(),
LocLabel: l.LocLabel, LocLabel: l.Label(),
LocCity: l.LocCity, LocCity: l.City(),
LocState: l.LocState, LocState: l.State(),
LocCountry: l.LocCountry, LocCountry: l.CountryCode(),
LocKeywords: l.KeywordString(),
} }
} }
m.LocName = l.LocName m.LocName = l.Name()
m.LocCategory = l.LocCategory m.LocCategory = l.Category()
m.LocSource = l.LocSource m.LocSource = l.Source()
if err := db.Create(m).Error; err == nil { if err := db.Create(m).Error; err == nil {
return nil return nil
@ -72,11 +73,17 @@ func (m *Location) Find(db *gorm.DB, api string) error {
// Keywords computes keyword based on a Location // Keywords computes keyword based on a Location
func (m *Location) Keywords() (result []string) { func (m *Location) Keywords() (result []string) {
if m.Place == nil {
log.Errorf("location: place for %s is nil - you might have found a bug", m.ID)
return result
}
result = append(result, txt.Keywords(txt.ReplaceSpaces(m.City(), "-"))...) 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.State(), "-"))...)
result = append(result, txt.Keywords(txt.ReplaceSpaces(m.CountryName(), "-"))...) result = append(result, txt.Keywords(txt.ReplaceSpaces(m.CountryName(), "-"))...)
result = append(result, txt.Keywords(m.Category())...) result = append(result, txt.Keywords(m.Category())...)
result = append(result, txt.Keywords(m.Name())...) result = append(result, txt.Keywords(m.Name())...)
result = append(result, txt.Keywords(m.Place.LocKeywords)...)
result = txt.UniqueWords(result) result = txt.UniqueWords(result)

View file

@ -455,8 +455,6 @@ func (m *Photo) SetTakenAt(taken, local time.Time, zone, source string) {
if zone != "" { if zone != "" {
m.TimeZone = zone m.TimeZone = zone
} else {
m.TimeZone = time.UTC.String()
} }
} }

View file

@ -11,10 +11,11 @@ import (
// Place used to associate photos to places // Place used to associate photos to places
type Place struct { type Place struct {
ID string `gorm:"type:varbinary(16);primary_key;auto_increment:false;"` ID string `gorm:"type:varbinary(16);primary_key;auto_increment:false;"`
LocLabel string `gorm:"type:varbinary(512);unique_index;"` LocLabel string `gorm:"type:varbinary(768);unique_index;"`
LocCity string `gorm:"type:varchar(128);"` LocCity string `gorm:"type:varchar(255);"`
LocState string `gorm:"type:varchar(128);"` LocState string `gorm:"type:varchar(255);"`
LocCountry string `gorm:"type:varbinary(2);"` LocCountry string `gorm:"type:varbinary(2);"`
LocKeywords string `gorm:"type:varchar(255);"`
LocNotes string `gorm:"type:text;"` LocNotes string `gorm:"type:text;"`
LocFavorite bool LocFavorite bool
CreatedAt time.Time CreatedAt time.Time
@ -24,11 +25,14 @@ type Place struct {
// UnknownPlace is defined here to use it as a default // UnknownPlace is defined here to use it as a default
var UnknownPlace = Place{ var UnknownPlace = Place{
ID: "zz", ID: "zz",
LocLabel: "Unknown", LocLabel: "Unknown",
LocCity: "Unknown", LocCity: "Unknown",
LocState: "Unknown", LocState: "Unknown",
LocCountry: "zz", LocCountry: "zz",
LocKeywords: "",
LocNotes: "",
LocFavorite: false,
} }
// CreateUnknownPlace initializes default place in the database // CreateUnknownPlace initializes default place in the database

View file

@ -32,6 +32,7 @@ type Location struct {
LocState string LocState string
LocCountry string LocCountry string
LocSource string LocSource string
LocKeywords []string
} }
type LocationSource interface { type LocationSource interface {
@ -42,9 +43,10 @@ type LocationSource interface {
City() string City() string
State() string State() string
Source() string Source() string
Keywords() []string
} }
func NewLocation(id string, name string, category string, label string, city string, state string, country string, source string) *Location { func NewLocation(id, name, category, label, city, state, country, source string, keywords []string) *Location {
result := &Location{ result := &Location{
ID: id, ID: id,
LocName: name, LocName: name,
@ -54,6 +56,7 @@ func NewLocation(id string, name string, category string, label string, city str
LocState: state, LocState: state,
LocCountry: country, LocCountry: country,
LocSource: source, LocSource: source,
LocKeywords: keywords,
} }
return result return result
@ -84,6 +87,7 @@ func (l *Location) QueryPlaces() error {
l.LocCountry = s.CountryCode() l.LocCountry = s.CountryCode()
l.LocCategory = s.Category() l.LocCategory = s.Category()
l.LocLabel = s.Label() l.LocLabel = s.Label()
l.LocKeywords = s.Keywords()
return nil return nil
} }
@ -114,6 +118,7 @@ func (l *Location) Assign(s LocationSource) error {
l.LocCountry = s.CountryCode() l.LocCountry = s.CountryCode()
l.LocCategory = s.Category() l.LocCategory = s.Category()
l.LocLabel = l.label() l.LocLabel = l.label()
l.LocKeywords = s.Keywords()
return nil return nil
} }
@ -147,6 +152,10 @@ func (l *Location) label() string {
return strings.Join(loc[:], ", ") return strings.Join(loc[:], ", ")
} }
func (l Location) S2Token() string {
return l.ID
}
func (l Location) Name() string { func (l Location) Name() string {
return l.LocName return l.LocName
} }
@ -178,3 +187,11 @@ func (l Location) CountryName() string {
func (l Location) Source() string { func (l Location) Source() string {
return l.LocSource return l.LocSource
} }
func (l Location) Keywords() []string {
return l.LocKeywords
}
func (l Location) KeywordString() string {
return strings.Join(l.LocKeywords, ", ")
}

View file

@ -15,18 +15,42 @@ func TestLocation_QueryPlaces(t *testing.T) {
lng := 13.40806264572578 lng := 13.40806264572578
id := s2.Token(lat, lng) id := s2.Token(lat, lng)
l := NewLocation(id, "", "", "", "", "", "", "") l := NewLocation(id, "", "", "", "", "", "", "", []string{})
if err := l.QueryPlaces(); err != nil { if err := l.QueryPlaces(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
assert.Equal(t, "Alt-Berlin", l.LocName)
assert.Equal(t, "Berlin, Germany", l.LocLabel) assert.Equal(t, "Berlin, Germany", l.LocLabel)
}) })
} }
func TestLocation_Assign(t *testing.T) { func TestLocation_Assign(t *testing.T) {
t.Run("Italy", func(t *testing.T) {
id := "47786b2bed37"
o, err := places.FindLocation(id)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, "Comici I", o.Name())
assert.Equal(t, "Trentino-Alto Adige", o.State())
assert.Equal(t, "it", o.CountryCode())
var l Location
if err := l.Assign(o); err != nil {
t.Fatal(err)
}
assert.Equal(t, "Comici I", l.LocName)
assert.Equal(t, "Plan de Gralba, Trentino-Alto Adige, Italy", l.LocLabel)
assert.IsType(t, []string{}, l.Keywords())
assert.Equal(t, "südtirol", l.KeywordString())
})
t.Run("BerlinFernsehturm", func(t *testing.T) { t.Run("BerlinFernsehturm", func(t *testing.T) {
lat := 52.5208 lat := 52.5208
lng := 13.40953 lng := 13.40953
@ -50,6 +74,8 @@ func TestLocation_Assign(t *testing.T) {
assert.Equal(t, "Fernsehturm Berlin", l.LocName) assert.Equal(t, "Fernsehturm Berlin", l.LocName)
assert.Equal(t, "Berlin, Germany", l.LocLabel) assert.Equal(t, "Berlin, Germany", l.LocLabel)
assert.IsType(t, []string{}, l.Keywords())
assert.Equal(t, "", l.KeywordString())
}) })
t.Run("SantaMonica", func(t *testing.T) { t.Run("SantaMonica", func(t *testing.T) {
@ -167,7 +193,7 @@ func TestLocation_Assign(t *testing.T) {
assert.Equal(t, "Indian Ocean", l.LocName) assert.Equal(t, "Indian Ocean", l.LocName)
assert.Equal(t, "", l.LocCategory) assert.Equal(t, "", l.LocCategory)
assert.Equal(t, "", l.LocCity) assert.Equal(t, "Unknown", l.LocCity)
// TODO: Should be zz for international waters, fixed in places server // TODO: Should be zz for international waters, fixed in places server
// assert.Equal(t, "", l.LocCountry) // assert.Equal(t, "", l.LocCountry)
}) })
@ -179,7 +205,7 @@ func TestLocation_Unknown(t *testing.T) {
lng := 0.0 lng := 0.0
id := s2.Token(lat, lng) id := s2.Token(lat, lng)
l := NewLocation(id, "", "", "", "", "", "", "") l := NewLocation(id, "", "", "", "", "", "", "", []string{})
assert.Equal(t, true, l.Unknown()) assert.Equal(t, true, l.Unknown())
}) })
@ -188,7 +214,7 @@ func TestLocation_Unknown(t *testing.T) {
lng := 29.148046666666666 lng := 29.148046666666666
id := s2.Token(lat, lng) id := s2.Token(lat, lng)
l := NewLocation(id, "", "", "", "", "", "", "") l := NewLocation(id, "", "", "", "", "", "", "", []string{})
assert.Equal(t, false, l.Unknown()) assert.Equal(t, false, l.Unknown())
}) })
@ -200,12 +226,12 @@ func TestLocation_place(t *testing.T) {
lng := 0.0 lng := 0.0
id := s2.Token(lat, lng) id := s2.Token(lat, lng)
l := NewLocation(id, "", "", "", "", "", "", "") l := NewLocation(id, "", "", "", "", "", "", "", []string{})
assert.Equal(t, "Unknown", l.label()) assert.Equal(t, "Unknown", l.label())
}) })
t.Run("Nürnberg, Bayern, Germany", func(t *testing.T) { t.Run("Nürnberg, Bayern, Germany", func(t *testing.T) {
l := NewLocation("", "", "", "", "Nürnberg", "Bayern", "de", "") l := NewLocation("", "", "", "", "Nürnberg", "Bayern", "de", "", []string{})
assert.Equal(t, "Unknown", l.label()) assert.Equal(t, "Unknown", l.label())
}) })
@ -213,7 +239,7 @@ func TestLocation_place(t *testing.T) {
func TestLocation_Name(t *testing.T) { func TestLocation_Name(t *testing.T) {
t.Run("Christkindlesmarkt", func(t *testing.T) { t.Run("Christkindlesmarkt", func(t *testing.T) {
l := NewLocation("", "Christkindlesmarkt", "", "", "Nürnberg", "Bayern", "de", "") l := NewLocation("", "Christkindlesmarkt", "", "", "Nürnberg", "Bayern", "de", "", []string{})
assert.Equal(t, "Christkindlesmarkt", l.Name()) assert.Equal(t, "Christkindlesmarkt", l.Name())
}) })
@ -221,7 +247,7 @@ func TestLocation_Name(t *testing.T) {
func TestLocation_City(t *testing.T) { func TestLocation_City(t *testing.T) {
t.Run("Nürnberg", func(t *testing.T) { t.Run("Nürnberg", func(t *testing.T) {
l := NewLocation("", "Christkindlesmarkt", "", "", "Nürnberg", "Bayern", "de", "") l := NewLocation("", "Christkindlesmarkt", "", "", "Nürnberg", "Bayern", "de", "", []string{})
assert.Equal(t, "Nürnberg", l.City()) assert.Equal(t, "Nürnberg", l.City())
}) })
@ -229,7 +255,7 @@ func TestLocation_City(t *testing.T) {
func TestLocation_State(t *testing.T) { func TestLocation_State(t *testing.T) {
t.Run("Bayern", func(t *testing.T) { t.Run("Bayern", func(t *testing.T) {
l := NewLocation("", "Christkindlesmarkt", "", "", "Nürnberg", "Bayern", "de", "") l := NewLocation("", "Christkindlesmarkt", "", "", "Nürnberg", "Bayern", "de", "", []string{})
assert.Equal(t, "Bayern", l.State()) assert.Equal(t, "Bayern", l.State())
}) })
@ -237,7 +263,7 @@ func TestLocation_State(t *testing.T) {
func TestLocation_Category(t *testing.T) { func TestLocation_Category(t *testing.T) {
t.Run("test", func(t *testing.T) { t.Run("test", func(t *testing.T) {
l := NewLocation("", "Christkindlesmarkt", "test", "", "Nürnberg", "Bayern", "de", "") l := NewLocation("", "Christkindlesmarkt", "test", "", "Nürnberg", "Bayern", "de", "", []string{})
assert.Equal(t, "test", l.Category()) assert.Equal(t, "test", l.Category())
}) })
@ -245,7 +271,7 @@ func TestLocation_Category(t *testing.T) {
func TestLocation_Source(t *testing.T) { func TestLocation_Source(t *testing.T) {
t.Run("source", func(t *testing.T) { t.Run("source", func(t *testing.T) {
l := NewLocation("", "Christkindlesmarkt", "", "", "Nürnberg", "Bayern", "de", "source") l := NewLocation("", "Christkindlesmarkt", "", "", "Nürnberg", "Bayern", "de", "source", []string{})
assert.Equal(t, "source", l.Source()) assert.Equal(t, "source", l.Source())
}) })
@ -253,7 +279,7 @@ func TestLocation_Source(t *testing.T) {
func TestLocation_Place(t *testing.T) { func TestLocation_Place(t *testing.T) {
t.Run("test-label", func(t *testing.T) { t.Run("test-label", func(t *testing.T) {
l := NewLocation("", "Christkindlesmarkt", "", "test-label", "Nürnberg", "Bayern", "de", "") l := NewLocation("", "Christkindlesmarkt", "", "test-label", "Nürnberg", "Bayern", "de", "", []string{})
assert.Equal(t, "test-label", l.Label()) assert.Equal(t, "test-label", l.Label())
}) })
@ -261,7 +287,7 @@ func TestLocation_Place(t *testing.T) {
func TestLocation_CountryCode(t *testing.T) { func TestLocation_CountryCode(t *testing.T) {
t.Run("de", func(t *testing.T) { t.Run("de", func(t *testing.T) {
l := NewLocation("", "Christkindlesmarkt", "test", "test-label", "Nürnberg", "Bayern", "de", "") l := NewLocation("", "Christkindlesmarkt", "test", "test-label", "Nürnberg", "Bayern", "de", "", []string{})
assert.Equal(t, "de", l.CountryCode()) assert.Equal(t, "de", l.CountryCode())
}) })
@ -269,14 +295,14 @@ func TestLocation_CountryCode(t *testing.T) {
func TestLocation_CountryName(t *testing.T) { func TestLocation_CountryName(t *testing.T) {
t.Run("Germany", func(t *testing.T) { t.Run("Germany", func(t *testing.T) {
l := NewLocation("", "Christkindlesmarkt", "test", "test-label", "Nürnberg", "Bayern", "de", "") l := NewLocation("", "Christkindlesmarkt", "test", "test-label", "Nürnberg", "Bayern", "de", "", []string{})
assert.Equal(t, "Germany", l.CountryName()) assert.Equal(t, "Germany", l.CountryName())
}) })
} }
func TestLocation_QueryApi(t *testing.T) { func TestLocation_QueryApi(t *testing.T) {
l := NewLocation("3", "Christkindlesmarkt", "test", "test-label", "Nürnberg", "Bayern", "de", "") l := NewLocation("3", "Christkindlesmarkt", "test", "test-label", "Nürnberg", "Bayern", "de", "", []string{})
t.Run("xxx", func(t *testing.T) { t.Run("xxx", func(t *testing.T) {
api := l.QueryApi("xxx") api := l.QueryApi("xxx")
assert.Error(t, api, "maps: reverse lookup disabled") assert.Error(t, api, "maps: reverse lookup disabled")

View file

@ -10,7 +10,6 @@ import (
"github.com/melihmucuk/geocache" "github.com/melihmucuk/geocache"
"github.com/photoprism/photoprism/pkg/s2" "github.com/photoprism/photoprism/pkg/s2"
"github.com/photoprism/photoprism/pkg/txt"
) )
type Location struct { type Location struct {
@ -123,7 +122,7 @@ func (l Location) CountryCode() (result string) {
} }
func (l Location) Keywords() (result []string) { func (l Location) Keywords() (result []string) {
return txt.Keywords(l.LocDisplayName) return result
} }
func (l Location) Source() string { func (l Location) Source() string {

View file

@ -26,7 +26,7 @@ type Location struct {
var ReverseLookupURL = "https://places.photoprism.org/v1/location/%s" var ReverseLookupURL = "https://places.photoprism.org/v1/location/%s"
var client = &http.Client{Timeout: 30 * time.Second} // TODO: Change timeout if needed var client = &http.Client{Timeout: 30 * time.Second} // TODO: Change timeout if needed
func NewLocation(id string, lat float64, lng float64, name string, category string, place Place, cached bool) *Location { func NewLocation(id string, lat, lng float64, name, category string, place Place, cached bool) *Location {
result := &Location{ result := &Location{
ID: id, ID: id,
LocLat: lat, LocLat: lat,
@ -68,7 +68,15 @@ func FindLocation(id string) (result Location, err error) {
return result, err return result, err
} }
r, err := client.Do(req) var r *http.Response
for i := 0; i < 3; i++ {
r, err = client.Do(req)
if err == nil {
break
}
}
if err != nil { if err != nil {
log.Errorf("places: %s", err.Error()) log.Errorf("places: %s", err.Error())
@ -135,7 +143,7 @@ func (l Location) Longitude() (result float64) {
} }
func (l Location) Keywords() (result []string) { func (l Location) Keywords() (result []string) {
return txt.Keywords(l.Label()) return txt.UniqueKeywords(l.Place.LocKeywords)
} }
func (l Location) Source() string { func (l Location) Source() string {

View file

@ -20,7 +20,6 @@ func TestFindLocation(t *testing.T) {
} }
assert.False(t, l.Cached) assert.False(t, l.Cached)
assert.Equal(t, "Alt-Berlin", l.Name())
assert.Equal(t, "Berlin", l.City()) assert.Equal(t, "Berlin", l.City())
assert.Equal(t, "de", l.CountryCode()) assert.Equal(t, "de", l.CountryCode())
}) })
@ -35,7 +34,7 @@ func TestFindLocation(t *testing.T) {
t.Log(l) t.Log(l)
}) })
t.Run("cached true", func(t *testing.T) { t.Run("cached true", func(t *testing.T) {
var p = NewPlace("1", "", "", "", "de") var p = NewPlace("1", "", "", "", "de", "")
location := NewLocation("54", 52.51961810676184, 13.40806264572578, "TestLocation", "test", p, true) location := NewLocation("54", 52.51961810676184, 13.40806264572578, "TestLocation", "test", p, true)
l, err := FindLocation(location.ID) l, err := FindLocation(location.ID)
if err != nil { if err != nil {
@ -52,7 +51,7 @@ func TestFindLocation(t *testing.T) {
} }
func TestLocationGetters(t *testing.T) { func TestLocationGetters(t *testing.T) {
var p = NewPlace("1", "testLabel", "berlin", "berlin", "de") var p = NewPlace("1", "testLabel", "berlin", "berlin", "de", "foobar")
location := NewLocation("54", 52.51961810676184, 13.40806264572578, "TestLocation", "test", p, true) location := NewLocation("54", 52.51961810676184, 13.40806264572578, "TestLocation", "test", p, true)
t.Run("wrong id", func(t *testing.T) { t.Run("wrong id", func(t *testing.T) {
assert.Equal(t, "54", location.CellID()) assert.Equal(t, "54", location.CellID())
@ -65,7 +64,7 @@ func TestLocationGetters(t *testing.T) {
assert.Equal(t, 52.51961810676184, location.Latitude()) assert.Equal(t, 52.51961810676184, location.Latitude())
assert.Equal(t, 13.40806264572578, location.Longitude()) assert.Equal(t, 13.40806264572578, location.Longitude())
assert.Equal(t, "places", location.Source()) assert.Equal(t, "places", location.Source())
assert.Equal(t, []string{"testlabel"}, location.Keywords()) assert.Equal(t, []string{"foobar"}, location.Keywords())
}) })
} }

View file

@ -2,20 +2,22 @@ package places
// Place // Place
type Place struct { type Place struct {
PlaceID string `json:"id"` PlaceID string `json:"id"`
LocLabel string `json:"label"` LocLabel string `json:"label"`
LocCity string `json:"city"` LocCity string `json:"city"`
LocState string `json:"state"` LocState string `json:"state"`
LocCountry string `json:"country"` LocCountry string `json:"country"`
LocKeywords string `json:"keywords"`
} }
func NewPlace(id string, label string, city string, state string, country string) Place { func NewPlace(id, label, city, state, country, keywords string) Place {
result := Place{ result := Place{
PlaceID: id, PlaceID: id,
LocLabel: label, LocLabel: label,
LocCity: city, LocCity: city,
LocState: state, LocState: state,
LocCountry: country, LocCountry: country,
LocKeywords: keywords,
} }
return result return result

View file

@ -201,7 +201,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
} }
if photo.TakenAt.IsZero() || photo.TakenAtLocal.IsZero() { if photo.TakenAt.IsZero() || photo.TakenAtLocal.IsZero() {
photo.SetTakenAt(m.DateCreated(), m.DateCreated(), time.UTC.String(), entity.SrcAuto) photo.SetTakenAt(m.DateCreated(), m.DateCreated(), "", entity.SrcAuto)
} }
if fileChanged || o.UpdateKeywords || o.UpdateLocation || o.UpdateTitle || photo.NoTitle() { if fileChanged || o.UpdateKeywords || o.UpdateLocation || o.UpdateTitle || photo.NoTitle() {

View file

@ -43,12 +43,12 @@ func TestFileType_Find(t *testing.T) {
result := TypeJpeg.Find("testdata/test (2).xmp", true) result := TypeJpeg.Find("testdata/test (2).xmp", true)
assert.Equal(t, "testdata/test.jpg", result) assert.Equal(t, "testdata/test.jpg", result)
}) })
t.Run("prefixUpper", func(t *testing.T) { t.Run("name upper", func(t *testing.T) {
result := TypeJpeg.Find("testdata/catyellow.xmp", true) result := TypeJpeg.Find("testdata/CATYELLOW.xmp", true)
assert.Equal(t, "testdata/CATYELLOW.jpg", result) assert.Equal(t, "testdata/CATYELLOW.jpg", result)
}) })
t.Run("prefixLower", func(t *testing.T) { t.Run("name lower", func(t *testing.T) {
result := TypeJpeg.Find("testdata/CHAMELEON_LIME.xmp", true) result := TypeJpeg.Find("testdata/chameleon_lime.xmp", true)
assert.Equal(t, "testdata/chameleon_lime.jpg", result) assert.Equal(t, "testdata/chameleon_lime.jpg", result)
}) })
} }

View file

@ -12,6 +12,8 @@ photos
import import
export export
abc abc
val
tmp
xyz xyz
jpg jpg
jpeg jpeg

View file

@ -17,6 +17,8 @@ var Stopwords = map[string]bool{
"import": true, "import": true,
"export": true, "export": true,
"abc": true, "abc": true,
"val": true,
"tmp": true,
"xyz": true, "xyz": true,
"jpg": true, "jpg": true,
"jpeg": true, "jpeg": true,