mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 08:44:04 +01:00
Config: Add option to skip all RAW images when indexing #2227
This commit is contained in:
@@ -1,8 +1,7 @@
|
|||||||
version: '3.5'
|
version: '3.5'
|
||||||
|
|
||||||
## Continuous Integration (CI) Test Environment
|
|
||||||
services:
|
services:
|
||||||
## App Dev Container
|
## Continuous Integration (CI) Environment
|
||||||
## Docs: https://docs.photoprism.app/developer-guide/
|
## Docs: https://docs.photoprism.app/developer-guide/
|
||||||
photoprism:
|
photoprism:
|
||||||
build: .
|
build: .
|
||||||
@@ -43,7 +42,7 @@ services:
|
|||||||
PHOTOPRISM_ORIGINALS_PATH: "/go/src/github.com/photoprism/photoprism/storage/originals"
|
PHOTOPRISM_ORIGINALS_PATH: "/go/src/github.com/photoprism/photoprism/storage/originals"
|
||||||
PHOTOPRISM_IMPORT_PATH: "/go/src/github.com/photoprism/photoprism/storage/import"
|
PHOTOPRISM_IMPORT_PATH: "/go/src/github.com/photoprism/photoprism/storage/import"
|
||||||
PHOTOPRISM_DISABLE_CHOWN: "true" # disables storage permission updates on startup
|
PHOTOPRISM_DISABLE_CHOWN: "true" # disables storage permission updates on startup
|
||||||
PHOTOPRISM_DISABLE_BACKUPS: "false" # disables creating YAML metadata files
|
PHOTOPRISM_DISABLE_BACKUPS: "false" # disables backing up albums and photo metadata to YAML files
|
||||||
PHOTOPRISM_DISABLE_WEBDAV: "false" # disables built-in WebDAV server
|
PHOTOPRISM_DISABLE_WEBDAV: "false" # disables built-in WebDAV server
|
||||||
PHOTOPRISM_DISABLE_SETTINGS: "false" # disables settings UI and API
|
PHOTOPRISM_DISABLE_SETTINGS: "false" # disables settings UI and API
|
||||||
PHOTOPRISM_DISABLE_PLACES: "false" # disables reverse geocoding and maps
|
PHOTOPRISM_DISABLE_PLACES: "false" # disables reverse geocoding and maps
|
||||||
@@ -51,7 +50,7 @@ services:
|
|||||||
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
|
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
|
||||||
PHOTOPRISM_DETECT_NSFW: "false" # flag photos as private that MAY be offensive (requires TensorFlow)
|
PHOTOPRISM_DETECT_NSFW: "false" # flag photos as private that MAY be offensive (requires TensorFlow)
|
||||||
PHOTOPRISM_UPLOAD_NSFW: "false" # allow uploads that may be offensive
|
PHOTOPRISM_UPLOAD_NSFW: "false" # allow uploads that may be offensive
|
||||||
PHOTOPRISM_RAW_PRESETS: "false" # enables RAW file converter presets (may reduce performance)
|
PHOTOPRISM_RAW_PRESETS: "false" # enables applying user presets when converting RAW files (reduces performance)
|
||||||
PHOTOPRISM_THUMB_FILTER: "lanczos" # resample filter, best to worst: blackman, lanczos, cubic, linear
|
PHOTOPRISM_THUMB_FILTER: "lanczos" # resample filter, best to worst: blackman, lanczos, cubic, linear
|
||||||
PHOTOPRISM_THUMB_UNCACHED: "true" # enables on-demand thumbnail rendering (high memory and cpu usage)
|
PHOTOPRISM_THUMB_UNCACHED: "true" # enables on-demand thumbnail rendering (high memory and cpu usage)
|
||||||
PHOTOPRISM_THUMB_SIZE: 2048 # pre-rendered thumbnail size limit (default 2048, min 720, max 7680)
|
PHOTOPRISM_THUMB_SIZE: 2048 # pre-rendered thumbnail size limit (default 2048, min 720, max 7680)
|
||||||
@@ -158,6 +157,10 @@ services:
|
|||||||
MARIADB_PASSWORD: "photoprism"
|
MARIADB_PASSWORD: "photoprism"
|
||||||
MARIADB_ROOT_PASSWORD: "photoprism"
|
MARIADB_ROOT_PASSWORD: "photoprism"
|
||||||
|
|
||||||
|
## Dummy OpenID Connect Provider
|
||||||
|
dummy-oidc:
|
||||||
|
image: photoprism/dummy-oidc:220405
|
||||||
|
|
||||||
## Dummy WebDAV Server
|
## Dummy WebDAV Server
|
||||||
dummy-webdav:
|
dummy-webdav:
|
||||||
image: photoprism/dummy-webdav:20211109
|
image: photoprism/dummy-webdav:20211109
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
version: '3.5'
|
version: '3.5'
|
||||||
|
|
||||||
## Latest Stable Release for QA
|
|
||||||
services:
|
services:
|
||||||
## App Server
|
## Stable Release
|
||||||
## Docs: https://docs.photoprism.org/
|
## Docs: https://docs.photoprism.org/
|
||||||
photoprism-latest:
|
photoprism-latest:
|
||||||
image: photoprism/photoprism:latest
|
image: photoprism/photoprism:latest
|
||||||
@@ -43,7 +42,7 @@ services:
|
|||||||
PHOTOPRISM_DATABASE_USER: "photoprism_latest"
|
PHOTOPRISM_DATABASE_USER: "photoprism_latest"
|
||||||
PHOTOPRISM_DATABASE_PASSWORD: "photoprism_latest"
|
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" # disables creating YAML metadata files
|
PHOTOPRISM_DISABLE_BACKUPS: "false" # disables backing up albums and photo metadata to YAML files
|
||||||
PHOTOPRISM_DISABLE_WEBDAV: "false" # disables built-in WebDAV server
|
PHOTOPRISM_DISABLE_WEBDAV: "false" # disables built-in WebDAV server
|
||||||
PHOTOPRISM_DISABLE_SETTINGS: "false" # disables settings UI and API
|
PHOTOPRISM_DISABLE_SETTINGS: "false" # disables settings UI and API
|
||||||
PHOTOPRISM_DISABLE_PLACES: "false" # disables reverse geocoding and maps
|
PHOTOPRISM_DISABLE_PLACES: "false" # disables reverse geocoding and maps
|
||||||
@@ -51,7 +50,7 @@ services:
|
|||||||
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
|
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
|
||||||
PHOTOPRISM_DETECT_NSFW: "false" # flag photos as private that MAY be offensive (requires TensorFlow)
|
PHOTOPRISM_DETECT_NSFW: "false" # flag photos as private that MAY be offensive (requires TensorFlow)
|
||||||
PHOTOPRISM_UPLOAD_NSFW: "false" # allows uploads that may be offensive
|
PHOTOPRISM_UPLOAD_NSFW: "false" # allows uploads that may be offensive
|
||||||
PHOTOPRISM_RAW_PRESETS: "false" # enables RAW file converter presets (may reduce performance)
|
PHOTOPRISM_RAW_PRESETS: "false" # enables applying user presets when converting RAW files (reduces performance)
|
||||||
PHOTOPRISM_THUMB_FILTER: "lanczos" # resample filter, best to worst: blackman, lanczos, cubic, linear
|
PHOTOPRISM_THUMB_FILTER: "lanczos" # resample filter, best to worst: blackman, lanczos, cubic, linear
|
||||||
PHOTOPRISM_THUMB_UNCACHED: "true" # enables on-demand thumbnail rendering (high memory and cpu usage)
|
PHOTOPRISM_THUMB_UNCACHED: "true" # enables on-demand thumbnail rendering (high memory and cpu usage)
|
||||||
PHOTOPRISM_THUMB_SIZE: 2048 # pre-rendered thumbnail size limit (default 2048, min 720, max 7680)
|
PHOTOPRISM_THUMB_SIZE: 2048 # pre-rendered thumbnail size limit (default 2048, min 720, max 7680)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
version: '3.5'
|
version: '3.5'
|
||||||
|
|
||||||
## MariaDB Server Versions for Development & Testing
|
# Additional MariaDB and MySQL versions for testing compatibility
|
||||||
|
|
||||||
services:
|
services:
|
||||||
## MariaDB 10.8 Database Server
|
## MariaDB 10.8 Database Server
|
||||||
## Docs: https://mariadb.com/kb/en/release-notes-mariadb-108-series/
|
## Docs: https://mariadb.com/kb/en/release-notes-mariadb-108-series/
|
||||||
|
|||||||
@@ -4,12 +4,8 @@ version: '3.5'
|
|||||||
# The current Gorm version does NOT support compatible general data types:
|
# The current Gorm version does NOT support compatible general data types:
|
||||||
# https://github.com/photoprism/photoprism/issues/47
|
# https://github.com/photoprism/photoprism/issues/47
|
||||||
|
|
||||||
## Development Environment with
|
|
||||||
## - App Dev Container
|
|
||||||
## - PostgreSQL Database Server
|
|
||||||
## - and Dummy Services
|
|
||||||
services:
|
services:
|
||||||
## App Dev Container
|
## PhotoPrism Development Environment (PostgresSQL)
|
||||||
## Docs: https://docs.photoprism.app/developer-guide/
|
## Docs: https://docs.photoprism.app/developer-guide/
|
||||||
photoprism:
|
photoprism:
|
||||||
build: .
|
build: .
|
||||||
@@ -54,7 +50,7 @@ services:
|
|||||||
PHOTOPRISM_ORIGINALS_PATH: "/go/src/github.com/photoprism/photoprism/storage/originals"
|
PHOTOPRISM_ORIGINALS_PATH: "/go/src/github.com/photoprism/photoprism/storage/originals"
|
||||||
PHOTOPRISM_IMPORT_PATH: "/go/src/github.com/photoprism/photoprism/storage/import"
|
PHOTOPRISM_IMPORT_PATH: "/go/src/github.com/photoprism/photoprism/storage/import"
|
||||||
PHOTOPRISM_DISABLE_CHOWN: "false" # disables storage permission updates on startup
|
PHOTOPRISM_DISABLE_CHOWN: "false" # disables storage permission updates on startup
|
||||||
PHOTOPRISM_DISABLE_BACKUPS: "false" # disables creating YAML metadata files
|
PHOTOPRISM_DISABLE_BACKUPS: "false" # disables backing up albums and photo metadata to YAML files
|
||||||
PHOTOPRISM_DISABLE_WEBDAV: "false" # disables built-in WebDAV server
|
PHOTOPRISM_DISABLE_WEBDAV: "false" # disables built-in WebDAV server
|
||||||
PHOTOPRISM_DISABLE_SETTINGS: "false" # disables settings UI and API
|
PHOTOPRISM_DISABLE_SETTINGS: "false" # disables settings UI and API
|
||||||
PHOTOPRISM_DISABLE_PLACES: "false" # disables reverse geocoding and maps
|
PHOTOPRISM_DISABLE_PLACES: "false" # disables reverse geocoding and maps
|
||||||
@@ -62,7 +58,7 @@ services:
|
|||||||
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
|
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
|
||||||
PHOTOPRISM_DETECT_NSFW: "false" # flag photos as private that MAY be offensive (requires TensorFlow)
|
PHOTOPRISM_DETECT_NSFW: "false" # flag photos as private that MAY be offensive (requires TensorFlow)
|
||||||
PHOTOPRISM_UPLOAD_NSFW: "false" # allows uploads that may be offensive
|
PHOTOPRISM_UPLOAD_NSFW: "false" # allows uploads that may be offensive
|
||||||
PHOTOPRISM_RAW_PRESETS: "false" # enables RAW file converter presets (may reduce performance)
|
PHOTOPRISM_RAW_PRESETS: "false" # enables applying user presets when converting RAW files (reduces performance)
|
||||||
PHOTOPRISM_THUMB_FILTER: "lanczos" # resample filter, best to worst: blackman, lanczos, cubic, linear
|
PHOTOPRISM_THUMB_FILTER: "lanczos" # resample filter, best to worst: blackman, lanczos, cubic, linear
|
||||||
PHOTOPRISM_THUMB_UNCACHED: "true" # enables on-demand thumbnail rendering (high memory and cpu usage)
|
PHOTOPRISM_THUMB_UNCACHED: "true" # enables on-demand thumbnail rendering (high memory and cpu usage)
|
||||||
PHOTOPRISM_THUMB_SIZE: 2048 # pre-rendered thumbnail size limit (default 2048, min 720, max 7680)
|
PHOTOPRISM_THUMB_SIZE: 2048 # pre-rendered thumbnail size limit (default 2048, min 720, max 7680)
|
||||||
|
|||||||
@@ -1,32 +1,7 @@
|
|||||||
version: '3.5'
|
version: '3.5'
|
||||||
|
|
||||||
## Development Environment with
|
|
||||||
## - HTTPS Reverse Proxy
|
|
||||||
## - App Dev Container
|
|
||||||
## - MariaDB Database Server
|
|
||||||
## - Keycloak OpenID Connect Provider
|
|
||||||
## - and Dummy Services
|
|
||||||
services:
|
services:
|
||||||
## Traefik HTTPS Reverse Proxy
|
## PhotoPrism Development Environment (MariaDB)
|
||||||
## Includes Let's Encrypt certs for local dev domain "localssl.dev" (all records point to 127.0.0.1)
|
|
||||||
## Docs: https://doc.traefik.io/traefik/
|
|
||||||
traefik:
|
|
||||||
image: photoprism/traefik:220405
|
|
||||||
ports:
|
|
||||||
# - "80:80" # HTTP (redirects to HTTPS)
|
|
||||||
- "443:443" # HTTPS (required)
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.http.routers.traefik.rule=Host(`traefik.localssl.dev`)"
|
|
||||||
- "traefik.http.routers.traefik.tls.domains[0].main=localssl.dev"
|
|
||||||
- "traefik.http.routers.traefik.tls.domains[0].sans=*.localssl.dev"
|
|
||||||
- "traefik.http.routers.traefik.tls=true"
|
|
||||||
- "traefik.http.routers.traefik.tls.certresolver=myresolver"
|
|
||||||
- "traefik.http.routers.traefik.service=api@internal"
|
|
||||||
volumes:
|
|
||||||
- "/var/run/docker.sock:/var/run/docker.sock" # enables Traefik to watch services
|
|
||||||
|
|
||||||
## App Build Environment
|
|
||||||
## Docs: https://docs.photoprism.org/developer-guide/
|
## Docs: https://docs.photoprism.org/developer-guide/
|
||||||
photoprism:
|
photoprism:
|
||||||
build: .
|
build: .
|
||||||
@@ -85,15 +60,16 @@ services:
|
|||||||
PHOTOPRISM_ORIGINALS_PATH: "/go/src/github.com/photoprism/photoprism/storage/originals"
|
PHOTOPRISM_ORIGINALS_PATH: "/go/src/github.com/photoprism/photoprism/storage/originals"
|
||||||
PHOTOPRISM_IMPORT_PATH: "/go/src/github.com/photoprism/photoprism/storage/import"
|
PHOTOPRISM_IMPORT_PATH: "/go/src/github.com/photoprism/photoprism/storage/import"
|
||||||
PHOTOPRISM_DISABLE_CHOWN: "false" # disables storage permission updates on startup
|
PHOTOPRISM_DISABLE_CHOWN: "false" # disables storage permission updates on startup
|
||||||
PHOTOPRISM_DISABLE_BACKUPS: "false" # disables creating YAML metadata files
|
PHOTOPRISM_DISABLE_BACKUPS: "false" # disables backing up albums and photo metadata to YAML files
|
||||||
PHOTOPRISM_DISABLE_WEBDAV: "false" # disables built-in WebDAV server
|
PHOTOPRISM_DISABLE_WEBDAV: "false" # disables built-in WebDAV server
|
||||||
PHOTOPRISM_DISABLE_SETTINGS: "false" # disables settings UI and API
|
PHOTOPRISM_DISABLE_SETTINGS: "false" # disables settings UI and API
|
||||||
PHOTOPRISM_DISABLE_PLACES: "false" # disables reverse geocoding and maps
|
PHOTOPRISM_DISABLE_PLACES: "false" # disables reverse geocoding and maps
|
||||||
PHOTOPRISM_DISABLE_EXIFTOOL: "false" # disables creating JSON metadata sidecar files with ExifTool
|
PHOTOPRISM_DISABLE_EXIFTOOL: "false" # disables creating JSON metadata sidecar files with ExifTool
|
||||||
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
|
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
|
||||||
|
PHOTOPRISM_DISABLE_RAW: "false" # disables indexing and conversion of RAW files
|
||||||
|
PHOTOPRISM_RAW_PRESETS: "false" # enables applying user presets when converting RAW files (reduces performance)
|
||||||
PHOTOPRISM_DETECT_NSFW: "false" # flag photos as private that MAY be offensive (requires TensorFlow)
|
PHOTOPRISM_DETECT_NSFW: "false" # flag photos as private that MAY be offensive (requires TensorFlow)
|
||||||
PHOTOPRISM_UPLOAD_NSFW: "false" # allows uploads that may be offensive
|
PHOTOPRISM_UPLOAD_NSFW: "false" # allows uploads that may be offensive
|
||||||
PHOTOPRISM_RAW_PRESETS: "false" # enables RAW file converter presets (may reduce performance)
|
|
||||||
PHOTOPRISM_THUMB_FILTER: "lanczos" # resample filter, best to worst: blackman, lanczos, cubic, linear
|
PHOTOPRISM_THUMB_FILTER: "lanczos" # resample filter, best to worst: blackman, lanczos, cubic, linear
|
||||||
PHOTOPRISM_THUMB_UNCACHED: "true" # enables on-demand thumbnail rendering (high memory and cpu usage)
|
PHOTOPRISM_THUMB_UNCACHED: "true" # enables on-demand thumbnail rendering (high memory and cpu usage)
|
||||||
PHOTOPRISM_THUMB_SIZE: 2048 # pre-rendered thumbnail size limit (default 2048, min 720, max 7680)
|
PHOTOPRISM_THUMB_SIZE: 2048 # pre-rendered thumbnail size limit (default 2048, min 720, max 7680)
|
||||||
@@ -146,9 +122,28 @@ services:
|
|||||||
MARIADB_PASSWORD: "photoprism"
|
MARIADB_PASSWORD: "photoprism"
|
||||||
MARIADB_ROOT_PASSWORD: "photoprism"
|
MARIADB_ROOT_PASSWORD: "photoprism"
|
||||||
|
|
||||||
|
## HTTPS Reverse Proxy
|
||||||
|
## includes "*.localssl.dev" SSL certificate for local development
|
||||||
|
## Docs: https://doc.traefik.io/traefik/
|
||||||
|
traefik:
|
||||||
|
image: photoprism/traefik:220405
|
||||||
|
ports:
|
||||||
|
# - "80:80" # HTTP (redirects to HTTPS)
|
||||||
|
- "443:443" # HTTPS (required)
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.traefik.rule=Host(`traefik.localssl.dev`)"
|
||||||
|
- "traefik.http.routers.traefik.tls.domains[0].main=localssl.dev"
|
||||||
|
- "traefik.http.routers.traefik.tls.domains[0].sans=*.localssl.dev"
|
||||||
|
- "traefik.http.routers.traefik.tls=true"
|
||||||
|
- "traefik.http.routers.traefik.tls.certresolver=myresolver"
|
||||||
|
- "traefik.http.routers.traefik.service=api@internal"
|
||||||
|
volumes:
|
||||||
|
- "/var/run/docker.sock:/var/run/docker.sock" # enables Traefik to watch services
|
||||||
|
|
||||||
## Keycloak OpenID Connect Provider
|
## Keycloak OpenID Connect Provider
|
||||||
## Admin Account: admin / photoprism
|
## Login: user / photoprism
|
||||||
## User Account: user / photoprism
|
## Admin: admin / photoprism
|
||||||
keycloak:
|
keycloak:
|
||||||
image: quay.io/keycloak/keycloak:17.0.1
|
image: quay.io/keycloak/keycloak:17.0.1
|
||||||
command: "start-dev" # development mode, do not use this in production!
|
command: "start-dev" # development mode, do not use this in production!
|
||||||
@@ -174,6 +169,18 @@ services:
|
|||||||
KC_DB_USERNAME: "keycloak"
|
KC_DB_USERNAME: "keycloak"
|
||||||
KC_DB_PASSWORD: "keycloak"
|
KC_DB_PASSWORD: "keycloak"
|
||||||
|
|
||||||
|
## Dummy OpenID Connect Provider
|
||||||
|
dummy-oidc:
|
||||||
|
image: photoprism/dummy-oidc:220405
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.services.dummy-oidc.loadbalancer.server.port=9998"
|
||||||
|
- "traefik.http.routers.dummy-oidc.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.dummy-oidc.rule=Host(`dummy-oidc.localssl.dev`)"
|
||||||
|
- "traefik.http.routers.dummy-oidc.tls.domains[0].main=localssl.dev"
|
||||||
|
- "traefik.http.routers.dummy-oidc.tls.domains[0].sans=*.localssl.dev"
|
||||||
|
- "traefik.http.routers.dummy-oidc.tls=true"
|
||||||
|
|
||||||
## Dummy WebDAV Server
|
## Dummy WebDAV Server
|
||||||
dummy-webdav:
|
dummy-webdav:
|
||||||
image: photoprism/dummy-webdav:220405
|
image: photoprism/dummy-webdav:220405
|
||||||
@@ -189,18 +196,6 @@ services:
|
|||||||
- "traefik.http.routers.dummy-webdav.tls.domains[0].sans=*.localssl.dev"
|
- "traefik.http.routers.dummy-webdav.tls.domains[0].sans=*.localssl.dev"
|
||||||
- "traefik.http.routers.dummy-webdav.tls=true"
|
- "traefik.http.routers.dummy-webdav.tls=true"
|
||||||
|
|
||||||
## Dummy OpenID Connect Server
|
|
||||||
dummy-oidc:
|
|
||||||
image: photoprism/dummy-oidc:220405
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.http.services.dummy-oidc.loadbalancer.server.port=9998"
|
|
||||||
- "traefik.http.routers.dummy-oidc.entrypoints=websecure"
|
|
||||||
- "traefik.http.routers.dummy-oidc.rule=Host(`dummy-oidc.localssl.dev`)"
|
|
||||||
- "traefik.http.routers.dummy-oidc.tls.domains[0].main=localssl.dev"
|
|
||||||
- "traefik.http.routers.dummy-oidc.tls.domains[0].sans=*.localssl.dev"
|
|
||||||
- "traefik.http.routers.dummy-oidc.tls=true"
|
|
||||||
|
|
||||||
## Create named volume for Go module cache
|
## Create named volume for Go module cache
|
||||||
volumes:
|
volumes:
|
||||||
go-mod:
|
go-mod:
|
||||||
|
|||||||
@@ -75,8 +75,9 @@ services:
|
|||||||
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
|
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
|
||||||
PHOTOPRISM_DISABLE_FACES: "false" # disables facial recognition
|
PHOTOPRISM_DISABLE_FACES: "false" # disables facial recognition
|
||||||
PHOTOPRISM_DISABLE_CLASSIFICATION: "false" # disables image classification
|
PHOTOPRISM_DISABLE_CLASSIFICATION: "false" # disables image classification
|
||||||
|
PHOTOPRISM_DISABLE_RAW: "false" # disables indexing and conversion of RAW files
|
||||||
|
PHOTOPRISM_RAW_PRESETS: "false" # enables applying user presets when converting RAW files (reduces performance)
|
||||||
PHOTOPRISM_JPEG_QUALITY: 85 # image quality, a higher value reduces compression (25-100)
|
PHOTOPRISM_JPEG_QUALITY: 85 # image quality, a higher value reduces compression (25-100)
|
||||||
PHOTOPRISM_RAW_PRESETS: "false" # enables RAW file converter presets (may reduce performance)
|
|
||||||
PHOTOPRISM_DETECT_NSFW: "false" # flag photos as private that MAY be offensive
|
PHOTOPRISM_DETECT_NSFW: "false" # flag photos as private that MAY be offensive
|
||||||
PHOTOPRISM_UPLOAD_NSFW: "true" # allow uploads that MAY be offensive
|
PHOTOPRISM_UPLOAD_NSFW: "true" # allow uploads that MAY be offensive
|
||||||
# PHOTOPRISM_DATABASE_DRIVER: "sqlite" # SQLite is an embedded database that doesn't require a server
|
# PHOTOPRISM_DATABASE_DRIVER: "sqlite" # SQLite is an embedded database that doesn't require a server
|
||||||
|
|||||||
@@ -68,8 +68,11 @@ services:
|
|||||||
PHOTOPRISM_DISABLE_WEBDAV: "false" # disables built-in WebDAV server
|
PHOTOPRISM_DISABLE_WEBDAV: "false" # disables built-in WebDAV server
|
||||||
PHOTOPRISM_DISABLE_SETTINGS: "false" # disables Settings in Web UI
|
PHOTOPRISM_DISABLE_SETTINGS: "false" # disables Settings in Web UI
|
||||||
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
|
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
|
||||||
PHOTOPRISM_DISABLE_FACES: "false" # disables facial recognition
|
PHOTOPRISM_DISABLE_FACES: "true" # disables facial recognition
|
||||||
PHOTOPRISM_DISABLE_CLASSIFICATION: "false" # disables image classification
|
PHOTOPRISM_DISABLE_CLASSIFICATION: "false" # disables image classification
|
||||||
|
PHOTOPRISM_DISABLE_RAW: "true" # disables indexing and conversion of RAW files
|
||||||
|
PHOTOPRISM_RAW_PRESETS: "false" # enables applying user presets when converting RAW files (reduces performance)
|
||||||
|
PHOTOPRISM_JPEG_QUALITY: 85 # image quality, a higher value reduces compression (25-100)
|
||||||
PHOTOPRISM_DETECT_NSFW: "false" # flag photos as private that MAY be offensive
|
PHOTOPRISM_DETECT_NSFW: "false" # flag photos as private that MAY be offensive
|
||||||
PHOTOPRISM_UPLOAD_NSFW: "true" # allows uploads that MAY be offensive
|
PHOTOPRISM_UPLOAD_NSFW: "true" # allows uploads that MAY be offensive
|
||||||
# PHOTOPRISM_DATABASE_DRIVER: "sqlite" # SQLite is an embedded database that doesn't require a server
|
# PHOTOPRISM_DATABASE_DRIVER: "sqlite" # SQLite is an embedded database that doesn't require a server
|
||||||
|
|||||||
@@ -146,8 +146,9 @@ services:
|
|||||||
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
|
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
|
||||||
PHOTOPRISM_DISABLE_FACES: "false" # disables facial recognition
|
PHOTOPRISM_DISABLE_FACES: "false" # disables facial recognition
|
||||||
PHOTOPRISM_DISABLE_CLASSIFICATION: "false" # disables image classification
|
PHOTOPRISM_DISABLE_CLASSIFICATION: "false" # disables image classification
|
||||||
|
PHOTOPRISM_DISABLE_RAW: "false" # disables indexing and conversion of RAW files
|
||||||
|
PHOTOPRISM_RAW_PRESETS: "false" # enables applying user presets when converting RAW files (reduces performance)
|
||||||
PHOTOPRISM_JPEG_QUALITY: 85 # image quality, a higher value reduces compression (25-100)
|
PHOTOPRISM_JPEG_QUALITY: 85 # image quality, a higher value reduces compression (25-100)
|
||||||
PHOTOPRISM_RAW_PRESETS: "false" # enables RAW file converter presets (may reduce performance)
|
|
||||||
PHOTOPRISM_DETECT_NSFW: "false" # flag photos as private that MAY be offensive (requires TensorFlow)
|
PHOTOPRISM_DETECT_NSFW: "false" # flag photos as private that MAY be offensive (requires TensorFlow)
|
||||||
PHOTOPRISM_UPLOAD_NSFW: "true" # allows uploads that MAY be offensive
|
PHOTOPRISM_UPLOAD_NSFW: "true" # allows uploads that MAY be offensive
|
||||||
PHOTOPRISM_DATABASE_DRIVER: "mysql" # use MariaDB 10.5+ or MySQL 8+ instead of SQLite for improved performance
|
PHOTOPRISM_DATABASE_DRIVER: "mysql" # use MariaDB 10.5+ or MySQL 8+ instead of SQLite for improved performance
|
||||||
|
|||||||
@@ -66,8 +66,9 @@ services:
|
|||||||
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
|
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
|
||||||
PHOTOPRISM_DISABLE_FACES: "false" # disables facial recognition
|
PHOTOPRISM_DISABLE_FACES: "false" # disables facial recognition
|
||||||
PHOTOPRISM_DISABLE_CLASSIFICATION: "false" # disables image classification
|
PHOTOPRISM_DISABLE_CLASSIFICATION: "false" # disables image classification
|
||||||
|
PHOTOPRISM_DISABLE_RAW: "false" # disables indexing and conversion of RAW files
|
||||||
|
PHOTOPRISM_RAW_PRESETS: "false" # enables applying user presets when converting RAW files (reduces performance)
|
||||||
PHOTOPRISM_JPEG_QUALITY: 85 # image quality, a higher value reduces compression (25-100)
|
PHOTOPRISM_JPEG_QUALITY: 85 # image quality, a higher value reduces compression (25-100)
|
||||||
PHOTOPRISM_RAW_PRESETS: "false" # enables RAW file converter presets (may reduce performance)
|
|
||||||
PHOTOPRISM_DETECT_NSFW: "false" # flag photos as private that MAY be offensive (requires TensorFlow)
|
PHOTOPRISM_DETECT_NSFW: "false" # flag photos as private that MAY be offensive (requires TensorFlow)
|
||||||
PHOTOPRISM_UPLOAD_NSFW: "true" # allows uploads that MAY be offensive
|
PHOTOPRISM_UPLOAD_NSFW: "true" # allows uploads that MAY be offensive
|
||||||
# PHOTOPRISM_DATABASE_DRIVER: "sqlite" # SQLite is an embedded database that doesn't require a server
|
# PHOTOPRISM_DATABASE_DRIVER: "sqlite" # SQLite is an embedded database that doesn't require a server
|
||||||
|
|||||||
@@ -63,8 +63,9 @@ services:
|
|||||||
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
|
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
|
||||||
PHOTOPRISM_DISABLE_FACES: "false" # disables facial recognition
|
PHOTOPRISM_DISABLE_FACES: "false" # disables facial recognition
|
||||||
PHOTOPRISM_DISABLE_CLASSIFICATION: "false" # disables image classification
|
PHOTOPRISM_DISABLE_CLASSIFICATION: "false" # disables image classification
|
||||||
|
PHOTOPRISM_DISABLE_RAW: "false" # disables indexing and conversion of RAW files
|
||||||
|
PHOTOPRISM_RAW_PRESETS: "false" # enables applying user presets when converting RAW files (reduces performance)
|
||||||
PHOTOPRISM_JPEG_QUALITY: 85 # image quality, a higher value reduces compression (25-100)
|
PHOTOPRISM_JPEG_QUALITY: 85 # image quality, a higher value reduces compression (25-100)
|
||||||
PHOTOPRISM_RAW_PRESETS: "false" # enables RAW file converter presets (may reduce performance)
|
|
||||||
PHOTOPRISM_DETECT_NSFW: "false" # flag photos as private that MAY be offensive (requires TensorFlow)
|
PHOTOPRISM_DETECT_NSFW: "false" # flag photos as private that MAY be offensive (requires TensorFlow)
|
||||||
PHOTOPRISM_UPLOAD_NSFW: "true" # allows uploads that MAY be offensive
|
PHOTOPRISM_UPLOAD_NSFW: "true" # allows uploads that MAY be offensive
|
||||||
PHOTOPRISM_DATABASE_DRIVER: "mysql" # use MariaDB 10.5+ or MySQL 8+ instead of SQLite for improved performance
|
PHOTOPRISM_DATABASE_DRIVER: "mysql" # use MariaDB 10.5+ or MySQL 8+ instead of SQLite for improved performance
|
||||||
|
|||||||
@@ -68,8 +68,9 @@ services:
|
|||||||
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
|
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
|
||||||
PHOTOPRISM_DISABLE_FACES: "false" # disables facial recognition
|
PHOTOPRISM_DISABLE_FACES: "false" # disables facial recognition
|
||||||
PHOTOPRISM_DISABLE_CLASSIFICATION: "false" # disables image classification
|
PHOTOPRISM_DISABLE_CLASSIFICATION: "false" # disables image classification
|
||||||
|
PHOTOPRISM_DISABLE_RAW: "false" # disables indexing and conversion of RAW files
|
||||||
|
PHOTOPRISM_RAW_PRESETS: "false" # enables applying user presets when converting RAW files (reduces performance)
|
||||||
PHOTOPRISM_JPEG_QUALITY: 85 # image quality, a higher value reduces compression (25-100)
|
PHOTOPRISM_JPEG_QUALITY: 85 # image quality, a higher value reduces compression (25-100)
|
||||||
PHOTOPRISM_RAW_PRESETS: "false" # enables RAW file converter presets (may reduce performance)
|
|
||||||
PHOTOPRISM_DETECT_NSFW: "false" # flag photos as private that MAY be offensive (requires TensorFlow)
|
PHOTOPRISM_DETECT_NSFW: "false" # flag photos as private that MAY be offensive (requires TensorFlow)
|
||||||
PHOTOPRISM_UPLOAD_NSFW: "true" # allows uploads that MAY be offensive
|
PHOTOPRISM_UPLOAD_NSFW: "true" # allows uploads that MAY be offensive
|
||||||
# PHOTOPRISM_DATABASE_DRIVER: "sqlite" # SQLite is an embedded database that doesn't require a server
|
# PHOTOPRISM_DATABASE_DRIVER: "sqlite" # SQLite is an embedded database that doesn't require a server
|
||||||
|
|||||||
@@ -66,8 +66,9 @@ services:
|
|||||||
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
|
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
|
||||||
PHOTOPRISM_DISABLE_FACES: "false" # disables facial recognition
|
PHOTOPRISM_DISABLE_FACES: "false" # disables facial recognition
|
||||||
PHOTOPRISM_DISABLE_CLASSIFICATION: "false" # disables image classification
|
PHOTOPRISM_DISABLE_CLASSIFICATION: "false" # disables image classification
|
||||||
|
PHOTOPRISM_DISABLE_RAW: "false" # disables indexing and conversion of RAW files
|
||||||
|
PHOTOPRISM_RAW_PRESETS: "false" # enables applying user presets when converting RAW files (reduces performance)
|
||||||
PHOTOPRISM_JPEG_QUALITY: 85 # image quality, a higher value reduces compression (25-100)
|
PHOTOPRISM_JPEG_QUALITY: 85 # image quality, a higher value reduces compression (25-100)
|
||||||
PHOTOPRISM_RAW_PRESETS: "false" # enables RAW file converter presets (may reduce performance)
|
|
||||||
PHOTOPRISM_DETECT_NSFW: "false" # flag photos as private that MAY be offensive (requires TensorFlow)
|
PHOTOPRISM_DETECT_NSFW: "false" # flag photos as private that MAY be offensive (requires TensorFlow)
|
||||||
PHOTOPRISM_UPLOAD_NSFW: "true" # allows uploads that MAY be offensive
|
PHOTOPRISM_UPLOAD_NSFW: "true" # allows uploads that MAY be offensive
|
||||||
PHOTOPRISM_DATABASE_DRIVER: "sqlite" # SQLite is an embedded database that doesn't require a server
|
PHOTOPRISM_DATABASE_DRIVER: "sqlite" # SQLite is an embedded database that doesn't require a server
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ services:
|
|||||||
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
|
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
|
||||||
PHOTOPRISM_DISABLE_FACES: "false" # disables facial recognition
|
PHOTOPRISM_DISABLE_FACES: "false" # disables facial recognition
|
||||||
PHOTOPRISM_DISABLE_CLASSIFICATION: "false" # disables image classification
|
PHOTOPRISM_DISABLE_CLASSIFICATION: "false" # disables image classification
|
||||||
PHOTOPRISM_RAW_PRESETS: "false" # enables RAW file converter presets (may reduce performance)
|
PHOTOPRISM_DISABLE_RAW: "false" # disables indexing and conversion of RAW files
|
||||||
|
PHOTOPRISM_RAW_PRESETS: "false" # enables applying user presets when converting RAW files (reduces performance)
|
||||||
PHOTOPRISM_JPEG_QUALITY: 85 # image quality, a higher value reduces compression (25-100)
|
PHOTOPRISM_JPEG_QUALITY: 85 # image quality, a higher value reduces compression (25-100)
|
||||||
PHOTOPRISM_DETECT_NSFW: "false" # flag photos as private that MAY be offensive (requires TensorFlow)
|
PHOTOPRISM_DETECT_NSFW: "false" # flag photos as private that MAY be offensive (requires TensorFlow)
|
||||||
PHOTOPRISM_UPLOAD_NSFW: "true" # allows uploads that MAY be offensive
|
PHOTOPRISM_UPLOAD_NSFW: "true" # allows uploads that MAY be offensive
|
||||||
|
|||||||
124
frontend/package-lock.json
generated
124
frontend/package-lock.json
generated
@@ -4348,9 +4348,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.4.103",
|
"version": "1.4.104",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.103.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.104.tgz",
|
||||||
"integrity": "sha512-c/uKWR1Z/W30Wy/sx3dkZoj4BijbXX85QKWu9jJfjho3LBAXNEGAEW3oWiGb+dotA6C6BzCTxL2/aLes7jlUeg=="
|
"integrity": "sha512-2kjoAyiG7uMyGRM9mx25s3HAzmQG2ayuYXxsFmYugHSDcwxREgLtscZvbL1JcW9S/OemeQ3f/SG6JhDwpnCclQ=="
|
||||||
},
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
@@ -4937,23 +4937,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-import": {
|
"node_modules/eslint-plugin-import": {
|
||||||
"version": "2.25.4",
|
"version": "2.26.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz",
|
||||||
"integrity": "sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA==",
|
"integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"array-includes": "^3.1.4",
|
"array-includes": "^3.1.4",
|
||||||
"array.prototype.flat": "^1.2.5",
|
"array.prototype.flat": "^1.2.5",
|
||||||
"debug": "^2.6.9",
|
"debug": "^2.6.9",
|
||||||
"doctrine": "^2.1.0",
|
"doctrine": "^2.1.0",
|
||||||
"eslint-import-resolver-node": "^0.3.6",
|
"eslint-import-resolver-node": "^0.3.6",
|
||||||
"eslint-module-utils": "^2.7.2",
|
"eslint-module-utils": "^2.7.3",
|
||||||
"has": "^1.0.3",
|
"has": "^1.0.3",
|
||||||
"is-core-module": "^2.8.0",
|
"is-core-module": "^2.8.1",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
"minimatch": "^3.0.4",
|
"minimatch": "^3.1.2",
|
||||||
"object.values": "^1.1.5",
|
"object.values": "^1.1.5",
|
||||||
"resolve": "^1.20.0",
|
"resolve": "^1.22.0",
|
||||||
"tsconfig-paths": "^3.12.0"
|
"tsconfig-paths": "^3.14.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
@@ -5156,9 +5156,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-vue": {
|
"node_modules/eslint-plugin-vue": {
|
||||||
"version": "8.5.0",
|
"version": "8.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-8.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-8.6.0.tgz",
|
||||||
"integrity": "sha512-i1uHCTAKOoEj12RDvdtONWrGzjFm/djkzqfhmQ0d6M/W8KM81mhswd/z+iTZ0jCpdUedW3YRgcVfQ37/J4zoYQ==",
|
"integrity": "sha512-abXiF2J18n/7ZPy9foSlJyouKf54IqpKlNvNmzhM93N0zs3QUxZG/oBd3tVPOJTKg7SlhBUtPxugpqzNbgGpQQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"eslint-utils": "^3.0.0",
|
"eslint-utils": "^3.0.0",
|
||||||
"natural-compare": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
@@ -6037,19 +6037,19 @@
|
|||||||
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg=="
|
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg=="
|
||||||
},
|
},
|
||||||
"node_modules/flow-parser": {
|
"node_modules/flow-parser": {
|
||||||
"version": "0.175.0",
|
"version": "0.175.1",
|
||||||
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.175.0.tgz",
|
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.175.1.tgz",
|
||||||
"integrity": "sha512-9XG5JGOjhODF+OQF5ufCw8XiGi+8B46scjr3Q49JxN7IDRdT2W+1AOuvKKd6j766/5E7qSuCn/dsq1y3hihntg==",
|
"integrity": "sha512-gYes5/nxeLYiu02MMb+WH4KaOIYrVcTVIuV9M4aP/4hqJ+zULxxS/In+WEj/tEBsQ+8/wSHo9IDWKQL1FhrLmA==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/flow-remove-types": {
|
"node_modules/flow-remove-types": {
|
||||||
"version": "2.175.0",
|
"version": "2.175.1",
|
||||||
"resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.175.0.tgz",
|
"resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.175.1.tgz",
|
||||||
"integrity": "sha512-5DKqnBquIdg6KwwH4VtHw+eHw+uCrnhVCTi32q9wMWGjbe2FnpJTDbODKIQyMYowCGTb8jTs0Kqk4Hb66kY1Mg==",
|
"integrity": "sha512-malB3a9t7zEi5TMBbQlSZ47vqH673aiN+a34Xlv6/q3fJwyaXYlIz2wa9tMa3h5kM26aH7dFRS0GeBbfV1C5IQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"flow-parser": "^0.175.0",
|
"flow-parser": "^0.175.1",
|
||||||
"pirates": "^3.0.2",
|
"pirates": "^3.0.2",
|
||||||
"vlq": "^0.2.1"
|
"vlq": "^0.2.1"
|
||||||
},
|
},
|
||||||
@@ -6320,9 +6320,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/graceful-fs": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.9",
|
"version": "4.2.10",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
|
||||||
"integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ=="
|
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
|
||||||
},
|
},
|
||||||
"node_modules/growl": {
|
"node_modules/growl": {
|
||||||
"version": "1.10.5",
|
"version": "1.10.5",
|
||||||
@@ -9028,15 +9028,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss-custom-properties": {
|
"node_modules/postcss-custom-properties": {
|
||||||
"version": "12.1.5",
|
"version": "12.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.6.tgz",
|
||||||
"integrity": "sha512-FHbbB/hRo/7cxLGkc2NS7cDRIDN1oFqQnUKBiyh4b/gwk8DD8udvmRDpUhEK836kB8ggUCieHVOvZDnF9XhI3g==",
|
"integrity": "sha512-QEnQkDkb+J+j2bfJisJJpTAFL+lUFl66rUNvnjPBIvRbZACLG4Eu5bmBCIY4FJCqhwsfbBpmJUyb3FcR/31lAg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"postcss-value-parser": "^4.2.0"
|
"postcss-value-parser": "^4.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12 || ^14 || >=16"
|
"node": "^12 || ^14 || >=16"
|
||||||
},
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"postcss": "^8.4"
|
"postcss": "^8.4"
|
||||||
}
|
}
|
||||||
@@ -11260,9 +11264,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/supercluster": {
|
"node_modules/supercluster": {
|
||||||
"version": "7.1.4",
|
"version": "7.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz",
|
||||||
"integrity": "sha512-GhKkRM1jMR6WUwGPw05fs66pOFWhf59lXq+Q3J3SxPvhNcmgOtLRV6aVQPMRsmXdpaeFJGivt+t7QXUPL3ff4g==",
|
"integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"kdbush": "^3.0.0"
|
"kdbush": "^3.0.0"
|
||||||
}
|
}
|
||||||
@@ -15911,9 +15915,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"electron-to-chromium": {
|
"electron-to-chromium": {
|
||||||
"version": "1.4.103",
|
"version": "1.4.104",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.103.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.104.tgz",
|
||||||
"integrity": "sha512-c/uKWR1Z/W30Wy/sx3dkZoj4BijbXX85QKWu9jJfjho3LBAXNEGAEW3oWiGb+dotA6C6BzCTxL2/aLes7jlUeg=="
|
"integrity": "sha512-2kjoAyiG7uMyGRM9mx25s3HAzmQG2ayuYXxsFmYugHSDcwxREgLtscZvbL1JcW9S/OemeQ3f/SG6JhDwpnCclQ=="
|
||||||
},
|
},
|
||||||
"emoji-regex": {
|
"emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
@@ -16447,23 +16451,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"eslint-plugin-import": {
|
"eslint-plugin-import": {
|
||||||
"version": "2.25.4",
|
"version": "2.26.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz",
|
||||||
"integrity": "sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA==",
|
"integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"array-includes": "^3.1.4",
|
"array-includes": "^3.1.4",
|
||||||
"array.prototype.flat": "^1.2.5",
|
"array.prototype.flat": "^1.2.5",
|
||||||
"debug": "^2.6.9",
|
"debug": "^2.6.9",
|
||||||
"doctrine": "^2.1.0",
|
"doctrine": "^2.1.0",
|
||||||
"eslint-import-resolver-node": "^0.3.6",
|
"eslint-import-resolver-node": "^0.3.6",
|
||||||
"eslint-module-utils": "^2.7.2",
|
"eslint-module-utils": "^2.7.3",
|
||||||
"has": "^1.0.3",
|
"has": "^1.0.3",
|
||||||
"is-core-module": "^2.8.0",
|
"is-core-module": "^2.8.1",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
"minimatch": "^3.0.4",
|
"minimatch": "^3.1.2",
|
||||||
"object.values": "^1.1.5",
|
"object.values": "^1.1.5",
|
||||||
"resolve": "^1.20.0",
|
"resolve": "^1.22.0",
|
||||||
"tsconfig-paths": "^3.12.0"
|
"tsconfig-paths": "^3.14.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": {
|
"debug": {
|
||||||
@@ -16604,9 +16608,9 @@
|
|||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"eslint-plugin-vue": {
|
"eslint-plugin-vue": {
|
||||||
"version": "8.5.0",
|
"version": "8.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-8.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-8.6.0.tgz",
|
||||||
"integrity": "sha512-i1uHCTAKOoEj12RDvdtONWrGzjFm/djkzqfhmQ0d6M/W8KM81mhswd/z+iTZ0jCpdUedW3YRgcVfQ37/J4zoYQ==",
|
"integrity": "sha512-abXiF2J18n/7ZPy9foSlJyouKf54IqpKlNvNmzhM93N0zs3QUxZG/oBd3tVPOJTKg7SlhBUtPxugpqzNbgGpQQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"eslint-utils": "^3.0.0",
|
"eslint-utils": "^3.0.0",
|
||||||
"natural-compare": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
@@ -17140,16 +17144,16 @@
|
|||||||
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg=="
|
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg=="
|
||||||
},
|
},
|
||||||
"flow-parser": {
|
"flow-parser": {
|
||||||
"version": "0.175.0",
|
"version": "0.175.1",
|
||||||
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.175.0.tgz",
|
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.175.1.tgz",
|
||||||
"integrity": "sha512-9XG5JGOjhODF+OQF5ufCw8XiGi+8B46scjr3Q49JxN7IDRdT2W+1AOuvKKd6j766/5E7qSuCn/dsq1y3hihntg=="
|
"integrity": "sha512-gYes5/nxeLYiu02MMb+WH4KaOIYrVcTVIuV9M4aP/4hqJ+zULxxS/In+WEj/tEBsQ+8/wSHo9IDWKQL1FhrLmA=="
|
||||||
},
|
},
|
||||||
"flow-remove-types": {
|
"flow-remove-types": {
|
||||||
"version": "2.175.0",
|
"version": "2.175.1",
|
||||||
"resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.175.0.tgz",
|
"resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.175.1.tgz",
|
||||||
"integrity": "sha512-5DKqnBquIdg6KwwH4VtHw+eHw+uCrnhVCTi32q9wMWGjbe2FnpJTDbODKIQyMYowCGTb8jTs0Kqk4Hb66kY1Mg==",
|
"integrity": "sha512-malB3a9t7zEi5TMBbQlSZ47vqH673aiN+a34Xlv6/q3fJwyaXYlIz2wa9tMa3h5kM26aH7dFRS0GeBbfV1C5IQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"flow-parser": "^0.175.0",
|
"flow-parser": "^0.175.1",
|
||||||
"pirates": "^3.0.2",
|
"pirates": "^3.0.2",
|
||||||
"vlq": "^0.2.1"
|
"vlq": "^0.2.1"
|
||||||
},
|
},
|
||||||
@@ -17329,9 +17333,9 @@
|
|||||||
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
|
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
|
||||||
},
|
},
|
||||||
"graceful-fs": {
|
"graceful-fs": {
|
||||||
"version": "4.2.9",
|
"version": "4.2.10",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
|
||||||
"integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ=="
|
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
|
||||||
},
|
},
|
||||||
"growl": {
|
"growl": {
|
||||||
"version": "1.10.5",
|
"version": "1.10.5",
|
||||||
@@ -19263,9 +19267,9 @@
|
|||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"postcss-custom-properties": {
|
"postcss-custom-properties": {
|
||||||
"version": "12.1.5",
|
"version": "12.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.6.tgz",
|
||||||
"integrity": "sha512-FHbbB/hRo/7cxLGkc2NS7cDRIDN1oFqQnUKBiyh4b/gwk8DD8udvmRDpUhEK836kB8ggUCieHVOvZDnF9XhI3g==",
|
"integrity": "sha512-QEnQkDkb+J+j2bfJisJJpTAFL+lUFl66rUNvnjPBIvRbZACLG4Eu5bmBCIY4FJCqhwsfbBpmJUyb3FcR/31lAg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"postcss-value-parser": "^4.2.0"
|
"postcss-value-parser": "^4.2.0"
|
||||||
}
|
}
|
||||||
@@ -20855,9 +20859,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"supercluster": {
|
"supercluster": {
|
||||||
"version": "7.1.4",
|
"version": "7.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz",
|
||||||
"integrity": "sha512-GhKkRM1jMR6WUwGPw05fs66pOFWhf59lXq+Q3J3SxPvhNcmgOtLRV6aVQPMRsmXdpaeFJGivt+t7QXUPL3ff4g==",
|
"integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"kdbush": "^3.0.0"
|
"kdbush": "^3.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export class ConfigOptions extends Model {
|
|||||||
DisableDarktable: config.values.disable.darktable,
|
DisableDarktable: config.values.disable.darktable,
|
||||||
DisableRawtherapee: config.values.disable.rawtherapee,
|
DisableRawtherapee: config.values.disable.rawtherapee,
|
||||||
DisableSips: config.values.disable.sips,
|
DisableSips: config.values.disable.sips,
|
||||||
|
DisableRaw: config.values.disable.raw,
|
||||||
DisableHeifConvert: config.values.disable.heifconvert,
|
DisableHeifConvert: config.values.disable.heifconvert,
|
||||||
DisableFFmpeg: config.values.disable.ffmpeg,
|
DisableFFmpeg: config.values.disable.ffmpeg,
|
||||||
DisableTensorFlow: config.values.disable.tensorflow,
|
DisableTensorFlow: config.values.disable.tensorflow,
|
||||||
|
|||||||
@@ -247,7 +247,7 @@
|
|||||||
<v-flex xs12 sm4 class="px-2 pb-2 pt-2">
|
<v-flex xs12 sm4 class="px-2 pb-2 pt-2">
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
v-model="settings.RawPresets"
|
v-model="settings.RawPresets"
|
||||||
:disabled="busy"
|
:disabled="busy || settings.DisableRaw"
|
||||||
class="ma-0 pa-0"
|
class="ma-0 pa-0"
|
||||||
color="secondary-dark"
|
color="secondary-dark"
|
||||||
:label="$gettext('Use Presets')"
|
:label="$gettext('Use Presets')"
|
||||||
@@ -262,7 +262,7 @@
|
|||||||
<v-flex xs12 sm4 class="px-2 pb-2 pt-2">
|
<v-flex xs12 sm4 class="px-2 pb-2 pt-2">
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
v-model="settings.DisableDarktable"
|
v-model="settings.DisableDarktable"
|
||||||
:disabled="busy"
|
:disabled="busy || settings.DisableRaw"
|
||||||
class="ma-0 pa-0 input-private"
|
class="ma-0 pa-0 input-private"
|
||||||
color="secondary-dark"
|
color="secondary-dark"
|
||||||
:label="$gettext('Disable Darktable')"
|
:label="$gettext('Disable Darktable')"
|
||||||
@@ -277,7 +277,7 @@
|
|||||||
<v-flex xs12 sm4 class="px-2 pb-2 pt-2">
|
<v-flex xs12 sm4 class="px-2 pb-2 pt-2">
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
v-model="settings.DisableRawtherapee"
|
v-model="settings.DisableRawtherapee"
|
||||||
:disabled="busy"
|
:disabled="busy || settings.DisableRaw"
|
||||||
class="ma-0 pa-0 input-private"
|
class="ma-0 pa-0 input-private"
|
||||||
color="secondary-dark"
|
color="secondary-dark"
|
||||||
:label="$gettext('Disable RawTherapee')"
|
:label="$gettext('Disable RawTherapee')"
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ func AlbumCover(router *gin.RouterGroup) {
|
|||||||
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
|
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
|
||||||
return
|
return
|
||||||
} else if thumbnail == "" {
|
} else if thumbnail == "" {
|
||||||
log.Errorf("%s: %s has empty thumb name - bug?", albumCover, filepath.Base(fileName))
|
log.Errorf("%s: %s has empty thumb name - possible bug", albumCover, filepath.Base(fileName))
|
||||||
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
|
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -229,7 +229,7 @@ func LabelCover(router *gin.RouterGroup) {
|
|||||||
c.Data(http.StatusOK, "image/svg+xml", labelIconSvg)
|
c.Data(http.StatusOK, "image/svg+xml", labelIconSvg)
|
||||||
return
|
return
|
||||||
} else if thumbnail == "" {
|
} else if thumbnail == "" {
|
||||||
log.Errorf("%s: %s has empty thumb name - bug?", labelCover, filepath.Base(fileName))
|
log.Errorf("%s: %s has empty thumb name - possible bug", labelCover, filepath.Base(fileName))
|
||||||
c.Data(http.StatusOK, "image/svg+xml", labelIconSvg)
|
c.Data(http.StatusOK, "image/svg+xml", labelIconSvg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ func FolderCover(router *gin.RouterGroup) {
|
|||||||
c.Data(http.StatusOK, "image/svg+xml", folderIconSvg)
|
c.Data(http.StatusOK, "image/svg+xml", folderIconSvg)
|
||||||
return
|
return
|
||||||
} else if thumbnail == "" {
|
} else if thumbnail == "" {
|
||||||
log.Errorf("%s: %s has empty thumb name - bug?", folderCover, filepath.Base(fileName))
|
log.Errorf("%s: %s has empty thumb name - possible bug", folderCover, filepath.Base(fileName))
|
||||||
c.Data(http.StatusOK, "image/svg+xml", folderIconSvg)
|
c.Data(http.StatusOK, "image/svg+xml", folderIconSvg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ func GetThumb(router *gin.RouterGroup) {
|
|||||||
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
|
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
|
||||||
return
|
return
|
||||||
} else if thumbnail == "" {
|
} else if thumbnail == "" {
|
||||||
log.Errorf("%s: %s has empty thumb name - bug?", logPrefix, filepath.Base(fileName))
|
log.Errorf("%s: %s has empty thumb name - possible bug", logPrefix, filepath.Base(fileName))
|
||||||
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
|
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ package api
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/pkg/sanitize"
|
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/service"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/photoprism"
|
"github.com/photoprism/photoprism/internal/photoprism"
|
||||||
"github.com/photoprism/photoprism/internal/query"
|
"github.com/photoprism/photoprism/internal/query"
|
||||||
|
"github.com/photoprism/photoprism/internal/service"
|
||||||
"github.com/photoprism/photoprism/internal/video"
|
"github.com/photoprism/photoprism/internal/video"
|
||||||
|
"github.com/photoprism/photoprism/pkg/sanitize"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetVideo streams videos.
|
// GetVideo streams videos.
|
||||||
@@ -75,7 +74,7 @@ func GetVideo(router *gin.RouterGroup) {
|
|||||||
} else if f.FileCodec != string(videoType.Codec) {
|
} else if f.FileCodec != string(videoType.Codec) {
|
||||||
conv := service.Convert()
|
conv := service.Convert()
|
||||||
|
|
||||||
if avcFile, err := conv.ToAvc(mf, service.Config().FFmpegEncoder()); err != nil {
|
if avcFile, err := conv.ToAvc(mf, service.Config().FFmpegEncoder(), false, false); err != nil {
|
||||||
log.Errorf("video: transcoding %s failed", sanitize.Log(f.FileName))
|
log.Errorf("video: transcoding %s failed", sanitize.Log(f.FileName))
|
||||||
c.Data(http.StatusOK, "image/svg+xml", videoIconSvg)
|
c.Data(http.StatusOK, "image/svg+xml", videoIconSvg)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func configAction(ctx *cli.Context) error {
|
|||||||
|
|
||||||
dbDriver := conf.DatabaseDriver()
|
dbDriver := conf.DatabaseDriver()
|
||||||
|
|
||||||
fmt.Printf("%-25s Value\n", "Name")
|
fmt.Printf("%-25s VALUE\n", "NAME")
|
||||||
|
|
||||||
// Flags.
|
// Flags.
|
||||||
fmt.Printf("%-25s %t\n", "debug", conf.Debug())
|
fmt.Printf("%-25s %t\n", "debug", conf.Debug())
|
||||||
@@ -73,16 +73,17 @@ func configAction(ctx *cli.Context) error {
|
|||||||
fmt.Printf("%-25s %t\n", "disable-tensorflow", conf.DisableTensorFlow())
|
fmt.Printf("%-25s %t\n", "disable-tensorflow", conf.DisableTensorFlow())
|
||||||
fmt.Printf("%-25s %t\n", "disable-faces", conf.DisableFaces())
|
fmt.Printf("%-25s %t\n", "disable-faces", conf.DisableFaces())
|
||||||
fmt.Printf("%-25s %t\n", "disable-classification", conf.DisableClassification())
|
fmt.Printf("%-25s %t\n", "disable-classification", conf.DisableClassification())
|
||||||
|
fmt.Printf("%-25s %t\n", "disable-ffmpeg", conf.DisableFFmpeg())
|
||||||
|
fmt.Printf("%-25s %t\n", "disable-exiftool", conf.DisableExifTool())
|
||||||
|
fmt.Printf("%-25s %t\n", "disable-heifconvert", conf.DisableHeifConvert())
|
||||||
fmt.Printf("%-25s %t\n", "disable-darktable", conf.DisableDarktable())
|
fmt.Printf("%-25s %t\n", "disable-darktable", conf.DisableDarktable())
|
||||||
fmt.Printf("%-25s %t\n", "disable-rawtherapee", conf.DisableRawtherapee())
|
fmt.Printf("%-25s %t\n", "disable-rawtherapee", conf.DisableRawtherapee())
|
||||||
fmt.Printf("%-25s %t\n", "disable-sips", conf.DisableSips())
|
fmt.Printf("%-25s %t\n", "disable-sips", conf.DisableSips())
|
||||||
fmt.Printf("%-25s %t\n", "disable-heifconvert", conf.DisableHeifConvert())
|
fmt.Printf("%-25s %t\n", "disable-raw", conf.DisableRaw())
|
||||||
fmt.Printf("%-25s %t\n", "disable-ffmpeg", conf.DisableFFmpeg())
|
|
||||||
fmt.Printf("%-25s %t\n", "disable-exiftool", conf.DisableExifTool())
|
|
||||||
|
|
||||||
// Format Flags.
|
// Format Flags.
|
||||||
fmt.Printf("%-25s %t\n", "exif-bruteforce", conf.ExifBruteForce())
|
|
||||||
fmt.Printf("%-25s %t\n", "raw-presets", conf.RawPresets())
|
fmt.Printf("%-25s %t\n", "raw-presets", conf.RawPresets())
|
||||||
|
fmt.Printf("%-25s %t\n", "exif-bruteforce", conf.ExifBruteForce())
|
||||||
|
|
||||||
// TensorFlow.
|
// TensorFlow.
|
||||||
fmt.Printf("%-25s %t\n", "detect-nsfw", conf.DetectNSFW())
|
fmt.Printf("%-25s %t\n", "detect-nsfw", conf.DetectNSFW())
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func TestConfigCommand(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Expected config command output.
|
// Expected config command output.
|
||||||
assert.Contains(t, output, "Name Value")
|
assert.Contains(t, output, "NAME VALUE")
|
||||||
assert.Contains(t, output, "config-file")
|
assert.Contains(t, output, "config-file")
|
||||||
assert.Contains(t, output, "darktable-cli")
|
assert.Contains(t, output, "darktable-cli")
|
||||||
assert.Contains(t, output, "originals-path")
|
assert.Contains(t, output, "originals-path")
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func (c *Config) CheckPassword(p string) bool {
|
|||||||
return ap == p
|
return ap == p
|
||||||
}
|
}
|
||||||
|
|
||||||
// InvalidDownloadToken tests if the token is invalid.
|
// InvalidDownloadToken checks if the token is invalid.
|
||||||
func (c *Config) InvalidDownloadToken(t string) bool {
|
func (c *Config) InvalidDownloadToken(t string) bool {
|
||||||
return c.DownloadToken() != t
|
return c.DownloadToken() != t
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@ func (c *Config) DownloadToken() string {
|
|||||||
return c.options.DownloadToken
|
return c.options.DownloadToken
|
||||||
}
|
}
|
||||||
|
|
||||||
// InvalidPreviewToken tests if the preview token is invalid.
|
// InvalidPreviewToken checks if the preview token is invalid.
|
||||||
func (c *Config) InvalidPreviewToken(t string) bool {
|
func (c *Config) InvalidPreviewToken(t string) bool {
|
||||||
return c.PreviewToken() != t && c.DownloadToken() != t
|
return c.PreviewToken() != t && c.DownloadToken() != t
|
||||||
}
|
}
|
||||||
@@ -51,6 +51,8 @@ func (c *Config) PreviewToken() string {
|
|||||||
if c.options.PreviewToken == "" {
|
if c.options.PreviewToken == "" {
|
||||||
if c.Public() {
|
if c.Public() {
|
||||||
c.options.PreviewToken = "public"
|
c.options.PreviewToken = "public"
|
||||||
|
} else if c.Serial() == "" {
|
||||||
|
return "********"
|
||||||
} else {
|
} else {
|
||||||
c.options.PreviewToken = c.SerialChecksum()
|
c.options.PreviewToken = c.SerialChecksum()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ type ClientDisable struct {
|
|||||||
Places bool `json:"places"`
|
Places bool `json:"places"`
|
||||||
ExifTool bool `json:"exiftool"`
|
ExifTool bool `json:"exiftool"`
|
||||||
FFmpeg bool `json:"ffmpeg"`
|
FFmpeg bool `json:"ffmpeg"`
|
||||||
|
Raw bool `json:"raw"`
|
||||||
Darktable bool `json:"darktable"`
|
Darktable bool `json:"darktable"`
|
||||||
Rawtherapee bool `json:"rawtherapee"`
|
Rawtherapee bool `json:"rawtherapee"`
|
||||||
Sips bool `json:"sips"`
|
Sips bool `json:"sips"`
|
||||||
@@ -193,6 +194,7 @@ func (c *Config) PublicConfig() ClientConfig {
|
|||||||
Places: c.DisablePlaces(),
|
Places: c.DisablePlaces(),
|
||||||
ExifTool: true,
|
ExifTool: true,
|
||||||
FFmpeg: true,
|
FFmpeg: true,
|
||||||
|
Raw: true,
|
||||||
Darktable: true,
|
Darktable: true,
|
||||||
Rawtherapee: true,
|
Rawtherapee: true,
|
||||||
Sips: true,
|
Sips: true,
|
||||||
@@ -264,6 +266,7 @@ func (c *Config) GuestConfig() ClientConfig {
|
|||||||
Places: c.DisablePlaces(),
|
Places: c.DisablePlaces(),
|
||||||
ExifTool: true,
|
ExifTool: true,
|
||||||
FFmpeg: true,
|
FFmpeg: true,
|
||||||
|
Raw: true,
|
||||||
Darktable: true,
|
Darktable: true,
|
||||||
Rawtherapee: true,
|
Rawtherapee: true,
|
||||||
Sips: true,
|
Sips: true,
|
||||||
@@ -329,6 +332,7 @@ func (c *Config) UserConfig() ClientConfig {
|
|||||||
Places: c.DisablePlaces(),
|
Places: c.DisablePlaces(),
|
||||||
ExifTool: c.DisableExifTool(),
|
ExifTool: c.DisableExifTool(),
|
||||||
FFmpeg: c.DisableFFmpeg(),
|
FFmpeg: c.DisableFFmpeg(),
|
||||||
|
Raw: c.DisableRaw(),
|
||||||
Darktable: c.DisableDarktable(),
|
Darktable: c.DisableDarktable(),
|
||||||
Rawtherapee: c.DisableRawtherapee(),
|
Rawtherapee: c.DisableRawtherapee(),
|
||||||
Sips: c.DisableSips(),
|
Sips: c.DisableSips(),
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ const MinMem = Gigabyte
|
|||||||
// RecommendedMem is the recommended amount of system memory.
|
// RecommendedMem is the recommended amount of system memory.
|
||||||
const RecommendedMem = 3 * Gigabyte
|
const RecommendedMem = 3 * Gigabyte
|
||||||
|
|
||||||
|
// serialName is the name of the unique storage serial.
|
||||||
|
const serialName = "serial"
|
||||||
|
|
||||||
// Config holds database, cache and all parameters of photoprism
|
// Config holds database, cache and all parameters of photoprism
|
||||||
type Config struct {
|
type Config struct {
|
||||||
once sync.Once
|
once sync.Once
|
||||||
@@ -143,7 +146,7 @@ func (c *Config) Unsafe() bool {
|
|||||||
// Options returns the raw config options.
|
// Options returns the raw config options.
|
||||||
func (c *Config) Options() *Options {
|
func (c *Config) Options() *Options {
|
||||||
if c.options == nil {
|
if c.options == nil {
|
||||||
log.Warnf("config: options should not be nil - bug?")
|
log.Warnf("config: options should not be nil - possible bug")
|
||||||
c.options = NewOptions(nil)
|
c.options = NewOptions(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +189,7 @@ func (c *Config) Init() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.initStorage(); err != nil {
|
if err := c.initSerial(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,11 +210,15 @@ func (c *Config) Init() error {
|
|||||||
log.Debugf("config: running on %s, %s memory detected", sanitize.Log(cpuid.CPU.BrandName), humanize.Bytes(TotalMem))
|
log.Debugf("config: running on %s, %s memory detected", sanitize.Log(cpuid.CPU.BrandName), humanize.Bytes(TotalMem))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check memory requirements.
|
// Exit if less than 128 MB RAM was detected.
|
||||||
if TotalMem < 128*Megabyte {
|
if TotalMem < 128*Megabyte {
|
||||||
return fmt.Errorf("config: %s of memory detected, %d GB required", humanize.Bytes(TotalMem), MinMem/Gigabyte)
|
return fmt.Errorf("config: %s of memory detected, %d GB required", humanize.Bytes(TotalMem), MinMem/Gigabyte)
|
||||||
} else if LowMem {
|
}
|
||||||
|
|
||||||
|
// Show warning if less than 1 GB RAM was detected.
|
||||||
|
if LowMem {
|
||||||
log.Warnf(`config: less than %d GB of memory detected, please upgrade if server becomes unstable or unresponsive`, MinMem/Gigabyte)
|
log.Warnf(`config: less than %d GB of memory detected, please upgrade if server becomes unstable or unresponsive`, MinMem/Gigabyte)
|
||||||
|
log.Warnf("config: tensorflow as well as indexing and conversion of RAW files have been disabled automatically")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show swap info.
|
// Show swap info.
|
||||||
@@ -242,28 +249,47 @@ func (c *Config) Init() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// initStorage initializes storage directories with a random serial.
|
// readSerial reads and returns the current storage serial.
|
||||||
func (c *Config) initStorage() error {
|
func (c *Config) readSerial() string {
|
||||||
if c.serial != "" {
|
storageName := filepath.Join(c.StoragePath(), serialName)
|
||||||
return nil
|
backupName := filepath.Join(c.BackupPath(), serialName)
|
||||||
|
|
||||||
|
if fs.FileExists(storageName) {
|
||||||
|
if data, err := os.ReadFile(storageName); err == nil && len(data) == 16 {
|
||||||
|
return string(data)
|
||||||
|
} else {
|
||||||
|
log.Tracef("config: could not read %s (%s)", sanitize.Log(storageName), err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const serialName = "serial"
|
if fs.FileExists(backupName) {
|
||||||
|
if data, err := os.ReadFile(backupName); err == nil && len(data) == 16 {
|
||||||
|
return string(data)
|
||||||
|
} else {
|
||||||
|
log.Tracef("config: could not read %s (%s)", sanitize.Log(backupName), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// initSerial initializes storage directories with a random serial.
|
||||||
|
func (c *Config) initSerial() (err error) {
|
||||||
|
if c.Serial() != "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
c.serial = rnd.PPID('z')
|
c.serial = rnd.PPID('z')
|
||||||
|
|
||||||
storageName := filepath.Join(c.StoragePath(), serialName)
|
storageName := filepath.Join(c.StoragePath(), serialName)
|
||||||
backupName := filepath.Join(c.BackupPath(), serialName)
|
backupName := filepath.Join(c.BackupPath(), serialName)
|
||||||
|
|
||||||
if data, err := os.ReadFile(storageName); err == nil && len(data) == 16 {
|
if err = os.WriteFile(storageName, []byte(c.serial), os.ModePerm); err != nil {
|
||||||
c.serial = string(data)
|
return fmt.Errorf("could not create %s: %s", storageName, err)
|
||||||
} else if data, err := os.ReadFile(backupName); err == nil && len(data) == 16 {
|
}
|
||||||
c.serial = string(data)
|
|
||||||
LogError(os.WriteFile(storageName, []byte(c.serial), os.ModePerm))
|
if err = os.WriteFile(backupName, []byte(c.serial), os.ModePerm); err != nil {
|
||||||
} else if err := os.WriteFile(storageName, []byte(c.serial), os.ModePerm); err != nil {
|
return fmt.Errorf("could not create %s: %s", backupName, err)
|
||||||
return fmt.Errorf("failed creating %s: %s", storageName, err)
|
|
||||||
} else if err := os.WriteFile(backupName, []byte(c.serial), os.ModePerm); err != nil {
|
|
||||||
return fmt.Errorf("failed creating %s: %s", backupName, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -271,8 +297,8 @@ func (c *Config) initStorage() error {
|
|||||||
|
|
||||||
// Serial returns the random storage serial.
|
// Serial returns the random storage serial.
|
||||||
func (c *Config) Serial() string {
|
func (c *Config) Serial() string {
|
||||||
if err := c.initStorage(); err != nil {
|
if c.serial == "" {
|
||||||
log.Errorf("config: %s", err)
|
c.serial = c.readSerial()
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.serial
|
return c.serial
|
||||||
@@ -419,17 +445,17 @@ func (c *Config) ImprintUrl() string {
|
|||||||
return c.options.ImprintUrl
|
return c.options.ImprintUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug tests if debug mode is enabled.
|
// Debug checks if debug mode is enabled.
|
||||||
func (c *Config) Debug() bool {
|
func (c *Config) Debug() bool {
|
||||||
return c.options.Debug
|
return c.options.Debug
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test tests if test mode is enabled.
|
// Test checks if test mode is enabled.
|
||||||
func (c *Config) Test() bool {
|
func (c *Config) Test() bool {
|
||||||
return c.options.Test
|
return c.options.Test
|
||||||
}
|
}
|
||||||
|
|
||||||
// Demo tests if demo mode is enabled.
|
// Demo checks if demo mode is enabled.
|
||||||
func (c *Config) Demo() bool {
|
func (c *Config) Demo() bool {
|
||||||
return c.options.Demo
|
return c.options.Demo
|
||||||
}
|
}
|
||||||
@@ -439,7 +465,7 @@ func (c *Config) Sponsor() bool {
|
|||||||
return c.options.Sponsor || c.Test()
|
return c.options.Sponsor || c.Test()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public tests if app runs in public mode and requires no authentication.
|
// Public checks if app runs in public mode and requires no authentication.
|
||||||
func (c *Config) Public() bool {
|
func (c *Config) Public() bool {
|
||||||
if c.Demo() {
|
if c.Demo() {
|
||||||
return true
|
return true
|
||||||
@@ -455,22 +481,22 @@ func (c *Config) SetPublic(p bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Experimental tests if experimental features should be enabled.
|
// Experimental checks if experimental features should be enabled.
|
||||||
func (c *Config) Experimental() bool {
|
func (c *Config) Experimental() bool {
|
||||||
return c.options.Experimental
|
return c.options.Experimental
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadOnly tests if photo directories are write protected.
|
// ReadOnly checks if photo directories are write protected.
|
||||||
func (c *Config) ReadOnly() bool {
|
func (c *Config) ReadOnly() bool {
|
||||||
return c.options.ReadOnly
|
return c.options.ReadOnly
|
||||||
}
|
}
|
||||||
|
|
||||||
// DetectNSFW tests if NSFW photos should be detected and flagged.
|
// DetectNSFW checks if NSFW photos should be detected and flagged.
|
||||||
func (c *Config) DetectNSFW() bool {
|
func (c *Config) DetectNSFW() bool {
|
||||||
return c.options.DetectNSFW
|
return c.options.DetectNSFW
|
||||||
}
|
}
|
||||||
|
|
||||||
// UploadNSFW tests if NSFW photos can be uploaded.
|
// UploadNSFW checks if NSFW photos can be uploaded.
|
||||||
func (c *Config) UploadNSFW() bool {
|
func (c *Config) UploadNSFW() bool {
|
||||||
return c.options.UploadNSFW
|
return c.options.UploadNSFW
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
// DisableWebDAV tests if the built-in WebDAV server should be disabled.
|
// DisableWebDAV checks if the built-in WebDAV server should be disabled.
|
||||||
func (c *Config) DisableWebDAV() bool {
|
func (c *Config) DisableWebDAV() bool {
|
||||||
if c.ReadOnly() || c.Demo() {
|
if c.ReadOnly() || c.Demo() {
|
||||||
return true
|
return true
|
||||||
@@ -9,7 +9,7 @@ func (c *Config) DisableWebDAV() bool {
|
|||||||
return c.options.DisableWebDAV
|
return c.options.DisableWebDAV
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisableBackups tests if photo and album metadata files should be disabled.
|
// DisableBackups checks if photo and album metadata files should be disabled.
|
||||||
func (c *Config) DisableBackups() bool {
|
func (c *Config) DisableBackups() bool {
|
||||||
if !c.SidecarWritable() {
|
if !c.SidecarWritable() {
|
||||||
return true
|
return true
|
||||||
@@ -18,36 +18,37 @@ func (c *Config) DisableBackups() bool {
|
|||||||
return c.options.DisableBackups
|
return c.options.DisableBackups
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisableSettings tests if users should not be allowed to change settings.
|
// DisableSettings checks if users should not be allowed to change settings.
|
||||||
func (c *Config) DisableSettings() bool {
|
func (c *Config) DisableSettings() bool {
|
||||||
return c.options.DisableSettings
|
return c.options.DisableSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisablePlaces tests if geocoding and maps should be disabled.
|
// DisablePlaces checks if geocoding and maps should be disabled.
|
||||||
func (c *Config) DisablePlaces() bool {
|
func (c *Config) DisablePlaces() bool {
|
||||||
return c.options.DisablePlaces
|
return c.options.DisablePlaces
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisableExifTool tests if ExifTool JSON files should not be created for improved metadata extraction.
|
// DisableExifTool checks if ExifTool JSON files should not be created for improved metadata extraction.
|
||||||
func (c *Config) DisableExifTool() bool {
|
func (c *Config) DisableExifTool() bool {
|
||||||
if !c.SidecarWritable() || c.ExifToolBin() == "" {
|
if c.options.DisableExifTool {
|
||||||
return true
|
return true
|
||||||
|
} else if !c.SidecarWritable() || c.ExifToolBin() == "" {
|
||||||
|
c.options.DisableExifTool = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.options.DisableExifTool
|
return c.options.DisableExifTool
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisableTensorFlow tests if all features depending on TensorFlow should be disabled.
|
// DisableTensorFlow checks if all features depending on TensorFlow should be disabled.
|
||||||
func (c *Config) DisableTensorFlow() bool {
|
func (c *Config) DisableTensorFlow() bool {
|
||||||
if LowMem && !c.options.DisableTensorFlow {
|
if LowMem && !c.options.DisableTensorFlow {
|
||||||
c.options.DisableTensorFlow = true
|
c.options.DisableTensorFlow = true
|
||||||
log.Warnf("config: disabled tensorflow due to memory constraints")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.options.DisableTensorFlow
|
return c.options.DisableTensorFlow
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisableFaces tests if facial recognition is disabled.
|
// DisableFaces checks if facial recognition is disabled.
|
||||||
func (c *Config) DisableFaces() bool {
|
func (c *Config) DisableFaces() bool {
|
||||||
if c.DisableTensorFlow() || c.options.DisableFaces {
|
if c.DisableTensorFlow() || c.options.DisableFaces {
|
||||||
return true
|
return true
|
||||||
@@ -56,7 +57,7 @@ func (c *Config) DisableFaces() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisableClassification tests if image classification is disabled.
|
// DisableClassification checks if image classification is disabled.
|
||||||
func (c *Config) DisableClassification() bool {
|
func (c *Config) DisableClassification() bool {
|
||||||
if c.DisableTensorFlow() || c.options.DisableClassification {
|
if c.DisableTensorFlow() || c.options.DisableClassification {
|
||||||
return true
|
return true
|
||||||
@@ -65,37 +66,67 @@ func (c *Config) DisableClassification() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisableFFmpeg tests if FFmpeg is disabled for video transcoding.
|
// DisableFFmpeg checks if FFmpeg is disabled for video transcoding.
|
||||||
func (c *Config) DisableFFmpeg() bool {
|
func (c *Config) DisableFFmpeg() bool {
|
||||||
return c.options.DisableFFmpeg || c.FFmpegBin() == ""
|
if c.options.DisableFFmpeg {
|
||||||
|
return true
|
||||||
|
} else if c.FFmpegBin() == "" {
|
||||||
|
c.options.DisableFFmpeg = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.options.DisableFFmpeg
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisableDarktable tests if Darktable is disabled for RAW conversion.
|
// DisableRaw checks if indexing and conversion of RAW files is disabled.
|
||||||
|
func (c *Config) DisableRaw() bool {
|
||||||
|
if LowMem && !c.options.DisableRaw {
|
||||||
|
c.options.DisableRaw = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.options.DisableRaw
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisableDarktable checks if conversion of RAW files with Darktable is disabled.
|
||||||
func (c *Config) DisableDarktable() bool {
|
func (c *Config) DisableDarktable() bool {
|
||||||
if LowMem && !c.options.DisableDarktable {
|
if c.DisableRaw() || c.options.DisableDarktable {
|
||||||
|
return true
|
||||||
|
} else if c.DarktableBin() == "" {
|
||||||
c.options.DisableDarktable = true
|
c.options.DisableDarktable = true
|
||||||
log.Warnf("config: disabled file conversion with Darktable due to memory constraints")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.options.DisableDarktable || c.DarktableBin() == ""
|
return c.options.DisableDarktable
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisableRawtherapee tests if Rawtherapee is disabled for RAW conversion.
|
// DisableRawtherapee checks if conversion of RAW files with Rawtherapee is disabled.
|
||||||
func (c *Config) DisableRawtherapee() bool {
|
func (c *Config) DisableRawtherapee() bool {
|
||||||
if LowMem && !c.options.DisableRawtherapee {
|
if c.DisableRaw() || c.options.DisableRawtherapee {
|
||||||
|
return true
|
||||||
|
} else if c.RawtherapeeBin() == "" {
|
||||||
c.options.DisableRawtherapee = true
|
c.options.DisableRawtherapee = true
|
||||||
log.Warnf("config: disabled file conversion with RawTherapee due to memory constraints")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.options.DisableRawtherapee || c.RawtherapeeBin() == ""
|
return c.options.DisableRawtherapee
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisableSips tests if SIPS is disabled for RAW conversion.
|
// DisableSips checks if conversion of RAW files with SIPS is disabled.
|
||||||
func (c *Config) DisableSips() bool {
|
func (c *Config) DisableSips() bool {
|
||||||
return c.options.DisableSips || c.SipsBin() == ""
|
if c.options.DisableSips {
|
||||||
|
return true
|
||||||
|
} else if c.SipsBin() == "" {
|
||||||
|
c.options.DisableSips = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.options.DisableSips
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisableHeifConvert tests if heif-convert is disabled for HEIF conversion.
|
// DisableHeifConvert checks if heif-convert is disabled for HEIF conversion.
|
||||||
func (c *Config) DisableHeifConvert() bool {
|
func (c *Config) DisableHeifConvert() bool {
|
||||||
return c.options.DisableHeifConvert || c.HeifConvertBin() == ""
|
if c.options.DisableHeifConvert {
|
||||||
|
return true
|
||||||
|
} else if c.HeifConvertBin() == "" {
|
||||||
|
c.options.DisableHeifConvert = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.options.DisableHeifConvert
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,3 +51,64 @@ func TestConfig_DisableClassification(t *testing.T) {
|
|||||||
c.options.DisableTensorFlow = false
|
c.options.DisableTensorFlow = false
|
||||||
assert.False(t, c.DisableClassification())
|
assert.False(t, c.DisableClassification())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfig_DisableRaw(t *testing.T) {
|
||||||
|
c := NewConfig(CliTestContext())
|
||||||
|
|
||||||
|
assert.False(t, c.DisableRaw())
|
||||||
|
c.options.DisableRaw = true
|
||||||
|
assert.True(t, c.DisableRaw())
|
||||||
|
assert.True(t, c.DisableDarktable())
|
||||||
|
assert.True(t, c.DisableRawtherapee())
|
||||||
|
c.options.DisableRaw = false
|
||||||
|
assert.False(t, c.DisableRaw())
|
||||||
|
c.options.DisableDarktable = true
|
||||||
|
c.options.DisableRawtherapee = true
|
||||||
|
assert.False(t, c.DisableRaw())
|
||||||
|
c.options.DisableDarktable = false
|
||||||
|
c.options.DisableRawtherapee = false
|
||||||
|
assert.False(t, c.DisableRaw())
|
||||||
|
assert.False(t, c.DisableDarktable())
|
||||||
|
assert.False(t, c.DisableRawtherapee())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_DisableDarktable(t *testing.T) {
|
||||||
|
c := NewConfig(CliTestContext())
|
||||||
|
missing := c.DarktableBin() == ""
|
||||||
|
|
||||||
|
assert.Equal(t, missing, c.DisableDarktable())
|
||||||
|
c.options.DisableRaw = true
|
||||||
|
assert.True(t, c.DisableDarktable())
|
||||||
|
c.options.DisableRaw = false
|
||||||
|
assert.Equal(t, missing, c.DisableDarktable())
|
||||||
|
c.options.DisableDarktable = true
|
||||||
|
assert.True(t, c.DisableDarktable())
|
||||||
|
c.options.DisableDarktable = false
|
||||||
|
assert.Equal(t, missing, c.DisableDarktable())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_DisableRawtherapee(t *testing.T) {
|
||||||
|
c := NewConfig(CliTestContext())
|
||||||
|
missing := c.RawtherapeeBin() == ""
|
||||||
|
|
||||||
|
assert.Equal(t, missing, c.DisableRawtherapee())
|
||||||
|
c.options.DisableRaw = true
|
||||||
|
assert.True(t, c.DisableRawtherapee())
|
||||||
|
c.options.DisableRaw = false
|
||||||
|
assert.Equal(t, missing, c.DisableRawtherapee())
|
||||||
|
c.options.DisableRawtherapee = true
|
||||||
|
assert.True(t, c.DisableRawtherapee())
|
||||||
|
c.options.DisableRawtherapee = false
|
||||||
|
assert.Equal(t, missing, c.DisableRawtherapee())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_DisableSips(t *testing.T) {
|
||||||
|
c := NewConfig(CliTestContext())
|
||||||
|
missing := c.SipsBin() == ""
|
||||||
|
|
||||||
|
assert.Equal(t, missing, c.DisableSips())
|
||||||
|
c.options.DisableSips = true
|
||||||
|
assert.True(t, c.DisableSips())
|
||||||
|
c.options.DisableSips = false
|
||||||
|
assert.Equal(t, missing, c.DisableSips())
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
|
import "github.com/photoprism/photoprism/internal/ffmpeg"
|
||||||
|
|
||||||
// FFmpegBin returns the ffmpeg executable file name.
|
// FFmpegBin returns the ffmpeg executable file name.
|
||||||
func (c *Config) FFmpegBin() string {
|
func (c *Config) FFmpegBin() string {
|
||||||
return findExecutable(c.options.FFmpegBin, "ffmpeg")
|
return findExecutable(c.options.FFmpegBin, "ffmpeg")
|
||||||
}
|
}
|
||||||
|
|
||||||
// FFmpegEnabled tests if FFmpeg is enabled for video transcoding.
|
// FFmpegEnabled checks if FFmpeg is enabled for video transcoding.
|
||||||
func (c *Config) FFmpegEnabled() bool {
|
func (c *Config) FFmpegEnabled() bool {
|
||||||
return !c.DisableFFmpeg()
|
return !c.DisableFFmpeg()
|
||||||
}
|
}
|
||||||
|
|
||||||
// FFmpegEncoder returns the ffmpeg AVC encoder name.
|
// FFmpegEncoder returns the FFmpeg AVC encoder name.
|
||||||
func (c *Config) FFmpegEncoder() string {
|
func (c *Config) FFmpegEncoder() ffmpeg.AvcEncoder {
|
||||||
if c.options.FFmpegEncoder == "" {
|
return ffmpeg.FindEncoder(c.options.FFmpegEncoder)
|
||||||
return "libx264"
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.options.FFmpegEncoder
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FFmpegBitrate returns the ffmpeg bitrate limit in MBit/s.
|
// FFmpegBitrate returns the ffmpeg bitrate limit in MBit/s.
|
||||||
|
|||||||
@@ -3,15 +3,22 @@ package config
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/ffmpeg"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConfig_FFmpegEncoder(t *testing.T) {
|
func TestConfig_FFmpegEncoder(t *testing.T) {
|
||||||
c := NewConfig(CliTestContext())
|
c := NewConfig(CliTestContext())
|
||||||
assert.Equal(t, "libx264", c.FFmpegEncoder())
|
assert.Equal(t, ffmpeg.SoftwareEncoder, c.FFmpegEncoder())
|
||||||
|
c.options.FFmpegEncoder = "nvidia"
|
||||||
c.options.FFmpegEncoder = "testEncoder"
|
assert.Equal(t, ffmpeg.NvidiaEncoder, c.FFmpegEncoder())
|
||||||
assert.Equal(t, "testEncoder", c.FFmpegEncoder())
|
c.options.FFmpegEncoder = "intel"
|
||||||
|
assert.Equal(t, ffmpeg.IntelEncoder, c.FFmpegEncoder())
|
||||||
|
c.options.FFmpegEncoder = "xxx"
|
||||||
|
assert.Equal(t, ffmpeg.SoftwareEncoder, c.FFmpegEncoder())
|
||||||
|
c.options.FFmpegEncoder = ""
|
||||||
|
assert.Equal(t, ffmpeg.SoftwareEncoder, c.FFmpegEncoder())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfig_FFmpegEnabled(t *testing.T) {
|
func TestConfig_FFmpegEnabled(t *testing.T) {
|
||||||
|
|||||||
@@ -174,29 +174,9 @@ var GlobalFlags = []cli.Flag{
|
|||||||
},
|
},
|
||||||
cli.BoolFlag{
|
cli.BoolFlag{
|
||||||
Name: "disable-backups",
|
Name: "disable-backups",
|
||||||
Usage: "disable creating YAML metadata files",
|
Usage: "disable backing up albums and photo metadata to YAML files",
|
||||||
EnvVar: "PHOTOPRISM_DISABLE_BACKUPS",
|
EnvVar: "PHOTOPRISM_DISABLE_BACKUPS",
|
||||||
},
|
},
|
||||||
cli.BoolFlag{
|
|
||||||
Name: "disable-darktable",
|
|
||||||
Usage: "disable converting RAW files with Darktable",
|
|
||||||
EnvVar: "PHOTOPRISM_DISABLE_DARKTABLE",
|
|
||||||
},
|
|
||||||
cli.BoolFlag{
|
|
||||||
Name: "disable-rawtherapee",
|
|
||||||
Usage: "disable converting RAW files with RawTherapee",
|
|
||||||
EnvVar: "PHOTOPRISM_DISABLE_RAWTHERAPEE",
|
|
||||||
},
|
|
||||||
cli.BoolFlag{
|
|
||||||
Name: "disable-sips",
|
|
||||||
Usage: "disable converting RAW files with Sips (macOS only)",
|
|
||||||
EnvVar: "PHOTOPRISM_DISABLE_SIPS",
|
|
||||||
},
|
|
||||||
cli.BoolFlag{
|
|
||||||
Name: "disable-heifconvert",
|
|
||||||
Usage: "disable converting HEIC/HEIF files",
|
|
||||||
EnvVar: "PHOTOPRISM_DISABLE_HEIFCONVERT",
|
|
||||||
},
|
|
||||||
cli.BoolFlag{
|
cli.BoolFlag{
|
||||||
Name: "disable-tensorflow",
|
Name: "disable-tensorflow",
|
||||||
Usage: "disable all features depending on TensorFlow",
|
Usage: "disable all features depending on TensorFlow",
|
||||||
@@ -223,15 +203,40 @@ var GlobalFlags = []cli.Flag{
|
|||||||
EnvVar: "PHOTOPRISM_DISABLE_EXIFTOOL",
|
EnvVar: "PHOTOPRISM_DISABLE_EXIFTOOL",
|
||||||
},
|
},
|
||||||
cli.BoolFlag{
|
cli.BoolFlag{
|
||||||
Name: "exif-bruteforce",
|
Name: "disable-heifconvert",
|
||||||
Usage: "always perform a brute-force search if no Exif headers were found",
|
Usage: "disable conversion of HEIC/HEIF files",
|
||||||
EnvVar: "PHOTOPRISM_EXIF_BRUTEFORCE",
|
EnvVar: "PHOTOPRISM_DISABLE_HEIFCONVERT",
|
||||||
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "disable-darktable",
|
||||||
|
Usage: "disable conversion of RAW files with Darktable",
|
||||||
|
EnvVar: "PHOTOPRISM_DISABLE_DARKTABLE",
|
||||||
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "disable-rawtherapee",
|
||||||
|
Usage: "disable conversion of RAW files with RawTherapee",
|
||||||
|
EnvVar: "PHOTOPRISM_DISABLE_RAWTHERAPEE",
|
||||||
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "disable-sips",
|
||||||
|
Usage: "disable conversion of RAW files with Sips (macOS only)",
|
||||||
|
EnvVar: "PHOTOPRISM_DISABLE_SIPS",
|
||||||
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "disable-raw",
|
||||||
|
Usage: "disable indexing and conversion of RAW files",
|
||||||
|
EnvVar: "PHOTOPRISM_DISABLE_RAW",
|
||||||
},
|
},
|
||||||
cli.BoolFlag{
|
cli.BoolFlag{
|
||||||
Name: "raw-presets",
|
Name: "raw-presets",
|
||||||
Usage: "enable RAW file converter presets (may reduce performance)",
|
Usage: "enables applying user presets when converting RAW files (reduces performance)",
|
||||||
EnvVar: "PHOTOPRISM_RAW_PRESETS",
|
EnvVar: "PHOTOPRISM_RAW_PRESETS",
|
||||||
},
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "exif-bruteforce",
|
||||||
|
Usage: "always perform a brute-force search if no Exif headers were found",
|
||||||
|
EnvVar: "PHOTOPRISM_EXIF_BRUTEFORCE",
|
||||||
|
},
|
||||||
cli.BoolFlag{
|
cli.BoolFlag{
|
||||||
Name: "detect-nsfw",
|
Name: "detect-nsfw",
|
||||||
Usage: "flag photos as private that may be offensive (requires TensorFlow)",
|
Usage: "flag photos as private that may be offensive (requires TensorFlow)",
|
||||||
@@ -397,6 +402,18 @@ var GlobalFlags = []cli.Flag{
|
|||||||
Value: "dng,cr3",
|
Value: "dng,cr3",
|
||||||
EnvVar: "PHOTOPRISM_DARKTABLE_BLACKLIST",
|
EnvVar: "PHOTOPRISM_DARKTABLE_BLACKLIST",
|
||||||
},
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "darktable-cache-path",
|
||||||
|
Usage: "custom Darktable cache `PATH` (automatically created if empty)",
|
||||||
|
Value: "",
|
||||||
|
EnvVar: "PHOTOPRISM_DARKTABLE_CACHE_PATH",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "darktable-config-path",
|
||||||
|
Usage: "custom Darktable config `PATH` (automatically created if empty)",
|
||||||
|
Value: "",
|
||||||
|
EnvVar: "PHOTOPRISM_DARKTABLE_CONFIG_PATH",
|
||||||
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "rawtherapee-bin",
|
Name: "rawtherapee-bin",
|
||||||
Usage: "RawTherapee CLI `COMMAND` for RAW to JPEG conversion",
|
Usage: "RawTherapee CLI `COMMAND` for RAW to JPEG conversion",
|
||||||
@@ -417,7 +434,7 @@ var GlobalFlags = []cli.Flag{
|
|||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "heifconvert-bin",
|
Name: "heifconvert-bin",
|
||||||
Usage: "HEIC/HEIF image convert `COMMAND`",
|
Usage: "HEIC/HEIF image conversion `COMMAND`",
|
||||||
Value: "heif-convert",
|
Value: "heif-convert",
|
||||||
EnvVar: "PHOTOPRISM_HEIFCONVERT_BIN",
|
EnvVar: "PHOTOPRISM_HEIFCONVERT_BIN",
|
||||||
},
|
},
|
||||||
@@ -463,7 +480,7 @@ var GlobalFlags = []cli.Flag{
|
|||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "thumb-colorspace",
|
Name: "thumb-colorspace",
|
||||||
Usage: "convert Apple Display P3 colors in thumbnails to standard color space (\"\" to disable)",
|
Usage: "standard colorspace for thumbnails (\"\" to disable)",
|
||||||
Value: "sRGB",
|
Value: "sRGB",
|
||||||
EnvVar: "PHOTOPRISM_THUMB_COLORSPACE",
|
EnvVar: "PHOTOPRISM_THUMB_COLORSPACE",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,31 +11,46 @@ import (
|
|||||||
"github.com/photoprism/photoprism/pkg/sanitize"
|
"github.com/photoprism/photoprism/pkg/sanitize"
|
||||||
)
|
)
|
||||||
|
|
||||||
func findExecutable(configBin, defaultBin string) (result string) {
|
// binPaths stores known executable paths.
|
||||||
|
var binPaths = make(map[string]string, 8)
|
||||||
|
|
||||||
|
// findExecutable searches binaries by their name.
|
||||||
|
func findExecutable(configBin, defaultBin string) (binPath string) {
|
||||||
|
// Cached?
|
||||||
|
cacheKey := defaultBin + configBin
|
||||||
|
if cached, ok := binPaths[cacheKey]; ok {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default if config value is empty.
|
||||||
if configBin == "" {
|
if configBin == "" {
|
||||||
result = defaultBin
|
binPath = defaultBin
|
||||||
} else {
|
} else {
|
||||||
result = configBin
|
binPath = configBin
|
||||||
}
|
}
|
||||||
|
|
||||||
if path, err := exec.LookPath(result); err == nil {
|
// Search.
|
||||||
result = path
|
if path, err := exec.LookPath(binPath); err == nil {
|
||||||
|
binPath = path
|
||||||
}
|
}
|
||||||
|
|
||||||
if !fs.FileExists(result) {
|
// Exists?
|
||||||
result = ""
|
if !fs.FileExists(binPath) {
|
||||||
|
binPath = ""
|
||||||
|
} else {
|
||||||
|
binPaths[cacheKey] = binPath
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return binPath
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateDirectories creates directories for storing photos, metadata and cache files.
|
// CreateDirectories creates directories for storing photos, metadata and cache files.
|
||||||
func (c *Config) CreateDirectories() error {
|
func (c *Config) CreateDirectories() error {
|
||||||
createError := func(path string, err error) (result error) {
|
createError := func(path string, err error) (result error) {
|
||||||
if fs.FileExists(path) {
|
if fs.FileExists(path) {
|
||||||
result = fmt.Errorf("%s is a file, not a folder: please check your configuration", sanitize.Log(path))
|
result = fmt.Errorf("directory path %s is a file, please check your configuration", sanitize.Log(path))
|
||||||
} else {
|
} else {
|
||||||
result = fmt.Errorf("cannot create %s, check config and permissions", sanitize.Log(path))
|
result = fmt.Errorf("failed to create the directory %s, check configuration and permissions", sanitize.Log(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug(err)
|
log.Debug(err)
|
||||||
@@ -44,7 +59,7 @@ func (c *Config) CreateDirectories() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
notFoundError := func(name string) error {
|
notFoundError := func(name string) error {
|
||||||
return fmt.Errorf("%s path not found, run 'photoprism config' to check configuration options", name)
|
return fmt.Errorf("invalid %s path, check configuration and permissions", sanitize.Log(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.AssetsPath() == "" {
|
if c.AssetsPath() == "" {
|
||||||
@@ -137,6 +152,16 @@ func (c *Config) CreateDirectories() error {
|
|||||||
return createError(filepath.Dir(c.LogFilename()), err)
|
return createError(filepath.Dir(c.LogFilename()), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.DarktableEnabled() {
|
||||||
|
if cachePath, err := c.CreateDarktableCachePath(); err != nil {
|
||||||
|
return fmt.Errorf("could not create darktable cache path %s", sanitize.Log(cachePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
if configPath, err := c.CreateDarktableConfigPath(); err != nil {
|
||||||
|
return fmt.Errorf("could not create darktable cache path %s", sanitize.Log(configPath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +215,7 @@ func (c *Config) LogFilename() string {
|
|||||||
return fs.Abs(c.options.LogFilename)
|
return fs.Abs(c.options.LogFilename)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CaseInsensitive tests if the storage path is case-insensitive.
|
// CaseInsensitive checks if the storage path is case-insensitive.
|
||||||
func (c *Config) CaseInsensitive() (result bool, err error) {
|
func (c *Config) CaseInsensitive() (result bool, err error) {
|
||||||
storagePath := c.StoragePath()
|
storagePath := c.StoragePath()
|
||||||
return fs.CaseInsensitive(storagePath)
|
return fs.CaseInsensitive(storagePath)
|
||||||
@@ -225,12 +250,12 @@ func (c *Config) SidecarPath() string {
|
|||||||
return c.options.SidecarPath
|
return c.options.SidecarPath
|
||||||
}
|
}
|
||||||
|
|
||||||
// SidecarPathIsAbs tests if sidecar path is absolute.
|
// SidecarPathIsAbs checks if sidecar path is absolute.
|
||||||
func (c *Config) SidecarPathIsAbs() bool {
|
func (c *Config) SidecarPathIsAbs() bool {
|
||||||
return filepath.IsAbs(c.SidecarPath())
|
return filepath.IsAbs(c.SidecarPath())
|
||||||
}
|
}
|
||||||
|
|
||||||
// SidecarWritable tests if sidecar files can be created.
|
// SidecarWritable checks if sidecar files can be created.
|
||||||
func (c *Config) SidecarWritable() bool {
|
func (c *Config) SidecarWritable() bool {
|
||||||
return !c.ReadOnly() || c.SidecarPathIsAbs()
|
return !c.ReadOnly() || c.SidecarPathIsAbs()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ func (c *Config) ExifToolBin() string {
|
|||||||
return findExecutable(c.options.ExifToolBin, "exiftool")
|
return findExecutable(c.options.ExifToolBin, "exiftool")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExifToolJson tests if creating JSON metadata sidecar files with Exiftool is enabled.
|
// ExifToolJson checks if creating JSON metadata sidecar files with Exiftool is enabled.
|
||||||
func (c *Config) ExifToolJson() bool {
|
func (c *Config) ExifToolJson() bool {
|
||||||
return !c.DisableExifTool()
|
return !c.DisableExifTool()
|
||||||
}
|
}
|
||||||
|
|
||||||
// BackupYaml tests if creating YAML files is enabled.
|
// BackupYaml checks if creating YAML files is enabled.
|
||||||
func (c *Config) BackupYaml() bool {
|
func (c *Config) BackupYaml() bool {
|
||||||
return !c.DisableBackups()
|
return !c.DisableBackups()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,17 +71,18 @@ type Options struct {
|
|||||||
DisableBackups bool `yaml:"DisableBackups" json:"DisableBackups" flag:"disable-backups"`
|
DisableBackups bool `yaml:"DisableBackups" json:"DisableBackups" flag:"disable-backups"`
|
||||||
DisableSettings bool `yaml:"DisableSettings" json:"-" flag:"disable-settings"`
|
DisableSettings bool `yaml:"DisableSettings" json:"-" flag:"disable-settings"`
|
||||||
DisablePlaces bool `yaml:"DisablePlaces" json:"DisablePlaces" flag:"disable-places"`
|
DisablePlaces bool `yaml:"DisablePlaces" json:"DisablePlaces" flag:"disable-places"`
|
||||||
DisableDarktable bool `yaml:"DisableDarktable" json:"DisableDarktable" flag:"disable-darktable"`
|
|
||||||
DisableRawtherapee bool `yaml:"DisableRawtherapee" json:"DisableRawtherapee" flag:"disable-rawtherapee"`
|
|
||||||
DisableSips bool `yaml:"DisableSips" json:"DisableSips" flag:"disable-sips"`
|
|
||||||
DisableHeifConvert bool `yaml:"DisableHeifConvert" json:"DisableHeifConvert" flag:"disable-heifconvert"`
|
|
||||||
DisableTensorFlow bool `yaml:"DisableTensorFlow" json:"DisableTensorFlow" flag:"disable-tensorflow"`
|
DisableTensorFlow bool `yaml:"DisableTensorFlow" json:"DisableTensorFlow" flag:"disable-tensorflow"`
|
||||||
DisableFaces bool `yaml:"DisableFaces" json:"DisableFaces" flag:"disable-faces"`
|
DisableFaces bool `yaml:"DisableFaces" json:"DisableFaces" flag:"disable-faces"`
|
||||||
DisableClassification bool `yaml:"DisableClassification" json:"DisableClassification" flag:"disable-classification"`
|
DisableClassification bool `yaml:"DisableClassification" json:"DisableClassification" flag:"disable-classification"`
|
||||||
DisableFFmpeg bool `yaml:"DisableFFmpeg" json:"DisableFFmpeg" flag:"disable-ffmpeg"`
|
DisableFFmpeg bool `yaml:"DisableFFmpeg" json:"DisableFFmpeg" flag:"disable-ffmpeg"`
|
||||||
DisableExifTool bool `yaml:"DisableExifTool" json:"DisableExifTool" flag:"disable-exiftool"`
|
DisableExifTool bool `yaml:"DisableExifTool" json:"DisableExifTool" flag:"disable-exiftool"`
|
||||||
ExifBruteForce bool `yaml:"ExifBruteForce" json:"ExifBruteForce" flag:"exif-bruteforce"`
|
DisableHeifConvert bool `yaml:"DisableHeifConvert" json:"DisableHeifConvert" flag:"disable-heifconvert"`
|
||||||
|
DisableDarktable bool `yaml:"DisableDarktable" json:"DisableDarktable" flag:"disable-darktable"`
|
||||||
|
DisableRawtherapee bool `yaml:"DisableRawtherapee" json:"DisableRawtherapee" flag:"disable-rawtherapee"`
|
||||||
|
DisableSips bool `yaml:"DisableSips" json:"DisableSips" flag:"disable-sips"`
|
||||||
|
DisableRaw bool `yaml:"DisableRaw" json:"DisableRaw" flag:"disable-raw"`
|
||||||
RawPresets bool `yaml:"RawPresets" json:"RawPresets" flag:"raw-presets"`
|
RawPresets bool `yaml:"RawPresets" json:"RawPresets" flag:"raw-presets"`
|
||||||
|
ExifBruteForce bool `yaml:"ExifBruteForce" json:"ExifBruteForce" flag:"exif-bruteforce"`
|
||||||
DetectNSFW bool `yaml:"DetectNSFW" json:"DetectNSFW" flag:"detect-nsfw"`
|
DetectNSFW bool `yaml:"DetectNSFW" json:"DetectNSFW" flag:"detect-nsfw"`
|
||||||
UploadNSFW bool `yaml:"UploadNSFW" json:"-" flag:"upload-nsfw"`
|
UploadNSFW bool `yaml:"UploadNSFW" json:"-" flag:"upload-nsfw"`
|
||||||
DefaultTheme string `yaml:"DefaultTheme" json:"DefaultTheme" flag:"default-theme"`
|
DefaultTheme string `yaml:"DefaultTheme" json:"DefaultTheme" flag:"default-theme"`
|
||||||
@@ -111,6 +112,8 @@ type Options struct {
|
|||||||
HttpMode string `yaml:"HttpMode" json:"-" flag:"http-mode"`
|
HttpMode string `yaml:"HttpMode" json:"-" flag:"http-mode"`
|
||||||
HttpCompression string `yaml:"HttpCompression" json:"-" flag:"http-compression"`
|
HttpCompression string `yaml:"HttpCompression" json:"-" flag:"http-compression"`
|
||||||
DarktableBin string `yaml:"DarktableBin" json:"-" flag:"darktable-bin"`
|
DarktableBin string `yaml:"DarktableBin" json:"-" flag:"darktable-bin"`
|
||||||
|
DarktableCachePath string `yaml:"DarktableCachePath" json:"-" flag:"darktable-cache-path"`
|
||||||
|
DarktableConfigPath string `yaml:"DarktableConfigPath" json:"-" flag:"darktable-config-path"`
|
||||||
DarktableBlacklist string `yaml:"DarktableBlacklist" json:"-" flag:"darktable-blacklist"`
|
DarktableBlacklist string `yaml:"DarktableBlacklist" json:"-" flag:"darktable-blacklist"`
|
||||||
RawtherapeeBin string `yaml:"RawtherapeeBin" json:"-" flag:"rawtherapee-bin"`
|
RawtherapeeBin string `yaml:"RawtherapeeBin" json:"-" flag:"rawtherapee-bin"`
|
||||||
RawtherapeeBlacklist string `yaml:"RawtherapeeBlacklist" json:"-" flag:"rawtherapee-blacklist"`
|
RawtherapeeBlacklist string `yaml:"RawtherapeeBlacklist" json:"-" flag:"rawtherapee-blacklist"`
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ package config
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/pkg/sanitize"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// RawPresets tests if RAW converter presents should be used (may reduce performance).
|
// RawEnabled checks if indexing and conversion of RAW files is enabled.
|
||||||
|
func (c *Config) RawEnabled() bool {
|
||||||
|
return !c.DisableRaw()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RawPresets checks if RAW converter presents should be used (may reduce performance).
|
||||||
func (c *Config) RawPresets() bool {
|
func (c *Config) RawPresets() bool {
|
||||||
return c.options.RawPresets
|
return c.options.RawPresets
|
||||||
}
|
}
|
||||||
@@ -24,27 +27,49 @@ func (c *Config) DarktableBlacklist() string {
|
|||||||
|
|
||||||
// DarktableConfigPath returns the darktable config directory.
|
// DarktableConfigPath returns the darktable config directory.
|
||||||
func (c *Config) DarktableConfigPath() string {
|
func (c *Config) DarktableConfigPath() string {
|
||||||
dir := filepath.Join(c.ConfigPath(), "darktable")
|
if c.options.DarktableConfigPath != "" {
|
||||||
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
|
return c.options.DarktableConfigPath
|
||||||
log.Errorf("darktable: cannot create config directory %s, check permissions", sanitize.Log(dir))
|
|
||||||
return c.ConfigPath()
|
|
||||||
}
|
}
|
||||||
return dir
|
|
||||||
|
return filepath.Join(c.ConfigPath(), "darktable")
|
||||||
}
|
}
|
||||||
|
|
||||||
// DarktableCachePath returns the darktable cache directory.
|
// DarktableCachePath returns the darktable cache directory.
|
||||||
func (c *Config) DarktableCachePath() string {
|
func (c *Config) DarktableCachePath() string {
|
||||||
dir := filepath.Join(c.CachePath(), "darktable")
|
if c.options.DarktableCachePath != "" {
|
||||||
|
return c.options.DarktableCachePath
|
||||||
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
|
|
||||||
log.Errorf("darktable: cannot create cache directory %s, check permissions", sanitize.Log(dir))
|
|
||||||
return c.ConfigPath()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return dir
|
return filepath.Join(c.CachePath(), "darktable")
|
||||||
}
|
}
|
||||||
|
|
||||||
// DarktableEnabled tests if Darktable is enabled for RAW conversion.
|
// CreateDarktableCachePath creates and returns the darktable cache directory.
|
||||||
|
func (c *Config) CreateDarktableCachePath() (string, error) {
|
||||||
|
cachePath := c.DarktableCachePath()
|
||||||
|
|
||||||
|
if err := os.MkdirAll(cachePath, os.ModePerm); err != nil {
|
||||||
|
return cachePath, err
|
||||||
|
} else {
|
||||||
|
c.options.DarktableCachePath = cachePath
|
||||||
|
}
|
||||||
|
|
||||||
|
return cachePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateDarktableConfigPath creates and returns the darktable config directory.
|
||||||
|
func (c *Config) CreateDarktableConfigPath() (string, error) {
|
||||||
|
configPath := c.DarktableConfigPath()
|
||||||
|
|
||||||
|
if err := os.MkdirAll(configPath, os.ModePerm); err != nil {
|
||||||
|
return configPath, err
|
||||||
|
} else {
|
||||||
|
c.options.DarktableConfigPath = configPath
|
||||||
|
}
|
||||||
|
|
||||||
|
return configPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DarktableEnabled checks if Darktable is enabled for RAW conversion.
|
||||||
func (c *Config) DarktableEnabled() bool {
|
func (c *Config) DarktableEnabled() bool {
|
||||||
return !c.DisableDarktable()
|
return !c.DisableDarktable()
|
||||||
}
|
}
|
||||||
@@ -59,12 +84,12 @@ func (c *Config) RawtherapeeBlacklist() string {
|
|||||||
return c.options.RawtherapeeBlacklist
|
return c.options.RawtherapeeBlacklist
|
||||||
}
|
}
|
||||||
|
|
||||||
// RawtherapeeEnabled tests if Rawtherapee is enabled for RAW conversion.
|
// RawtherapeeEnabled checks if Rawtherapee is enabled for RAW conversion.
|
||||||
func (c *Config) RawtherapeeEnabled() bool {
|
func (c *Config) RawtherapeeEnabled() bool {
|
||||||
return !c.DisableRawtherapee()
|
return !c.DisableRawtherapee()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SipsEnabled tests if SIPS is enabled for RAW conversion.
|
// SipsEnabled checks if SIPS is enabled for RAW conversion.
|
||||||
func (c *Config) SipsEnabled() bool {
|
func (c *Config) SipsEnabled() bool {
|
||||||
return !c.DisableSips()
|
return !c.DisableSips()
|
||||||
}
|
}
|
||||||
@@ -79,7 +104,7 @@ func (c *Config) HeifConvertBin() string {
|
|||||||
return findExecutable(c.options.HeifConvertBin, "heif-convert")
|
return findExecutable(c.options.HeifConvertBin, "heif-convert")
|
||||||
}
|
}
|
||||||
|
|
||||||
// HeifConvertEnabled tests if heif-convert is enabled for HEIF conversion.
|
// HeifConvertEnabled checks if heif-convert is enabled for HEIF conversion.
|
||||||
func (c *Config) HeifConvertEnabled() bool {
|
func (c *Config) HeifConvertEnabled() bool {
|
||||||
return !c.DisableHeifConvert()
|
return !c.DisableHeifConvert()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestConfig_RawEnabled(t *testing.T) {
|
||||||
|
c := NewConfig(CliTestContext())
|
||||||
|
|
||||||
|
assert.NotEqual(t, c.DisableRaw(), c.RawEnabled())
|
||||||
|
}
|
||||||
|
|
||||||
func TestConfig_RawtherapeeBin(t *testing.T) {
|
func TestConfig_RawtherapeeBin(t *testing.T) {
|
||||||
c := NewConfig(CliTestContext())
|
c := NewConfig(CliTestContext())
|
||||||
|
|
||||||
@@ -64,8 +70,7 @@ func TestConfig_SipsBin(t *testing.T) {
|
|||||||
|
|
||||||
func TestConfig_SipsEnabled(t *testing.T) {
|
func TestConfig_SipsEnabled(t *testing.T) {
|
||||||
c := NewConfig(CliTestContext())
|
c := NewConfig(CliTestContext())
|
||||||
c.options.DisableSips = true
|
assert.NotEqual(t, c.DisableSips(), c.SipsEnabled())
|
||||||
assert.False(t, c.SipsEnabled())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfig_HeifConvertBin(t *testing.T) {
|
func TestConfig_HeifConvertBin(t *testing.T) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/photoprism/photoprism/pkg/fs"
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DetachServer tests if server should detach from console (daemon mode).
|
// DetachServer checks if server should detach from console (daemon mode).
|
||||||
func (c *Config) DetachServer() bool {
|
func (c *Config) DetachServer() bool {
|
||||||
return c.options.DetachServer
|
return c.options.DetachServer
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,7 @@ func (c *Config) TemplatesPath() string {
|
|||||||
return filepath.Join(c.AssetsPath(), "templates")
|
return filepath.Join(c.AssetsPath(), "templates")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TemplateExists tests if a template with the given name exists (e.g. index.tmpl).
|
// TemplateExists checks if a template with the given name exists (e.g. index.tmpl).
|
||||||
func (c *Config) TemplateExists(name string) bool {
|
func (c *Config) TemplateExists(name string) bool {
|
||||||
return fs.FileExists(filepath.Join(c.TemplatesPath(), name))
|
return fs.FileExists(filepath.Join(c.TemplatesPath(), name))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/entity"
|
"gopkg.in/yaml.v2"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
"github.com/photoprism/photoprism/internal/i18n"
|
"github.com/photoprism/photoprism/internal/i18n"
|
||||||
"github.com/photoprism/photoprism/pkg/fs"
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
"github.com/photoprism/photoprism/pkg/sanitize"
|
"github.com/photoprism/photoprism/pkg/sanitize"
|
||||||
"gopkg.in/yaml.v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// UISettings represents user interface settings.
|
// UISettings represents user interface settings.
|
||||||
@@ -172,17 +172,17 @@ func (s *Settings) Propagate() {
|
|||||||
i18n.SetLocale(s.UI.Language)
|
i18n.SetLocale(s.UI.Language)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StackSequences tests if files should be stacked based on their file name prefix (sequential names).
|
// StackSequences checks if files should be stacked based on their file name prefix (sequential names).
|
||||||
func (s Settings) StackSequences() bool {
|
func (s Settings) StackSequences() bool {
|
||||||
return s.Stack.Name
|
return s.Stack.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
// StackUUID tests if files should be stacked based on unique image or instance id.
|
// StackUUID checks if files should be stacked based on unique image or instance id.
|
||||||
func (s Settings) StackUUID() bool {
|
func (s Settings) StackUUID() bool {
|
||||||
return s.Stack.UUID
|
return s.Stack.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
// StackMeta tests if files should be stacked based on their place and time metadata.
|
// StackMeta checks if files should be stacked based on their place and time metadata.
|
||||||
func (s Settings) StackMeta() bool {
|
func (s Settings) StackMeta() bool {
|
||||||
return s.Stack.Meta
|
return s.Stack.Meta
|
||||||
}
|
}
|
||||||
@@ -235,11 +235,11 @@ func (c *Config) initSettings() {
|
|||||||
fileName := c.SettingsFile()
|
fileName := c.SettingsFile()
|
||||||
|
|
||||||
if err := c.settings.Load(fileName); err == nil {
|
if err := c.settings.Load(fileName); err == nil {
|
||||||
log.Debugf("config: settings loaded from %s ", fileName)
|
log.Debugf("settings: loaded from %s ", fileName)
|
||||||
} else if err := c.settings.Save(fileName); err != nil {
|
} else if err := c.settings.Save(fileName); err != nil {
|
||||||
log.Errorf("failed creating %s: %s", fileName, err)
|
log.Errorf("settings: could not create %s (%s)", fileName, err)
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("config: created %s ", fileName)
|
log.Debugf("settings: saved to %s ", fileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
i18n.SetDir(c.LocalesPath())
|
i18n.SetDir(c.LocalesPath())
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ func FirstOrCreateCell(m *Cell) *Cell {
|
|||||||
// Keywords returns search keywords for a location.
|
// Keywords returns search keywords for a location.
|
||||||
func (m *Cell) Keywords() (result []string) {
|
func (m *Cell) Keywords() (result []string) {
|
||||||
if m.Place == nil {
|
if m.Place == nil {
|
||||||
log.Errorf("cell: info for %s is nil - you might have found a bug", m.ID)
|
log.Errorf("cell: info for %s is nil - possible bug", m.ID)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func Update(m interface{}, keyNames ...string) (err error) {
|
|||||||
if res.Error != nil {
|
if res.Error != nil {
|
||||||
return err
|
return err
|
||||||
} else if res.RowsAffected > 1 {
|
} else if res.RowsAffected > 1 {
|
||||||
log.Debugf("entity: updated statement affected more than one record - bug?")
|
log.Debugf("entity: updated statement affected more than one record - possible bug")
|
||||||
return nil
|
return nil
|
||||||
} else if res.RowsAffected == 1 {
|
} else if res.RowsAffected == 1 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ func (m *Marker) BeforeCreate(scope *gorm.Scope) error {
|
|||||||
// NewMarker creates a new entity.
|
// NewMarker creates a new entity.
|
||||||
func NewMarker(file File, area crop.Area, subjUID, markerSrc, markerType string, size, score int) *Marker {
|
func NewMarker(file File, area crop.Area, subjUID, markerSrc, markerType string, size, score int) *Marker {
|
||||||
if file.FileHash == "" {
|
if file.FileHash == "" {
|
||||||
log.Errorf("markers: file hash is empty - you might have found a bug")
|
log.Errorf("markers: file hash is empty - possible bug")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -442,7 +442,7 @@ func (m *Photo) IndexKeywords() error {
|
|||||||
kw := FirstOrCreateKeyword(NewKeyword(w))
|
kw := FirstOrCreateKeyword(NewKeyword(w))
|
||||||
|
|
||||||
if kw == nil {
|
if kw == nil {
|
||||||
log.Errorf("index keyword should not be nil - bug?")
|
log.Errorf("index keyword should not be nil - possible bug")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,7 +562,7 @@ func (m *Photo) AddLabels(labels classify.Labels) {
|
|||||||
labelEntity := FirstOrCreateLabel(NewLabel(classifyLabel.Title(), classifyLabel.Priority))
|
labelEntity := FirstOrCreateLabel(NewLabel(classifyLabel.Title(), classifyLabel.Priority))
|
||||||
|
|
||||||
if labelEntity == nil {
|
if labelEntity == nil {
|
||||||
log.Errorf("index: label %s should not be nil - bug? (%s)", sanitize.Log(classifyLabel.Title()), m)
|
log.Errorf("index: label %s should not be nil - possible bug (%s)", sanitize.Log(classifyLabel.Title()), m)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,7 +578,7 @@ func (m *Photo) AddLabels(labels classify.Labels) {
|
|||||||
photoLabel := FirstOrCreatePhotoLabel(NewPhotoLabel(m.ID, labelEntity.ID, classifyLabel.Uncertainty, classifyLabel.Source))
|
photoLabel := FirstOrCreatePhotoLabel(NewPhotoLabel(m.ID, labelEntity.ID, classifyLabel.Uncertainty, classifyLabel.Source))
|
||||||
|
|
||||||
if photoLabel == nil {
|
if photoLabel == nil {
|
||||||
log.Errorf("index: photo-label %d should not be nil - bug? (%s)", labelEntity.ID, m)
|
log.Errorf("index: photo-label %d should not be nil - possible bug (%s)", labelEntity.ID, m)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ func FirstOrCreatePhotoLabel(m *PhotoLabel) *PhotoLabel {
|
|||||||
// ClassifyLabel returns the label as classify.Label
|
// ClassifyLabel returns the label as classify.Label
|
||||||
func (m *PhotoLabel) ClassifyLabel() classify.Label {
|
func (m *PhotoLabel) ClassifyLabel() classify.Label {
|
||||||
if m.Label == nil {
|
if m.Label == nil {
|
||||||
log.Errorf("photo-label: classify label is nil (photo id %d, label id %d) - bug?", m.PhotoID, m.LabelID)
|
log.Errorf("photo-label: classify label is nil (photo id %d, label id %d) - possible bug", m.PhotoID, m.LabelID)
|
||||||
return classify.Label{}
|
return classify.Label{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
123
internal/ffmpeg/convert.go
Normal file
123
internal/ffmpeg/convert.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package ffmpeg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AvcConvertCommand returns the command for converting video files to MPEG-4 AVC.
|
||||||
|
func AvcConvertCommand(fileName, avcName, ffmpegBin, bitrate string, encoder AvcEncoder) (result *exec.Cmd, useMutex bool, err error) {
|
||||||
|
// Don't transcode more than one video at the same time.
|
||||||
|
useMutex = true
|
||||||
|
|
||||||
|
encoderName := string(encoder)
|
||||||
|
|
||||||
|
// Display encoder info.
|
||||||
|
if encoder != SoftwareEncoder {
|
||||||
|
log.Infof("convert: ffmpeg encoder %s selected", encoderName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if encoder == IntelEncoder {
|
||||||
|
format := "format=rgb32"
|
||||||
|
|
||||||
|
// Options: ffmpeg -hide_banner -h encoder=h264_qsv
|
||||||
|
result = exec.Command(
|
||||||
|
ffmpegBin,
|
||||||
|
"-qsv_device", "/dev/dri/renderD128",
|
||||||
|
"-i", fileName,
|
||||||
|
"-c:a", "aac",
|
||||||
|
"-vf", format,
|
||||||
|
"-c:v", string(encoder),
|
||||||
|
"-vsync", "vfr",
|
||||||
|
"-r", "30",
|
||||||
|
"-b:v", bitrate,
|
||||||
|
"-itrate", bitrate,
|
||||||
|
"-f", "mp4",
|
||||||
|
"-y",
|
||||||
|
avcName,
|
||||||
|
)
|
||||||
|
} else if encoder == AppleEncoder {
|
||||||
|
format := "format=yuv420p"
|
||||||
|
|
||||||
|
// Options: ffmpeg -hide_banner -h encoder=h264_videotoolbox
|
||||||
|
result = exec.Command(
|
||||||
|
ffmpegBin,
|
||||||
|
"-i", fileName,
|
||||||
|
"-c:v", string(encoder),
|
||||||
|
"-c:a", "aac",
|
||||||
|
"-vf", format,
|
||||||
|
"-profile", "high",
|
||||||
|
"-level", "51",
|
||||||
|
"-vsync", "vfr",
|
||||||
|
"-r", "30",
|
||||||
|
"-b:v", bitrate,
|
||||||
|
"-f", "mp4",
|
||||||
|
"-y",
|
||||||
|
avcName,
|
||||||
|
)
|
||||||
|
} else if encoder == NvidiaEncoder {
|
||||||
|
// Options: ffmpeg -hide_banner -h encoder=h264_nvenc
|
||||||
|
result = exec.Command(
|
||||||
|
ffmpegBin,
|
||||||
|
"-r", "30",
|
||||||
|
"-i", fileName,
|
||||||
|
"-pix_fmt", "yuv420p",
|
||||||
|
"-c:v", string(encoder),
|
||||||
|
"-c:a", "aac",
|
||||||
|
"-preset", "15",
|
||||||
|
"-pixel_format", "yuv420p",
|
||||||
|
"-gpu", "any",
|
||||||
|
"-vf", "format=yuv420p",
|
||||||
|
"-rc:v", "constqp",
|
||||||
|
"-cq", "0",
|
||||||
|
"-tune", "2",
|
||||||
|
"-b:v", bitrate,
|
||||||
|
"-profile:v", "1",
|
||||||
|
"-level:v", "41",
|
||||||
|
"-coder:v", "1",
|
||||||
|
"-f", "mp4",
|
||||||
|
"-y",
|
||||||
|
avcName,
|
||||||
|
)
|
||||||
|
} else if encoder == Video4LinuxEncoder {
|
||||||
|
format := "format=yuv420p"
|
||||||
|
|
||||||
|
// Options: ffmpeg -hide_banner -h encoder=h264_v4l2m2m
|
||||||
|
result = exec.Command(
|
||||||
|
ffmpegBin,
|
||||||
|
"-i", fileName,
|
||||||
|
"-c:v", string(encoder),
|
||||||
|
"-c:a", "aac",
|
||||||
|
"-vf", format,
|
||||||
|
"-num_output_buffers", "72",
|
||||||
|
"-num_capture_buffers", "64",
|
||||||
|
"-max_muxing_queue_size", "1024",
|
||||||
|
"-crf", "23",
|
||||||
|
"-vsync", "vfr",
|
||||||
|
"-r", "30",
|
||||||
|
"-b:v", bitrate,
|
||||||
|
"-f", "mp4",
|
||||||
|
"-y",
|
||||||
|
avcName,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format := "format=yuv420p"
|
||||||
|
|
||||||
|
result = exec.Command(
|
||||||
|
ffmpegBin,
|
||||||
|
"-i", fileName,
|
||||||
|
"-c:v", string(encoder),
|
||||||
|
"-c:a", "aac",
|
||||||
|
"-vf", format,
|
||||||
|
"-max_muxing_queue_size", "1024",
|
||||||
|
"-crf", "23",
|
||||||
|
"-vsync", "vfr",
|
||||||
|
"-r", "30",
|
||||||
|
"-b:v", bitrate,
|
||||||
|
"-f", "mp4",
|
||||||
|
"-y",
|
||||||
|
avcName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, useMutex, nil
|
||||||
|
}
|
||||||
63
internal/ffmpeg/encoders.go
Normal file
63
internal/ffmpeg/encoders.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package ffmpeg
|
||||||
|
|
||||||
|
import "github.com/photoprism/photoprism/pkg/sanitize"
|
||||||
|
|
||||||
|
// AvcEncoder represents a supported FFmpeg AVC encoder name.
|
||||||
|
type AvcEncoder string
|
||||||
|
|
||||||
|
// String returns the FFmpeg AVC encoder name as string.
|
||||||
|
func (name AvcEncoder) String() string {
|
||||||
|
return string(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supported FFmpeg AVC encoders.
|
||||||
|
const (
|
||||||
|
SoftwareEncoder AvcEncoder = "libx264" // SoftwareEncoder see https://trac.ffmpeg.org/wiki/HWAccelIntro.
|
||||||
|
IntelEncoder AvcEncoder = "h264_qsv" // IntelEncoder is the Intel Quick Sync H.264 encoder.
|
||||||
|
AppleEncoder AvcEncoder = "h264_videotoolbox" // AppleEncoder is the Apple Video Toolbox H.264 encoder.
|
||||||
|
VAAPIEncoder AvcEncoder = "h264_vaapi" // VAAPIEncoder is the Video Acceleration API H.264 encoder.
|
||||||
|
NvidiaEncoder AvcEncoder = "h264_nvenc" // NvidiaEncoder is the NVIDIA H.264 encoder.
|
||||||
|
Video4LinuxEncoder AvcEncoder = "h264_v4l2m2m" // Video4LinuxEncoder is the Video4Linux H.264 encoder.
|
||||||
|
)
|
||||||
|
|
||||||
|
// AvcEncoders is the list of supported H.264 encoders with aliases.
|
||||||
|
var AvcEncoders = map[string]AvcEncoder{
|
||||||
|
"": SoftwareEncoder,
|
||||||
|
"default": SoftwareEncoder,
|
||||||
|
"software": SoftwareEncoder,
|
||||||
|
string(SoftwareEncoder): SoftwareEncoder,
|
||||||
|
"intel": IntelEncoder,
|
||||||
|
"qsv": IntelEncoder,
|
||||||
|
string(IntelEncoder): IntelEncoder,
|
||||||
|
"apple": AppleEncoder,
|
||||||
|
"osx": AppleEncoder,
|
||||||
|
"mac": AppleEncoder,
|
||||||
|
"macos": AppleEncoder,
|
||||||
|
"darwin": AppleEncoder,
|
||||||
|
string(AppleEncoder): AppleEncoder,
|
||||||
|
"vaapi": VAAPIEncoder,
|
||||||
|
"libva": VAAPIEncoder,
|
||||||
|
string(VAAPIEncoder): VAAPIEncoder,
|
||||||
|
"nvidia": NvidiaEncoder,
|
||||||
|
"nvenc": NvidiaEncoder,
|
||||||
|
"cuda": NvidiaEncoder,
|
||||||
|
string(NvidiaEncoder): NvidiaEncoder,
|
||||||
|
"v4l2": Video4LinuxEncoder,
|
||||||
|
"v4l": Video4LinuxEncoder,
|
||||||
|
"video4linux": Video4LinuxEncoder,
|
||||||
|
"rp4": Video4LinuxEncoder,
|
||||||
|
"raspberry": Video4LinuxEncoder,
|
||||||
|
"raspberrypi": Video4LinuxEncoder,
|
||||||
|
string(Video4LinuxEncoder): Video4LinuxEncoder,
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindEncoder finds an FFmpeg encoder by name.
|
||||||
|
func FindEncoder(s string) AvcEncoder {
|
||||||
|
if encoder, ok := AvcEncoders[s]; ok {
|
||||||
|
return encoder
|
||||||
|
} else {
|
||||||
|
log.Warnf("ffmpeg: unsupported encoder %s", sanitize.Log(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
return SoftwareEncoder
|
||||||
|
}
|
||||||
33
internal/ffmpeg/ffmpeg.go
Normal file
33
internal/ffmpeg/ffmpeg.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
Package ffmpeg provides FFmpeg video transcoding related types and functions.
|
||||||
|
|
||||||
|
Copyright (c) 2018 - 2022 Michael Mayer <hello@photoprism.app>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under Version 3 of the GNU Affero General Public License (the "AGPL"):
|
||||||
|
<https://docs.photoprism.app/license/agpl>
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
The AGPL is supplemented by our Trademark and Brand Guidelines,
|
||||||
|
which describe how our Brand Assets may be used:
|
||||||
|
<https://photoprism.app/trademark>
|
||||||
|
|
||||||
|
Feel free to send an e-mail to hello@photoprism.app if you have questions,
|
||||||
|
want to support our work, or just want to say hello.
|
||||||
|
|
||||||
|
Additional information can be found in our Developer Guide:
|
||||||
|
<https://docs.photoprism.app/developer-guide/>
|
||||||
|
|
||||||
|
*/
|
||||||
|
package ffmpeg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log = event.Log
|
||||||
@@ -1,25 +1,17 @@
|
|||||||
package photoprism
|
package photoprism
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/karrick/godirwalk"
|
"github.com/karrick/godirwalk"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/internal/event"
|
|
||||||
"github.com/photoprism/photoprism/internal/mutex"
|
"github.com/photoprism/photoprism/internal/mutex"
|
||||||
"github.com/photoprism/photoprism/internal/thumb"
|
|
||||||
"github.com/photoprism/photoprism/pkg/fs"
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
"github.com/photoprism/photoprism/pkg/sanitize"
|
"github.com/photoprism/photoprism/pkg/sanitize"
|
||||||
)
|
)
|
||||||
@@ -130,242 +122,3 @@ func (c *Convert) Start(path string, force bool) (err error) {
|
|||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToJson uses exiftool to export metadata to a json file.
|
|
||||||
func (c *Convert) ToJson(f *MediaFile) (jsonName string, err error) {
|
|
||||||
if f == nil {
|
|
||||||
return "", fmt.Errorf("exiftool: file is nil - you might have found a bug")
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonName, err = f.ExifToolJsonName()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if fs.FileExists(jsonName) {
|
|
||||||
return jsonName, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("exiftool: extracting metadata from %s", sanitize.Log(f.RootRelName()))
|
|
||||||
|
|
||||||
cmd := exec.Command(c.conf.ExifToolBin(), "-n", "-m", "-api", "LargeFileSupport", "-j", f.FileName())
|
|
||||||
|
|
||||||
// Fetch command output.
|
|
||||||
var out bytes.Buffer
|
|
||||||
var stderr bytes.Buffer
|
|
||||||
cmd.Stdout = &out
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
|
|
||||||
// Log exact command for debugging in trace mode.
|
|
||||||
log.Trace(cmd.String())
|
|
||||||
|
|
||||||
// Run convert command.
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
if stderr.String() != "" {
|
|
||||||
return "", errors.New(stderr.String())
|
|
||||||
} else {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write output to file.
|
|
||||||
if err := os.WriteFile(jsonName, []byte(out.String()), os.ModePerm); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if file exists.
|
|
||||||
if !fs.FileExists(jsonName) {
|
|
||||||
return "", fmt.Errorf("exiftool: failed creating %s", filepath.Base(jsonName))
|
|
||||||
}
|
|
||||||
|
|
||||||
return jsonName, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// JpegConvertCommand returns the command for converting files to JPEG, depending on the format.
|
|
||||||
func (c *Convert) JpegConvertCommand(f *MediaFile, jpegName string, xmpName string) (result *exec.Cmd, useMutex bool, err error) {
|
|
||||||
if f == nil {
|
|
||||||
return result, useMutex, fmt.Errorf("file is nil - you might have found a bug")
|
|
||||||
}
|
|
||||||
|
|
||||||
size := strconv.Itoa(c.conf.JpegSize())
|
|
||||||
fileExt := f.Extension()
|
|
||||||
|
|
||||||
if f.IsRaw() {
|
|
||||||
if c.conf.SipsEnabled() {
|
|
||||||
result = exec.Command(c.conf.SipsBin(), "-Z", size, "-s", "format", "jpeg", "--out", jpegName, f.FileName())
|
|
||||||
} else if c.conf.DarktableEnabled() && c.darktableBlacklist.Ok(fileExt) {
|
|
||||||
var args []string
|
|
||||||
|
|
||||||
// Set RAW, XMP, and JPEG filenames.
|
|
||||||
if xmpName != "" {
|
|
||||||
args = []string{f.FileName(), xmpName, jpegName}
|
|
||||||
} else {
|
|
||||||
args = []string{f.FileName(), jpegName}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set RAW to JPEG conversion options.
|
|
||||||
if c.conf.RawPresets() {
|
|
||||||
useMutex = true // can run one instance only with presets enabled
|
|
||||||
args = append(args, "--width", size, "--height", size, "--hq", "true", "--upscale", "false")
|
|
||||||
} else {
|
|
||||||
useMutex = false // --apply-custom-presets=false disables locking
|
|
||||||
args = append(args, "--apply-custom-presets", "false", "--width", size, "--height", size, "--hq", "true", "--upscale", "false")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set Darktable core storage paths.
|
|
||||||
args = append(args, "--core", "--configdir", c.conf.DarktableConfigPath(), "--cachedir", c.conf.DarktableCachePath(), "--library", ":memory:")
|
|
||||||
|
|
||||||
result = exec.Command(c.conf.DarktableBin(), args...)
|
|
||||||
} else if c.conf.RawtherapeeEnabled() && c.rawtherapeeBlacklist.Ok(fileExt) {
|
|
||||||
jpegQuality := fmt.Sprintf("-j%d", c.conf.JpegQuality())
|
|
||||||
profile := filepath.Join(conf.AssetsPath(), "profiles", "raw.pp3")
|
|
||||||
|
|
||||||
args := []string{"-o", jpegName, "-p", profile, "-s", "-d", jpegQuality, "-js3", "-b8", "-c", f.FileName()}
|
|
||||||
|
|
||||||
result = exec.Command(c.conf.RawtherapeeBin(), args...)
|
|
||||||
} else {
|
|
||||||
return nil, useMutex, fmt.Errorf("no suitable converter found")
|
|
||||||
}
|
|
||||||
} else if f.IsVideo() && c.conf.FFmpegEnabled() {
|
|
||||||
result = exec.Command(c.conf.FFmpegBin(), "-y", "-i", f.FileName(), "-ss", "00:00:00.001", "-vframes", "1", jpegName)
|
|
||||||
} else if f.IsHEIF() && c.conf.HeifConvertEnabled() {
|
|
||||||
result = exec.Command(c.conf.HeifConvertBin(), f.FileName(), jpegName)
|
|
||||||
} else {
|
|
||||||
return nil, useMutex, fmt.Errorf("file type %s not supported", f.FileType())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log convert command in trace mode only as it exposes server internals.
|
|
||||||
if result != nil {
|
|
||||||
log.Tracef("convert: %s", result.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, useMutex, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToJpeg converts a single image file to JPEG if possible.
|
|
||||||
func (c *Convert) ToJpeg(f *MediaFile, force bool) (*MediaFile, error) {
|
|
||||||
if f == nil {
|
|
||||||
return nil, fmt.Errorf("convert: file is nil - you might have found a bug")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !f.Exists() {
|
|
||||||
return nil, fmt.Errorf("convert: %s not found", f.RootRelName())
|
|
||||||
}
|
|
||||||
|
|
||||||
if f.IsJpeg() {
|
|
||||||
return f, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
jpegName := fs.FormatJpeg.FindFirst(f.FileName(), []string{c.conf.SidecarPath(), fs.HiddenPath}, c.conf.OriginalsPath(), false)
|
|
||||||
|
|
||||||
mediaFile, err := NewMediaFile(jpegName)
|
|
||||||
|
|
||||||
// Replace existing sidecar if "force" is true.
|
|
||||||
if err == nil && mediaFile.IsJpeg() {
|
|
||||||
if force && mediaFile.InSidecar() {
|
|
||||||
if err := mediaFile.Remove(); err != nil {
|
|
||||||
return mediaFile, fmt.Errorf("convert: failed removing %s (%s)", mediaFile.RootRelName(), err)
|
|
||||||
} else {
|
|
||||||
log.Infof("convert: replacing %s", sanitize.Log(mediaFile.RootRelName()))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return mediaFile, nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
jpegName = fs.FileName(f.FileName(), c.conf.SidecarPath(), c.conf.OriginalsPath(), fs.JpegExt)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !c.conf.SidecarWritable() {
|
|
||||||
return nil, fmt.Errorf("convert: disabled in read only mode (%s)", f.RootRelName())
|
|
||||||
}
|
|
||||||
|
|
||||||
fileName := f.RelName(c.conf.OriginalsPath())
|
|
||||||
xmpName := fs.FormatXMP.Find(f.FileName(), false)
|
|
||||||
|
|
||||||
event.Publish("index.converting", event.Data{
|
|
||||||
"fileType": f.FileType(),
|
|
||||||
"fileName": fileName,
|
|
||||||
"baseName": filepath.Base(fileName),
|
|
||||||
"xmpName": filepath.Base(xmpName),
|
|
||||||
})
|
|
||||||
|
|
||||||
start := time.Now()
|
|
||||||
|
|
||||||
if f.IsImageOther() {
|
|
||||||
log.Infof("convert: converting %s to %s (%s)", sanitize.Log(filepath.Base(fileName)), sanitize.Log(filepath.Base(jpegName)), f.FileType())
|
|
||||||
|
|
||||||
_, err = thumb.Jpeg(f.FileName(), jpegName, f.Orientation())
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("convert: %s created in %s (%s)", sanitize.Log(filepath.Base(jpegName)), time.Since(start), f.FileType())
|
|
||||||
|
|
||||||
return NewMediaFile(jpegName)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd, useMutex, err := c.JpegConvertCommand(f, jpegName, xmpName)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if useMutex {
|
|
||||||
// Make sure only one command is executed at a time.
|
|
||||||
// See https://photo.stackexchange.com/questions/105969/darktable-cli-fails-because-of-locked-database-file
|
|
||||||
c.cmdMutex.Lock()
|
|
||||||
defer c.cmdMutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
if fs.FileExists(jpegName) {
|
|
||||||
return NewMediaFile(jpegName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch command output.
|
|
||||||
var out bytes.Buffer
|
|
||||||
var stderr bytes.Buffer
|
|
||||||
cmd.Stdout = &out
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
|
|
||||||
log.Infof("convert: converting %s to %s (%s)", sanitize.Log(filepath.Base(fileName)), sanitize.Log(filepath.Base(jpegName)), filepath.Base(cmd.Path))
|
|
||||||
|
|
||||||
// Log exact command for debugging in trace mode.
|
|
||||||
log.Trace(cmd.String())
|
|
||||||
|
|
||||||
// Run convert command.
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
if stderr.String() != "" {
|
|
||||||
return nil, errors.New(stderr.String())
|
|
||||||
} else {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("convert: %s created in %s (%s)", sanitize.Log(filepath.Base(jpegName)), time.Since(start), filepath.Base(cmd.Path))
|
|
||||||
|
|
||||||
return NewMediaFile(jpegName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AvcBitrate returns the ideal AVC encoding bitrate in megabits per second.
|
|
||||||
func (c *Convert) AvcBitrate(f *MediaFile) string {
|
|
||||||
const defaultBitrate = "8M"
|
|
||||||
|
|
||||||
if f == nil {
|
|
||||||
return defaultBitrate
|
|
||||||
}
|
|
||||||
|
|
||||||
limit := c.conf.FFmpegBitrate()
|
|
||||||
quality := 12
|
|
||||||
|
|
||||||
bitrate := int(math.Ceil(float64(f.Width()*f.Height()*quality) / 1000000))
|
|
||||||
|
|
||||||
if bitrate <= 0 {
|
|
||||||
return defaultBitrate
|
|
||||||
} else if bitrate > limit {
|
|
||||||
bitrate = limit
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%dM", bitrate)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,196 +4,23 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/event"
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
|
"github.com/photoprism/photoprism/internal/ffmpeg"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/pkg/fs"
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
"github.com/photoprism/photoprism/pkg/sanitize"
|
"github.com/photoprism/photoprism/pkg/sanitize"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FFmpegSoftwareEncoder see https://trac.ffmpeg.org/wiki/HWAccelIntro.
|
|
||||||
const FFmpegSoftwareEncoder = "libx264"
|
|
||||||
|
|
||||||
// FFmpegIntelEncoder is the Intel Quick Sync H.264 encoder.
|
|
||||||
const FFmpegIntelEncoder = "h264_qsv"
|
|
||||||
|
|
||||||
// FFmpegAppleEncoder is the Apple Video Toolboar H.264 encoder.
|
|
||||||
const FFmpegAppleEncoder = "h264_videotoolbox"
|
|
||||||
|
|
||||||
// FFmpegVAAPIEncoder is the Video Acceleration API H.264 encoder.
|
|
||||||
const FFmpegVAAPIEncoder = "h264_vaapi"
|
|
||||||
|
|
||||||
// FFmpegNvidiaEncoder is the NVIDIA H.264 encoder.
|
|
||||||
const FFmpegNvidiaEncoder = "h264_nvenc"
|
|
||||||
|
|
||||||
// FFmpegV4L2Encoder is the Video4Linux H.264 encoder.
|
|
||||||
const FFmpegV4L2Encoder = "h264_v4l2m2m"
|
|
||||||
|
|
||||||
// FFmpegAvcEncoders is the list of supported H.264 encoders with aliases.
|
|
||||||
var FFmpegAvcEncoders = map[string]string{
|
|
||||||
"": FFmpegSoftwareEncoder,
|
|
||||||
"default": FFmpegSoftwareEncoder,
|
|
||||||
"software": FFmpegSoftwareEncoder,
|
|
||||||
FFmpegSoftwareEncoder: FFmpegSoftwareEncoder,
|
|
||||||
"intel": FFmpegIntelEncoder,
|
|
||||||
"qsv": FFmpegIntelEncoder,
|
|
||||||
FFmpegIntelEncoder: FFmpegIntelEncoder,
|
|
||||||
"apple": FFmpegAppleEncoder,
|
|
||||||
"osx": FFmpegAppleEncoder,
|
|
||||||
"mac": FFmpegAppleEncoder,
|
|
||||||
"macos": FFmpegAppleEncoder,
|
|
||||||
"darwin": FFmpegAppleEncoder,
|
|
||||||
FFmpegAppleEncoder: FFmpegAppleEncoder,
|
|
||||||
"vaapi": FFmpegVAAPIEncoder,
|
|
||||||
"libva": FFmpegVAAPIEncoder,
|
|
||||||
FFmpegVAAPIEncoder: FFmpegVAAPIEncoder,
|
|
||||||
"nvidia": FFmpegNvidiaEncoder,
|
|
||||||
"nvenc": FFmpegNvidiaEncoder,
|
|
||||||
"cuda": FFmpegNvidiaEncoder,
|
|
||||||
FFmpegNvidiaEncoder: FFmpegNvidiaEncoder,
|
|
||||||
"v4l2": FFmpegV4L2Encoder,
|
|
||||||
"v4l": FFmpegV4L2Encoder,
|
|
||||||
"video4linux": FFmpegV4L2Encoder,
|
|
||||||
"rp4": FFmpegV4L2Encoder,
|
|
||||||
"raspberry": FFmpegV4L2Encoder,
|
|
||||||
"raspberrypi": FFmpegV4L2Encoder,
|
|
||||||
FFmpegV4L2Encoder: FFmpegV4L2Encoder,
|
|
||||||
}
|
|
||||||
|
|
||||||
// AvcConvertCommand returns the command for converting video files to MPEG-4 AVC.
|
|
||||||
func (c *Convert) AvcConvertCommand(f *MediaFile, avcName, encoderName string) (result *exec.Cmd, useMutex bool, err error) {
|
|
||||||
if f.IsVideo() {
|
|
||||||
// Don't transcode more than one video at the same time.
|
|
||||||
useMutex = true
|
|
||||||
|
|
||||||
// Display encoder info.
|
|
||||||
if encoderName != FFmpegSoftwareEncoder {
|
|
||||||
log.Infof("convert: ffmpeg encoder %s selected", encoderName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if encoderName == FFmpegIntelEncoder {
|
|
||||||
format := "format=rgb32"
|
|
||||||
|
|
||||||
// Options: ffmpeg -hide_banner -h encoder=h264_qsv
|
|
||||||
result = exec.Command(
|
|
||||||
c.conf.FFmpegBin(),
|
|
||||||
"-qsv_device", "/dev/dri/renderD128",
|
|
||||||
"-i", f.FileName(),
|
|
||||||
"-c:a", "aac",
|
|
||||||
"-vf", format,
|
|
||||||
"-c:v", encoderName,
|
|
||||||
"-vsync", "vfr",
|
|
||||||
"-r", "30",
|
|
||||||
"-b:v", c.AvcBitrate(f),
|
|
||||||
"-maxrate", c.AvcBitrate(f),
|
|
||||||
"-f", "mp4",
|
|
||||||
"-y",
|
|
||||||
avcName,
|
|
||||||
)
|
|
||||||
} else if encoderName == FFmpegAppleEncoder {
|
|
||||||
format := "format=yuv420p"
|
|
||||||
|
|
||||||
// Options: ffmpeg -hide_banner -h encoder=h264_videotoolbox
|
|
||||||
result = exec.Command(
|
|
||||||
c.conf.FFmpegBin(),
|
|
||||||
"-i", f.FileName(),
|
|
||||||
"-c:v", encoderName,
|
|
||||||
"-c:a", "aac",
|
|
||||||
"-vf", format,
|
|
||||||
"-profile", "high",
|
|
||||||
"-level", "51",
|
|
||||||
"-vsync", "vfr",
|
|
||||||
"-r", "30",
|
|
||||||
"-b:v", c.AvcBitrate(f),
|
|
||||||
"-f", "mp4",
|
|
||||||
"-y",
|
|
||||||
avcName,
|
|
||||||
)
|
|
||||||
} else if encoderName == FFmpegNvidiaEncoder {
|
|
||||||
// Options: ffmpeg -hide_banner -h encoder=h264_nvenc
|
|
||||||
result = exec.Command(
|
|
||||||
c.conf.FFmpegBin(),
|
|
||||||
"-r", "30",
|
|
||||||
"-i", f.FileName(),
|
|
||||||
"-pix_fmt", "yuv420p",
|
|
||||||
"-c:v", encoderName,
|
|
||||||
"-c:a", "aac",
|
|
||||||
"-preset", "15",
|
|
||||||
"-pixel_format", "yuv420p",
|
|
||||||
"-gpu", "any",
|
|
||||||
"-vf", "format=yuv420p",
|
|
||||||
"-rc:v", "constqp",
|
|
||||||
"-cq", "0",
|
|
||||||
"-tune", "2",
|
|
||||||
"-b:v", c.AvcBitrate(f),
|
|
||||||
"-profile:v", "1",
|
|
||||||
"-level:v", "41",
|
|
||||||
"-coder:v", "1",
|
|
||||||
"-f", "mp4",
|
|
||||||
"-y",
|
|
||||||
avcName,
|
|
||||||
)
|
|
||||||
} else if encoderName == FFmpegV4L2Encoder {
|
|
||||||
format := "format=yuv420p"
|
|
||||||
|
|
||||||
// Options: ffmpeg -hide_banner -h encoder=h264_v4l2m2m
|
|
||||||
result = exec.Command(
|
|
||||||
c.conf.FFmpegBin(),
|
|
||||||
"-i", f.FileName(),
|
|
||||||
"-c:v", encoderName,
|
|
||||||
"-c:a", "aac",
|
|
||||||
"-vf", format,
|
|
||||||
"-num_output_buffers", "72",
|
|
||||||
"-num_capture_buffers", "64",
|
|
||||||
"-max_muxing_queue_size", "1024",
|
|
||||||
"-crf", "23",
|
|
||||||
"-vsync", "vfr",
|
|
||||||
"-r", "30",
|
|
||||||
"-b:v", c.AvcBitrate(f),
|
|
||||||
"-f", "mp4",
|
|
||||||
"-y",
|
|
||||||
avcName,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
format := "format=yuv420p"
|
|
||||||
|
|
||||||
result = exec.Command(
|
|
||||||
c.conf.FFmpegBin(),
|
|
||||||
"-i", f.FileName(),
|
|
||||||
"-c:v", encoderName,
|
|
||||||
"-c:a", "aac",
|
|
||||||
"-vf", format,
|
|
||||||
"-max_muxing_queue_size", "1024",
|
|
||||||
"-crf", "23",
|
|
||||||
"-vsync", "vfr",
|
|
||||||
"-r", "30",
|
|
||||||
"-b:v", c.AvcBitrate(f),
|
|
||||||
"-f", "mp4",
|
|
||||||
"-y",
|
|
||||||
avcName,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return nil, useMutex, fmt.Errorf("convert: file type %s not supported in %s", f.FileType(), sanitize.Log(f.BaseName()))
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, useMutex, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToAvc converts a single video file to MPEG-4 AVC.
|
// ToAvc converts a single video file to MPEG-4 AVC.
|
||||||
func (c *Convert) ToAvc(f *MediaFile, encoderName string) (file *MediaFile, err error) {
|
func (c *Convert) ToAvc(f *MediaFile, encoder ffmpeg.AvcEncoder, noMutex, force bool) (file *MediaFile, err error) {
|
||||||
if n := FFmpegAvcEncoders[encoderName]; n != "" {
|
|
||||||
encoderName = n
|
|
||||||
} else {
|
|
||||||
log.Warnf("convert: unsupported ffmpeg encoder %s", encoderName)
|
|
||||||
encoderName = FFmpegSoftwareEncoder
|
|
||||||
}
|
|
||||||
|
|
||||||
if f == nil {
|
if f == nil {
|
||||||
return nil, fmt.Errorf("convert: file is nil - you might have found a bug")
|
return nil, fmt.Errorf("convert: file is nil - possible bug")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !f.Exists() {
|
if !f.Exists() {
|
||||||
@@ -219,22 +46,30 @@ func (c *Convert) ToAvc(f *MediaFile, encoderName string) (file *MediaFile, err
|
|||||||
fileName := f.RelName(c.conf.OriginalsPath())
|
fileName := f.RelName(c.conf.OriginalsPath())
|
||||||
avcName = fs.FileName(f.FileName(), c.conf.SidecarPath(), c.conf.OriginalsPath(), fs.AvcExt)
|
avcName = fs.FileName(f.FileName(), c.conf.SidecarPath(), c.conf.OriginalsPath(), fs.AvcExt)
|
||||||
|
|
||||||
cmd, useMutex, err := c.AvcConvertCommand(f, avcName, encoderName)
|
cmd, useMutex, err := c.AvcConvertCommand(f, avcName, encoder)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if useMutex {
|
// Make sure only one convert command runs at a time.
|
||||||
// Make sure only one command is executed at a time.
|
if useMutex && !noMutex {
|
||||||
// See https://photo.stackexchange.com/questions/105969/darktable-cli-fails-because-of-locked-database-file
|
|
||||||
c.cmdMutex.Lock()
|
c.cmdMutex.Lock()
|
||||||
defer c.cmdMutex.Unlock()
|
defer c.cmdMutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
if fs.FileExists(avcName) {
|
if fs.FileExists(avcName) {
|
||||||
return NewMediaFile(avcName)
|
avcFile, avcErr := NewMediaFile(avcName)
|
||||||
|
if avcErr != nil {
|
||||||
|
return avcFile, avcErr
|
||||||
|
} else if !force || !avcFile.InSidecar() {
|
||||||
|
return avcFile, nil
|
||||||
|
} else if err = avcFile.Remove(); err != nil {
|
||||||
|
return avcFile, fmt.Errorf("convert: failed removing %s (%s)", sanitize.Log(avcFile.RootRelName()), err)
|
||||||
|
} else {
|
||||||
|
log.Infof("convert: replacing %s", sanitize.Log(avcFile.RootRelName()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch command output.
|
// Fetch command output.
|
||||||
@@ -250,7 +85,7 @@ func (c *Convert) ToAvc(f *MediaFile, encoderName string) (file *MediaFile, err
|
|||||||
"xmpName": "",
|
"xmpName": "",
|
||||||
})
|
})
|
||||||
|
|
||||||
log.Infof("%s: transcoding %s to %s", encoderName, fileName, fs.FormatAvc)
|
log.Infof("%s: transcoding %s to %s", encoder, fileName, fs.FormatAvc)
|
||||||
|
|
||||||
// Log exact command for debugging in trace mode.
|
// Log exact command for debugging in trace mode.
|
||||||
log.Trace(cmd.String())
|
log.Trace(cmd.String())
|
||||||
@@ -258,8 +93,6 @@ func (c *Convert) ToAvc(f *MediaFile, encoderName string) (file *MediaFile, err
|
|||||||
// Run convert command.
|
// Run convert command.
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
if err = cmd.Run(); err != nil {
|
if err = cmd.Run(); err != nil {
|
||||||
_ = os.Remove(avcName)
|
|
||||||
|
|
||||||
if stderr.String() != "" {
|
if stderr.String() != "" {
|
||||||
err = errors.New(stderr.String())
|
err = errors.New(stderr.String())
|
||||||
}
|
}
|
||||||
@@ -270,17 +103,67 @@ func (c *Convert) ToAvc(f *MediaFile, encoderName string) (file *MediaFile, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Log filename and transcoding time.
|
// Log filename and transcoding time.
|
||||||
log.Warnf("%s: failed transcoding %s [%s]", encoderName, fileName, time.Since(start))
|
log.Warnf("%s: failed transcoding %s [%s]", encoder, fileName, time.Since(start))
|
||||||
|
|
||||||
if encoderName != FFmpegSoftwareEncoder {
|
// Remove broken video file.
|
||||||
return c.ToAvc(f, FFmpegSoftwareEncoder)
|
if !fs.FileExists(avcName) {
|
||||||
|
// Do nothing.
|
||||||
|
} else if err = os.Remove(avcName); err != nil {
|
||||||
|
return nil, fmt.Errorf("convert: failed removing %s (%s)", sanitize.Log(RootRelName(avcName)), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try again using software encoder.
|
||||||
|
if encoder != ffmpeg.SoftwareEncoder {
|
||||||
|
return c.ToAvc(f, ffmpeg.SoftwareEncoder, true, false)
|
||||||
} else {
|
} else {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log transcoding time.
|
// Log transcoding time.
|
||||||
log.Infof("%s: created %s [%s]", encoderName, filepath.Base(avcName), time.Since(start))
|
log.Infof("%s: created %s [%s]", encoder, filepath.Base(avcName), time.Since(start))
|
||||||
|
|
||||||
return NewMediaFile(avcName)
|
return NewMediaFile(avcName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AvcConvertCommand returns the command for converting video files to MPEG-4 AVC.
|
||||||
|
func (c *Convert) AvcConvertCommand(f *MediaFile, avcName string, encoder ffmpeg.AvcEncoder) (result *exec.Cmd, useMutex bool, err error) {
|
||||||
|
fileName := f.FileName()
|
||||||
|
bitrate := c.AvcBitrate(f)
|
||||||
|
ffmpegBin := c.conf.FFmpegBin()
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case fileName == "":
|
||||||
|
return nil, false, fmt.Errorf("convert: %s video filename is empty - possible bug", f.FileType())
|
||||||
|
case bitrate == "":
|
||||||
|
return nil, false, fmt.Errorf("convert: transcoding bitrate is empty - possible bug")
|
||||||
|
case ffmpegBin == "":
|
||||||
|
return nil, false, fmt.Errorf("convert: ffmpeg must be installed to transcode %s to avc", sanitize.Log(f.BaseName()))
|
||||||
|
case !f.IsVideo():
|
||||||
|
return nil, false, fmt.Errorf("convert: file type %s of %s cannot be transcoded to avc", f.FileType(), sanitize.Log(f.BaseName()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return ffmpeg.AvcConvertCommand(fileName, avcName, ffmpegBin, c.AvcBitrate(f), encoder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AvcBitrate returns the ideal AVC encoding bitrate in megabits per second.
|
||||||
|
func (c *Convert) AvcBitrate(f *MediaFile) string {
|
||||||
|
const defaultBitrate = "8M"
|
||||||
|
|
||||||
|
if f == nil {
|
||||||
|
return defaultBitrate
|
||||||
|
}
|
||||||
|
|
||||||
|
limit := c.conf.FFmpegBitrate()
|
||||||
|
quality := 12
|
||||||
|
|
||||||
|
bitrate := int(math.Ceil(float64(f.Width()*f.Height()*quality) / 1000000))
|
||||||
|
|
||||||
|
if bitrate <= 0 {
|
||||||
|
return defaultBitrate
|
||||||
|
} else if bitrate > limit {
|
||||||
|
bitrate = limit
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%dM", bitrate)
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
"github.com/photoprism/photoprism/pkg/fs"
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConvert_ToAvc(t *testing.T) {
|
func TestConvert_ToAvc(t *testing.T) {
|
||||||
@@ -28,7 +29,7 @@ func TestConvert_ToAvc(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
avcFile, err := convert.ToAvc(mf, "")
|
avcFile, err := convert.ToAvc(mf, "", false, false)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -59,7 +60,7 @@ func TestConvert_ToAvc(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
avcFile, err := convert.ToAvc(mf, "")
|
avcFile, err := convert.ToAvc(mf, "", false, false)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Nil(t, avcFile)
|
assert.Nil(t, avcFile)
|
||||||
})
|
})
|
||||||
|
|||||||
186
internal/photoprism/convert_jpeg.go
Normal file
186
internal/photoprism/convert_jpeg.go
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
package photoprism
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
|
"github.com/photoprism/photoprism/internal/thumb"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
|
"github.com/photoprism/photoprism/pkg/sanitize"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToJpeg converts a single image file to JPEG if possible.
|
||||||
|
func (c *Convert) ToJpeg(f *MediaFile, force bool) (*MediaFile, error) {
|
||||||
|
if f == nil {
|
||||||
|
return nil, fmt.Errorf("convert: file is nil - possible bug")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !f.Exists() {
|
||||||
|
return nil, fmt.Errorf("convert: %s not found", sanitize.Log(f.RootRelName()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.IsJpeg() {
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
jpegName := fs.FormatJpeg.FindFirst(f.FileName(), []string{c.conf.SidecarPath(), fs.HiddenPath}, c.conf.OriginalsPath(), false)
|
||||||
|
|
||||||
|
mediaFile, err := NewMediaFile(jpegName)
|
||||||
|
|
||||||
|
// Replace existing sidecar if "force" is true.
|
||||||
|
if err == nil && mediaFile.IsJpeg() {
|
||||||
|
if force && mediaFile.InSidecar() {
|
||||||
|
if err := mediaFile.Remove(); err != nil {
|
||||||
|
return mediaFile, fmt.Errorf("convert: failed removing %s (%s)", sanitize.Log(mediaFile.RootRelName()), err)
|
||||||
|
} else {
|
||||||
|
log.Infof("convert: replacing %s", sanitize.Log(mediaFile.RootRelName()))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return mediaFile, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
jpegName = fs.FileName(f.FileName(), c.conf.SidecarPath(), c.conf.OriginalsPath(), fs.JpegExt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.conf.SidecarWritable() {
|
||||||
|
return nil, fmt.Errorf("convert: disabled in read only mode (%s)", sanitize.Log(f.RootRelName()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fileName := f.RelName(c.conf.OriginalsPath())
|
||||||
|
xmpName := fs.FormatXMP.Find(f.FileName(), false)
|
||||||
|
|
||||||
|
event.Publish("index.converting", event.Data{
|
||||||
|
"fileType": f.FileType(),
|
||||||
|
"fileName": fileName,
|
||||||
|
"baseName": filepath.Base(fileName),
|
||||||
|
"xmpName": filepath.Base(xmpName),
|
||||||
|
})
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
if f.IsImageOther() {
|
||||||
|
log.Infof("convert: converting %s to %s (%s)", sanitize.Log(filepath.Base(fileName)), sanitize.Log(filepath.Base(jpegName)), f.FileType())
|
||||||
|
|
||||||
|
_, err = thumb.Jpeg(f.FileName(), jpegName, f.Orientation())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("convert: %s created in %s (%s)", sanitize.Log(filepath.Base(jpegName)), time.Since(start), f.FileType())
|
||||||
|
|
||||||
|
return NewMediaFile(jpegName)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd, useMutex, err := c.JpegConvertCommand(f, jpegName, xmpName)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if useMutex {
|
||||||
|
// Make sure only one command is executed at a time.
|
||||||
|
// See https://photo.stackexchange.com/questions/105969/darktable-cli-fails-because-of-locked-database-file
|
||||||
|
c.cmdMutex.Lock()
|
||||||
|
defer c.cmdMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
if fs.FileExists(jpegName) {
|
||||||
|
return NewMediaFile(jpegName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch command output.
|
||||||
|
var out bytes.Buffer
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &out
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
log.Infof("convert: converting %s to %s (%s)", sanitize.Log(filepath.Base(fileName)), sanitize.Log(filepath.Base(jpegName)), filepath.Base(cmd.Path))
|
||||||
|
|
||||||
|
// Log exact command for debugging in trace mode.
|
||||||
|
log.Trace(cmd.String())
|
||||||
|
|
||||||
|
// Run convert command.
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
if stderr.String() != "" {
|
||||||
|
return nil, errors.New(stderr.String())
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("convert: %s created in %s (%s)", sanitize.Log(filepath.Base(jpegName)), time.Since(start), filepath.Base(cmd.Path))
|
||||||
|
|
||||||
|
return NewMediaFile(jpegName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JpegConvertCommand returns the command for converting files to JPEG, depending on the format.
|
||||||
|
func (c *Convert) JpegConvertCommand(f *MediaFile, jpegName string, xmpName string) (result *exec.Cmd, useMutex bool, err error) {
|
||||||
|
if f == nil {
|
||||||
|
return result, useMutex, fmt.Errorf("file is nil - possible bug")
|
||||||
|
}
|
||||||
|
|
||||||
|
fileExt := f.Extension()
|
||||||
|
maxSize := strconv.Itoa(c.conf.JpegSize())
|
||||||
|
|
||||||
|
// Select conversion command depending on the file type and runtime environment.
|
||||||
|
if c.conf.SipsEnabled() && (f.IsRaw() || f.IsHEIF()) {
|
||||||
|
result = exec.Command(c.conf.SipsBin(), "-Z", maxSize, "-s", "format", "jpeg", "--out", jpegName, f.FileName())
|
||||||
|
} else if f.IsRaw() && c.conf.RawEnabled() {
|
||||||
|
if c.conf.DarktableEnabled() && c.darktableBlacklist.Ok(fileExt) {
|
||||||
|
cachePath, configPath := conf.DarktableCachePath(), conf.DarktableConfigPath()
|
||||||
|
|
||||||
|
var args []string
|
||||||
|
|
||||||
|
// Set RAW, XMP, and JPEG filenames.
|
||||||
|
if xmpName != "" {
|
||||||
|
args = []string{f.FileName(), xmpName, jpegName}
|
||||||
|
} else {
|
||||||
|
args = []string{f.FileName(), jpegName}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set RAW to JPEG conversion options.
|
||||||
|
if c.conf.RawPresets() {
|
||||||
|
useMutex = true // can run one instance only with presets enabled
|
||||||
|
args = append(args, "--width", maxSize, "--height", maxSize, "--hq", "true", "--upscale", "false")
|
||||||
|
} else {
|
||||||
|
useMutex = false // --apply-custom-presets=false disables locking
|
||||||
|
args = append(args, "--apply-custom-presets", "false", "--width", maxSize, "--height", maxSize, "--hq", "true", "--upscale", "false")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set Darktable core storage paths.
|
||||||
|
args = append(args, "--core", "--configdir", configPath, "--cachedir", cachePath, "--library", ":memory:")
|
||||||
|
|
||||||
|
result = exec.Command(c.conf.DarktableBin(), args...)
|
||||||
|
} else if c.conf.RawtherapeeEnabled() && c.rawtherapeeBlacklist.Ok(fileExt) {
|
||||||
|
jpegQuality := fmt.Sprintf("-j%d", c.conf.JpegQuality())
|
||||||
|
profile := filepath.Join(conf.AssetsPath(), "profiles", "raw.pp3")
|
||||||
|
|
||||||
|
args := []string{"-o", jpegName, "-p", profile, "-s", "-d", jpegQuality, "-js3", "-b8", "-c", f.FileName()}
|
||||||
|
|
||||||
|
result = exec.Command(c.conf.RawtherapeeBin(), args...)
|
||||||
|
} else {
|
||||||
|
return nil, useMutex, fmt.Errorf("no suitable converter found")
|
||||||
|
}
|
||||||
|
} else if f.IsVideo() && c.conf.FFmpegEnabled() {
|
||||||
|
result = exec.Command(c.conf.FFmpegBin(), "-y", "-i", f.FileName(), "-ss", "00:00:00.001", "-vframes", "1", jpegName)
|
||||||
|
} else if f.IsHEIF() && c.conf.HeifConvertEnabled() {
|
||||||
|
result = exec.Command(c.conf.HeifConvertBin(), f.FileName(), jpegName)
|
||||||
|
} else {
|
||||||
|
return nil, useMutex, fmt.Errorf("file type %s not supported", f.FileType())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log convert command in trace mode only as it exposes server internals.
|
||||||
|
if result != nil {
|
||||||
|
log.Tracef("convert: %s", result.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, useMutex, nil
|
||||||
|
}
|
||||||
64
internal/photoprism/convert_json.go
Normal file
64
internal/photoprism/convert_json.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package photoprism
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
|
"github.com/photoprism/photoprism/pkg/sanitize"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToJson uses exiftool to export metadata to a json file.
|
||||||
|
func (c *Convert) ToJson(f *MediaFile) (jsonName string, err error) {
|
||||||
|
if f == nil {
|
||||||
|
return "", fmt.Errorf("exiftool: file is nil - possible bug")
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonName, err = f.ExifToolJsonName()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if fs.FileExists(jsonName) {
|
||||||
|
return jsonName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("exiftool: extracting metadata from %s", sanitize.Log(f.RootRelName()))
|
||||||
|
|
||||||
|
cmd := exec.Command(c.conf.ExifToolBin(), "-n", "-m", "-api", "LargeFileSupport", "-j", f.FileName())
|
||||||
|
|
||||||
|
// Fetch command output.
|
||||||
|
var out bytes.Buffer
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &out
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
// Log exact command for debugging in trace mode.
|
||||||
|
log.Trace(cmd.String())
|
||||||
|
|
||||||
|
// Run convert command.
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
if stderr.String() != "" {
|
||||||
|
return "", errors.New(stderr.String())
|
||||||
|
} else {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write output to file.
|
||||||
|
if err := os.WriteFile(jsonName, []byte(out.String()), os.ModePerm); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if file exists.
|
||||||
|
if !fs.FileExists(jsonName) {
|
||||||
|
return "", fmt.Errorf("exiftool: failed creating %s", filepath.Base(jsonName))
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonName, err
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ func ConvertWorker(jobs <-chan ConvertJob) {
|
|||||||
logError(err, job)
|
logError(err, job)
|
||||||
} else if metaData := job.file.MetaData(); metaData.CodecAvc() {
|
} else if metaData := job.file.MetaData(); metaData.CodecAvc() {
|
||||||
continue
|
continue
|
||||||
} else if _, err := job.convert.ToAvc(job.file, job.convert.conf.FFmpegEncoder()); err != nil {
|
} else if _, err := job.convert.ToAvc(job.file, job.convert.conf.FFmpegEncoder(), false, false); err != nil {
|
||||||
logError(err, job)
|
logError(err, job)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ func (w *Faces) Cluster(opt FacesOptions) (added entity.Faces, err error) {
|
|||||||
|
|
||||||
for _, cluster := range results {
|
for _, cluster := range results {
|
||||||
if f := entity.NewFace("", entity.SrcAuto, cluster); f == nil {
|
if f := entity.NewFace("", entity.SrcAuto, cluster); f == nil {
|
||||||
log.Errorf("faces: face should not be nil - bug?")
|
log.Errorf("faces: face should not be nil - possible bug")
|
||||||
} else if f.SkipMatching() {
|
} else if f.SkipMatching() {
|
||||||
log.Infof("faces: skipped cluster %s, embedding not distinct enough", f.ID)
|
log.Infof("faces: skipped cluster %s, embedding not distinct enough", f.ID)
|
||||||
} else if err := f.Create(); err == nil {
|
} else if err := f.Create(); err == nil {
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ func (imp *Import) Start(opt ImportOptions) fs.Done {
|
|||||||
|
|
||||||
convert := imp.conf.Settings().Index.Convert && imp.conf.SidecarWritable()
|
convert := imp.conf.Settings().Index.Convert && imp.conf.SidecarWritable()
|
||||||
indexOpt := NewIndexOptions("/", true, convert, true, false)
|
indexOpt := NewIndexOptions("/", true, convert, true, false)
|
||||||
|
skipRaw := imp.conf.DisableRaw()
|
||||||
ignore := fs.NewIgnoreList(fs.IgnoreFile, true, false)
|
ignore := fs.NewIgnoreList(fs.IgnoreFile, true, false)
|
||||||
|
|
||||||
if err := ignore.Dir(importPath); err != nil {
|
if err := ignore.Dir(importPath); err != nil {
|
||||||
@@ -156,20 +156,23 @@ func (imp *Import) Start(opt ImportOptions) fs.Done {
|
|||||||
|
|
||||||
mf, err := NewMediaFile(fileName)
|
mf, err := NewMediaFile(fileName)
|
||||||
|
|
||||||
|
// Check if file exists and is not empty.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Warnf("import: %s", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if mf.FileSize() == 0 {
|
// Ignore RAW images?
|
||||||
log.Infof("import: skipped empty file %s", sanitize.Log(mf.BaseName()))
|
if mf.IsRaw() && skipRaw {
|
||||||
|
log.Infof("import: skipped raw %s", sanitize.Log(mf.RootRelName()))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find related files to import.
|
||||||
related, err := mf.RelatedFiles(imp.conf.Settings().StackSequences())
|
related, err := mf.RelatedFiles(imp.conf.Settings().StackSequences())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
event.Error(fmt.Sprintf("import: %s", err.Error()))
|
event.Error(fmt.Sprintf("import: %s", err.Error()))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ func (ind *Index) Start(o IndexOptions) fs.Done {
|
|||||||
defer ind.files.Done()
|
defer ind.files.Done()
|
||||||
|
|
||||||
filesIndexed := 0
|
filesIndexed := 0
|
||||||
|
skipRaw := ind.conf.DisableRaw()
|
||||||
ignore := fs.NewIgnoreList(fs.IgnoreFile, true, false)
|
ignore := fs.NewIgnoreList(fs.IgnoreFile, true, false)
|
||||||
|
|
||||||
if err := ignore.Dir(originalsPath); err != nil {
|
if err := ignore.Dir(originalsPath); err != nil {
|
||||||
@@ -176,25 +177,28 @@ func (ind *Index) Start(o IndexOptions) fs.Done {
|
|||||||
|
|
||||||
mf, err := NewMediaFile(fileName)
|
mf, err := NewMediaFile(fileName)
|
||||||
|
|
||||||
|
// Check if file exists and is not empty.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Errorf("index: %s", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if mf.FileSize() == 0 {
|
// Ignore RAW images?
|
||||||
log.Infof("index: skipped empty file %s", sanitize.Log(mf.BaseName()))
|
if mf.IsRaw() && skipRaw {
|
||||||
|
log.Infof("index: skipped raw %s", sanitize.Log(mf.RootRelName()))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip?
|
||||||
if ind.files.Indexed(relName, entity.RootOriginals, mf.modTime, o.Rescan) {
|
if ind.files.Indexed(relName, entity.RootOriginals, mf.modTime, o.Rescan) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find related files to index.
|
||||||
related, err := mf.RelatedFiles(ind.conf.Settings().StackSequences())
|
related, err := mf.RelatedFiles(ind.conf.Settings().StackSequences())
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("index: %s", err.Error())
|
log.Warnf("index: %s", err.Error())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import (
|
|||||||
// MediaFile indexes a single media file.
|
// MediaFile indexes a single media file.
|
||||||
func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID string) (result IndexResult) {
|
func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID string) (result IndexResult) {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
err := errors.New("index: media file is nil - you might have found a bug")
|
err := errors.New("index: media file is nil - possible bug")
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
result.Err = err
|
result.Err = err
|
||||||
result.Status = IndexFailed
|
result.Status = IndexFailed
|
||||||
|
|||||||
@@ -74,8 +74,11 @@ func NewMediaFile(fileName string) (*MediaFile, error) {
|
|||||||
height: -1,
|
height: -1,
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, _, err := m.Stat(); err != nil {
|
// Check if file exists and is not empty.
|
||||||
return m, fmt.Errorf("media: %s not found", sanitize.Log(m.BaseName()))
|
if size, _, err := m.Stat(); err != nil {
|
||||||
|
return m, fmt.Errorf("%s not found", sanitize.Log(m.RootRelName()))
|
||||||
|
} else if size == 0 {
|
||||||
|
return m, fmt.Errorf("%s is empty", sanitize.Log(m.RootRelName()))
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
@@ -297,6 +300,9 @@ func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err e
|
|||||||
sidecarPrefix := Config().SidecarPath() + "/"
|
sidecarPrefix := Config().SidecarPath() + "/"
|
||||||
originalsPrefix := Config().OriginalsPath() + "/"
|
originalsPrefix := Config().OriginalsPath() + "/"
|
||||||
|
|
||||||
|
// Ignore RAW images?
|
||||||
|
skipRaw := Config().DisableRaw()
|
||||||
|
|
||||||
// Replace sidecar with originals path in search prefix.
|
// Replace sidecar with originals path in search prefix.
|
||||||
if len(sidecarPrefix) > 1 && sidecarPrefix != originalsPrefix && strings.HasPrefix(prefix, sidecarPrefix) {
|
if len(sidecarPrefix) > 1 && sidecarPrefix != originalsPrefix && strings.HasPrefix(prefix, sidecarPrefix) {
|
||||||
prefix = strings.Replace(prefix, sidecarPrefix, originalsPrefix, 1)
|
prefix = strings.Replace(prefix, sidecarPrefix, originalsPrefix, 1)
|
||||||
@@ -326,15 +332,16 @@ func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err e
|
|||||||
isHEIF := false
|
isHEIF := false
|
||||||
|
|
||||||
for _, fileName := range matches {
|
for _, fileName := range matches {
|
||||||
f, err := NewMediaFile(fileName)
|
f, fileErr := NewMediaFile(fileName)
|
||||||
|
|
||||||
if err != nil {
|
if fileErr != nil {
|
||||||
log.Warnf("media: %s in %s", err, sanitize.Log(filepath.Base(fileName)))
|
log.Warn(fileErr)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.FileSize() == 0 {
|
// Ignore RAW images?
|
||||||
log.Warnf("media: %s is empty", sanitize.Log(filepath.Base(fileName)))
|
if f.IsRaw() && skipRaw {
|
||||||
|
log.Debugf("media: skipped related raw file %s", sanitize.Log(f.RootRelName()))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1742,10 +1742,10 @@ func TestMediaFile_Megapixels(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("2018-04-12 19_24_49.mov", func(t *testing.T) {
|
t.Run("2018-04-12 19_24_49.mov", func(t *testing.T) {
|
||||||
if f, err := NewMediaFile("testdata/2018-04-12 19_24_49.mov"); err != nil {
|
if _, err := NewMediaFile("testdata/2018-04-12 19_24_49.mov"); err != nil {
|
||||||
t.Fatal(err)
|
assert.EqualError(t, err, "'testdata/2018-04-12 19_24_49.mov' is empty")
|
||||||
} else {
|
} else {
|
||||||
assert.Equal(t, 0, f.Megapixels())
|
t.Errorf("error expected")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("rotate/6.png", func(t *testing.T) {
|
t.Run("rotate/6.png", func(t *testing.T) {
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ func (worker *Sync) refresh(a entity.Account) (complete bool, err error) {
|
|||||||
f = entity.FirstOrCreateFileSync(f)
|
f = entity.FirstOrCreateFileSync(f)
|
||||||
|
|
||||||
if f == nil {
|
if f == nil {
|
||||||
log.Errorf("sync: file sync entity should not be nil - bug?")
|
log.Errorf("sync: file sync entity should not be nil - possible bug")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user